Compare commits

...

789 Commits

Author SHA1 Message Date
Bernhard Kirchen
35491cafed
Prepare Release 2024.05.06 (merge development into master) 2024-05-06 11:54:31 +02:00
Alexander Kukushkin
2f7e1f3f70
Fix: solar_passthrough_losses setting persistence (#957)
Due to a typo the value of `solar_passtrough_losses` was lost after a restart.
2024-05-06 11:12:25 +02:00
Bernhard Kirchen
b7a8bdf07b remove rweather/Crypt lib from firmware (save 2.1k of flash)
mbedtls is already integral part of the firmware. use it in favor of
rweather/Crypto library to calculate a sha256 checksum of a string, as
used in the HTTP power meter implementation.
2024-05-06 11:04:24 +02:00
Bernhard Kirchen
1dd64a57fd remove FirebaseJson lib from firmware (save 17.5k of flash)
we used this library solely to interpret the answer of an HTTP web
server as JSON and find a particular value using a path expression in
the HTTP power meter implementation.

since we ran out of flash memory on non-S3 ESP32, we need to cut some
corners. removing FirebaseJson is the last low-hanging fruit that we
currently know of. we can get rid of it by using ArduinoJson (which is
already integral part of the firmware) and implementing a custom logic
to extract a value based on a path expression.

other than the FirebaseJson path "finder", the new implementation
only knows how to access sub-keys delimited by a forward slash. in
particular, accessing array members is not supported any more. I am
hoping that this is simply not an issue. if so, we will have users
complaining and we can add this functionality in a later release.
2024-05-06 11:04:24 +02:00
Bernhard Kirchen
d2990bd8fd
Prepare Release 2024.05.03 (merge development into master) 2024-05-03 21:48:11 +02:00
SW-Nico
6620ab487a Fix: VE.Direct: take the load current into account
when calculating efficiency, we need to take into account that the load
might also sink a significant amount of current and power, which adds to
the total output of the charge controller.
2024-05-02 21:46:57 +02:00
eu-gh
686b5df64e
Feature: Publish Huawei AC charger mode via MQTT (#876) 2024-05-02 21:19:25 +02:00
Bernhard Kirchen
744df41b01
Merge pull request #945 from helgeerbe/merge-v24.4.24
Merge upstream v24.4.24
2024-05-02 21:07:03 +02:00
Bernhard Kirchen
18dab3cf1c Merge upstream tag 'v24.4.24' into development 2024-05-02 20:49:41 +02:00
Bernhard Kirchen
dae50ec3b7 workflow: ignore v** tags
this is an attempt to exempt the whole build workflow (and respective releases from being created) for upstream tags.
2024-04-29 21:32:37 +02:00
Bernhard Kirchen
4cf596eb5a
Merge pull request #923 from helgeerbe/merge-v24.4.12
merge upstream tag v24.4.12, resolve conflicts (helgeerbe), fix eslint errors (schlimmchen) and adopt new web api method to save code duplication (schlimmchen).
2024-04-29 21:17:14 +02:00
Bernhard Kirchen
4e36c8c9ea
Feature: battery interface: use HW serial 0 on ESP32-C3 or S3 (#933)
this allows to use two VE.Direct interfaces, as there is no conflict
regarding HW serial port 2 after making the battery interfaces use
serial port 0 on devices with USB CDC. on those chips HW serial 0 is
free to be used since serial messages are written through the USB
interface directly.
2024-04-29 20:43:35 +02:00
Bernhard Kirchen
d3b306e2fc appease eslint 2024-04-29 20:31:50 +02:00
Bernhard Kirchen
84e83f2dbb adopt WebApiClass::parseRequestData() method
saves redundant code, reducing flash usage.
2024-04-29 20:31:50 +02:00
MalteSchm
64738a6246 Introducing defines for RX2/TX2 2024-04-25 22:54:12 +02:00
helgeerbe
fdc5054480 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into merge-v24.4.12 2024-04-25 20:59:19 +02:00
MalteSchm
f0df583c13
Feature: support for Huawei target power consumption 2024-04-24 20:26:56 +02:00
Bernhard Kirchen
74330a5617 Feature: restart unresponsive inverter
we found that the inverter sometimes stops responding to commands,
especially to the "start producing" command. we now count the number of
consecutive timeouts when trying to send a new limit or power state
commands. after two timeouts were recorded, every additional timeout
will send a restart command to the inverter.

as a last resort, if the counter keeps climbing, the DTU is restarted.

notice that this only targets unresponsive inverters which are
reachable. unreachable inverters are not restarted and do not cause a
DTU reboot. this is important for solar-driven inverters, which are
unreachable during the night. the DPL will not calculate a new limit and
hence the updateInverter() method will do nothing while the target
inverter is unreachable.

publish the timeout counter to MQTT for monitoring purposes.
2024-04-24 16:15:01 +02:00
Bernhard Kirchen
eb9bfd1ac6 Fix: DPL mode 2 for solar-powered inverters
for solar-powered inverters, the unconditional full solar-passthrough
implementation now sets the upper limit configured in the DPL settings.
2024-04-22 12:51:31 +02:00
Bernhard Kirchen
1d4bea24ff Fix: JK BMS: BMS name is second part of product ID 2024-04-18 16:36:11 +02:00
Bernhard Kirchen
4cd690de66 VE.Direct: publish data retrieved from HEX protocol 2024-04-18 13:06:37 +02:00
Bernhard Kirchen
abe01ae36f Feature: HTTP power meter: support changing sign 2024-04-18 12:10:29 +02:00
Bernhard Kirchen
ede1abb5e6 Feature: HTTP power meter: support mW/kW as units 2024-04-18 12:10:29 +02:00
Bernhard Kirchen
247cfe712e HTTP power meter: prevent out-of-bound array access 2024-04-18 12:10:29 +02:00
Bernhard Kirchen
e92701ccdf reuse power meter's HTTP config struct 2024-04-18 12:10:29 +02:00
Bernhard Kirchen
4bc4defe66 DPL: insist on power meter value more recent than inverter stats
avoid performing a calculation based on a (slightly) outdated power
meter reading, which was aquired just before the limit was actually
applied by the inverter, but which was received by OpenDTU-OnBattery
after the inverter stats.
2024-04-15 23:02:58 +02:00
Bernhard Kirchen
5fcf09d0a0 fix hysteresis hint texts 2024-04-15 23:02:58 +02:00
Bernhard Kirchen
52d7ac9581 Feature: DPL: support setups without power meter
without a power meter configured, the DPL now sets the base load as the
inverter limit if the battery charge allows it. it also takes
solar-passthrough into account, i.e., if the battery is in a charge
cycle but the solar output (Victron MPPT) is significant, the solar
power will be used up until the base load. if the battery reaches the
full solar passthrough threshold, the DPL will match the inverter limit
to the MPPT solar output.
2024-04-15 23:02:58 +02:00
Bernhard Kirchen
7e307114e5 Feature: DPL: introduce base load setting
on power meter issues (usually a timeout), keep the inverter enabled and
make it produce the configured base load limit if the battery can be
discharged. that should be okay since the base load config value is
expected to be small and a little less than the actual household base
load, i.e., if this amount of power is produced, the household will
consume it in any case and no energy is fed into the grid.
2024-04-15 23:02:58 +02:00
Bernhard Kirchen
cf1ea42f8b power limiter: remove obsolete enum 2024-04-15 23:02:58 +02:00
Bernhard Kirchen
4c2822cdbc remove usage of F() macro
frees 888 Bytes of flash.
2024-04-12 15:33:47 +02:00
Bernhard Kirchen
8b3a1bef47 Fix: show AC input power of Huawei AC charger in live view
makes the value match its description. since most values in the top part
of the live view are related to the AC side of the system, it makes
sense to use the correct value rather than to change the description.
2024-04-11 14:27:47 +02:00
Bernhard Kirchen
f634f58788 Fix: DPL: use correct channel type to get inverter efficiency 2024-04-11 08:28:49 +02:00
Bernhard Kirchen
a9c3e05f05 PowerMeter admin: URL examples to the top and hidden if disabled 2024-04-10 20:44:46 +02:00
PhilJaro
165a9bc168
adjust VE.Direct MPPT yield resulotion (#859) 2024-04-09 20:31:05 +02:00
eu-gh
0ed09aeb4c Feature: Huawei: add SoC stop threshold and verbose logging switch 2024-04-07 17:16:50 +02:00
SW-Nico
b9ad1e3054 VE.Direct: process more values and refactor variable names
* process "IL", "AR" and "MON"
* discard "BMV" and (unsolicited) History Data
* simplify isDataValid()
* veMpptStruct, veStruct: new, verbose variable names, including units,
  and replace floats (save values with original integer precision)
* comment on rollover situation in isDataValid()
2024-04-07 17:13:07 +02:00
Bernhard Kirchen
3934906001
Merge pull request #836 from helgeerbe/merge-v24.3.31
Merges v24.3.31 from upstream
2024-04-03 20:08:09 +02:00
Bernhard Kirchen
21cdc69625 Feature: use VE.Direct "network total DC power"
1. makes the DPL use the power generated by all connected charge
   controllers for calculations based on solar passthrough.
2. makes the network total DC power appear as "MPPT Total Power" in the
   live view at the top.
3. shows the network total DC power in the VE.Direct live data card.
2024-04-03 16:33:15 +02:00
Bernhard Kirchen
6b8c93d2e6 polish VE.Direct HEX support
* show charge controller temperature in live view
* send hex requests right after decoding a frame. this seems to have the
  best chance of getting an answer to all requests.
* deem 0xFFFFFFFF value of network total DC power as invalid indicator.
  neither network state, nor network info, nor network mode seem to
  indicate that the charge controller is part of a VE.Smart network. for
  that reason, we revert to always querying the network total DC power
  value, but testing it for max(uin32_t) value, which seems to indicate
  that the charge controller is not part of a VE.Smart network.
* improve (verbose) logging, e.g., use _logId, and print names of
  response codes and known registers, always print error messages,
  add additional tests to prevent overly verbose messages.
* move hex protocol definitions to VeDirectData.h header
  and use enum classes
* define register addresses in enum class
* move values retrieved through hex protocol into main MPPT data struct
* do not send HEX requests if the serial interface cannot send data
* detect whether smart battery sense temperature is available
* web app: make all VE.Direct sub-cards iterable. this makes addind more
  values much simpler and saves a bunch of code in the web app.
* make VeDirectFrameHandler state a type-safe enum class
* unindent MPPT controller loop()
* whitespace cleanup
2024-04-03 16:33:15 +02:00
SW-Nico
aadd7303ac Feature: add support for VE.Direct hex messages 2024-04-03 16:33:15 +02:00
helgeerbe
ff44267e73 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into merge-v24.3.31 2024-04-03 11:53:03 +02:00
Bernhard Kirchen
8c6e925ca4 VE.Direct: make state machine timeout robust against overflow 2024-04-02 21:05:59 +02:00
Bernhard Kirchen
43f553d2d4 VE.Direct MQTT: simplify code
the use of a #define is warranted here since it saves a lot of code
duplication and improves code readability.
2024-04-02 21:05:59 +02:00
Bernhard Kirchen
92a7f27919 VE.Direct: use float rather than double
double precision floating point numbers are not needed to handle
VE.Direct values. handling double is implemented in software and hence
*much* more resource intensive.
2024-04-02 21:05:59 +02:00
Bernhard Kirchen
b299b9dc6c VE.Direct: simplify access to data
hand out const& to the data structs. this is possible now that this
struct is "stable", i.e., not reset regularly.
2024-04-02 21:05:59 +02:00
Bernhard Kirchen
ad125ea804 Fix: properly handle fragmented VE.Direct messages
queue every text event until the frame was checked by it checksum. then
process the data directly into the buffer struct. do not clear the
buffer struct, so it will always include the most recent value of a
particular data point.
2024-04-02 21:05:59 +02:00
Bernhard Kirchen
187f197e32 Feature: add unique prefix to VE.Direct messages 2024-04-02 21:05:59 +02:00
MalteSchm
8abf614047 Feature: BMS initiated emergency charging
This change logically connects the AC-Charger with the BMS to add BMS
initiated emergency charging and respecting BMS current limits.
2024-04-02 21:05:00 +02:00
PhilJaro
da96273085
Fix typo in German locale (#831) 2024-04-02 12:05:45 +02:00
Bernhard Kirchen
91a0992964 fix: VE.Direct MPPT data not always updated in websocket
set the "last published" timestampt after handling *all* MPPTs.
2024-03-26 20:48:39 +01:00
David von Oheimb
811b64adb5 PowerLimiter.cpp: simplification and minor correction of logic table comments 2024-03-25 10:55:00 +01:00
Bernhard Kirchen
0169b29cfd
Merge development into master to prepare release 2024.03.23
Prepare release 2024.03.23
2024-03-23 23:16:34 +01:00
Bernhard Kirchen
06f39f8396 Merge remote-tracking branch 'tbnobody/master' into development 2024-03-23 22:58:21 +01:00
Bernhard Kirchen
12f7caf998 fix: HTTP power meter: retrieve multiple JSON paths
if only a single request was made (switch "Individual HTTP requests per
phase" is off), the user could still enable phase 2 and phase 3 config
and configure a respective JSON path. however, the value was never
extracted from the successful HTTP request for phase 1.

closes #637.
2024-03-23 22:13:59 +01:00
Bernhard Kirchen
b8d0998a49 HTTP power meter: refactor tryGetFloatValueForPhase
unindent code, prepare this method for re-use on the same HTTP response
but with a different JSON path.
2024-03-23 22:13:59 +01:00
Bernhard Kirchen
c41d6f5138 HTTP power meter: remove trailing whitespace
this commit is empty when inspecting it with --ignore-all-space.
2024-03-23 22:13:59 +01:00
Bernhard Kirchen
054a677575 DPL settings in web app: split metadata from config
users are manipulating the DPL using HTTP POST requests. often they are
requesting the current settings using HTTP GET on the respective route,
then change a particular settings, and send all the data back using HTTP
POST. if they failed to remove the metadata node from the JSON,
OpenDTU-OnBattery would not be able to process the JSON due to its size.
the web app does not submit the metadata.

to avoid problems, the metadata is now split from the configuration
data.
2024-03-23 22:13:50 +01:00
Bernhard Kirchen
8bfb5c6523 implement and use Utils::checkJsonOverflow()
this method calls the overflowed() method on the respective
DynamicJsonDocument and prints a respective message if not all
data could be added to the DynamicJsonDocument.
2024-03-23 17:57:13 +01:00
Bernhard Kirchen
1fb2d4262e do not publish live data for broken MPPT controllers
in case the user defines the respective pins, an MPPT controller will be
setup and the websocket would previously publish its invalid data even
if no data at all was received. this lead to using an invalid index and
to publish a useless card. instead, skip those controllers.

if the connection to a controller breaks, it will always present as a
card in the live view with the respective serial number and the last
known data.
2024-03-23 17:56:51 +01:00
Bernhard Kirchen
0154da91ab fix VE.Direct live data response size
PLLIMIT would not (always) be part of the JSON string transmitted to the
web application and trip it. with the help of the ArduinoJson assistant,
new values to use in the response size calculation were introduced.
also, importantly, a constant offset was added for the DPL status and
the JSON structure sourrounding the MPPTs.
2024-03-23 17:56:51 +01:00
Bernhard Kirchen
1c51c2de40 fix and harden BatteryStats update timestamp handling
* updating the SoC or value shall also update the general timestamp, as
  the latter is defined as "any value changed", which includes SoC and
  voltage, of course.
* if the last update is not a valid timestamp at all, the
  updateAvailable method must always return false, obviously.
2024-03-23 17:56:46 +01:00
Bernhard Kirchen
d935283d1f avoid sending null through live data websockets
on the "main" live data websocket this can happen if no
OnBattery-specific data is available to sent but the empty
JSON document is still sent, which trips the web application.

publishing null in the battery live data websocket also trips the web
application, which rightfully assumes a valid object if data is received
through the websocket.
2024-03-23 17:54:45 +01:00
Bernhard Kirchen
13e42051b6 DPL: improve verbose logging (more variables logged) 2024-03-22 21:50:42 +01:00
helgeerbe
03435f66ee Feature: Set max number of supported Inverters to 10 (upstream default)
Due to the latest live view api changes, I revert the maximum number of supported inverters to the upstream default (10).
2024-03-22 13:50:16 +01:00
Snoopy-HSS
bbaed260f5
SMA Homemanager: fix power value calculation
when consuming from the grid, the power meter value shall be positive, and it shall be negative when exporting power.
2024-03-22 10:20:53 +01:00
Bernhard Kirchen
b449dd1196 replace VICTRON_MAX_COUNT
determine the amount of controllers actually in use dynamically,
especially to avoid indices which are invalid, causing an error
to be printed, even though the user did not do anything wrong.
2024-03-20 22:11:11 +01:00
Bernhard Kirchen
8e5e8d169d fix: VE.Direct live data after supporting second MPPT
an error was made when determining whether or not to push a VE.Direct
MPPT's state through the websocket based on its data's age.
2024-03-20 22:11:10 +01:00
Bernhard Kirchen
cffc5b1d26
Merge pull request #778 from helgeerbe/upstream-v24.3.15
merges upstream v24.3.15
2024-03-20 21:00:04 +01:00
helgeerbe
5259fc172a Merge remote-tracking branch 'tbnobody/OpenDTU/master' into v24.3.15 2024-03-20 09:42:35 +01:00
Bernhard Kirchen
cd339a3a14 Feature: implement PowerMeter pin config for serial interfaces
in your pin_mapping.json, add a powermeter object like this:

[
    {
        "name": "My Board",
        ...
        "powermeter": {
            "rx": <num>,
            "tx": <num>,
            "dere": <num>
        },
        ...
    }
]

the SML power meter requires the rx pin to be set. the SDM power meter
requires the rx and tx pins are set. the "dere" pin pin is optional and
if set, this pin controls the driver enable and receiver enable pins of
the RS485 transceiver. the SDM library handles this pin.

closes #771.
2024-03-19 21:27:25 +01:00
Bernhard Kirchen
7d4a30dde4
hardware_flash.md: move some info to wiki
closes #628
2024-03-19 20:01:25 +01:00
PhilJaro
4f0385285c add new Victron SmartShunt values to liveView and MQTT (HASS) 2024-03-17 21:00:32 +01:00
Bernhard Kirchen
45c7243937 polish SMA HomeManager integration
* remove/comment unused variables to avoid compiler warnings

* cleanups: fix indention and style, make variable private, implement
  getters in header and make const.

* optimize message output: respect verbose logging setting. prefix
  output with SMA_HM.

* use newly introduced mutex in PowerMeterClass also for SMA HomeManager

* refactor code for readibility, unindent where possible.
2024-03-17 20:20:29 +01:00
Snoopy-HSS
f6680bd664 Feature: Support SMA HomeManager 2.0 as PowerMeter 2024-03-17 20:20:29 +01:00
Bernhard Kirchen
900326742c fix: prevent getTotalPower() reading intermediate results
the SDM power meter (among others) writes the power consumption of three
phases in multiple steps. this change helps to prevent getTotalPower()
reading intermediate values, e.g., reading a new value for phase 1 but
old values for phase 2 and 3 since phase 2 is currently read.

cache the values, and write them all at once, protected by a mutex,
later.

closes #732.
2024-03-17 16:54:48 +01:00
Bernhard Kirchen
13bc943dd5 Feature: Refactor/Simplify DPL settings
this changeset refactors the web application's DPL settings view. the
DPL settings can be complex, and they shall be presented in a way that
allows users to comprehend their meaning. irrelevant settings are now
hidden or displayed dynamically based on the influencing settings.

* group SoC thresholds into their own card

* hide battery SoC thresholds if battery disabled. if the user did not
  even enable the battery interface, battery SoC values will not be used
  for DPL decisions. in that case we completely hide the respective
  settings from the DPL admin view. this reduces the amount of settings
  for new users and especially users who don't even have a battery in
  their setup or have no BMS connected.

* group voltage thresholds and improve label texts

* fix load correction factor unit

* fix header (wording)

* group solar-passthrough settings in new card

* group inverter-related settings

* hide solar passthrough settings if VE.Direct is disabled. closes #662.

* completely disable form if any requirement is not met

* list available inverters by name and type. this makes it much more
  convenient to select the right inverter, especially since the order of
  the inverters in the web UI is decoupled from their position in the
  internal array, which was used to select them previously. care was
  taken that old configs select the same inverter after an update.
  when editing the DPL settings, the selects an inverter from the newly
  created drow-down list, and the respective old inverter is
  pre-selected.

* disable form if no inverter is configured (config alert)

* make inverter input selection dynamic. adjust selection to actual
  amount of channels for selected inverter. skip selection altogether if
  inverter has only one channel, or if it is solar powered.

* web app: wording adjustments

* group meta data into new property and exclude from submission. saves
  memory when evaluating the submitted settings.

* hide irrelevant settings if inverter is solar-powered

* move restart hour setting to inverter card. translate setting which
  disabled automatic restart.

* simplify "drain strategy" setting into an on/off toggle. care was
  taken that existing configs work the same after an upgrade. the
  respective drain strategy is translated into the new setting when
  reading the config. once the config is written, the new setting is
  persisted and the old is not part of the config any more.

* show more configuration hints, depending on actual configuration

* replace inputs by InputElement components where possible
2024-03-17 16:50:57 +01:00
Bernhard Kirchen
7d6b7252bf polish support for second VE.Direct MPPT charge controller
* fix compiler warning in SerialPortManager.cpp: function must not
  return void

* clean up and simplify implementation of usesHwPort2()
  * make const
  * overrides are final
  * default implementation returns false
  * implement in header, as the implementation is very simple

* rename PortManager to SerialPortManager. as "PortManager" is too
  generic, the static instance of the serial port manager is renamed to
  "SerialPortManager". the class is therefore renamed to
  SerialPortManagerClass, which is in line with other (static) classes
  withing OpenDTU(-OnBattery).

* implement separate data ages for MPPT charge controllers

* make sure MPPT data and live data time out

* do not use invalid data of MPPT controlers for calculations

* add :key binding to v-for iterating over MPPT instances
2024-03-17 16:50:15 +01:00
Arman Vartan
75541be248 Feature: Support for second Victron MPPT charge controller
this change adds support for a second Victron MPPT charge controller
using a second serial connection.

* Add device configuration for a second victron mppt
* Update VedirectView for second victron mppt
* Update MqttHandleVedirect for second victron mppt
* Update MqttHandleVedirectHass for second victron mppt
* Handle nonexisting victron controllers with optionals
* Add bool-function to Battery and inherited classes, if uart port 2 is
  being used
* Introduced a serial port manager. In order to prevent the battery and
  the Victron MPPT to use the same hw serial ports, this class keeps
  track of the used ports and their owners.
2024-03-17 16:50:15 +01:00
Bernhard Kirchen
21c19f4b7f fix: preserve SmartShunt energy values precision 2024-03-17 08:31:54 +01:00
Bernhard Kirchen
7ebd4f4632 clean up defaults.h
* remove duplicated #defines. this is most probably a merge error from
  2024-01-16, as evidenced by 63205f88b, which added these duplicates.

* sort values by upstream and downstream projects. add a comment which
  tells us in the future where OpenDTU-OnBattery-specific values start.
2024-03-16 14:31:07 +01:00
Bernhard Kirchen
2efa1b35b0 live view: do not access undefined data
if the respective feature is disabled, there is no data. do not try to
access it.
2024-03-15 08:55:21 +01:00
Bernhard Kirchen
5c6b4a8f12 disable restarting solar-powered inverters
now that users can tell the DPL that their inverter is not powered by a
battery but powered by solar panels, we shall not restart inverters to
reset the daily yield value, if they are solar powered. these inverters
will reboot every night by themselves.
2024-03-14 21:37:47 +01:00
Bernhard Kirchen
8895791145 live data: exclude data if respective feature disabled 2024-03-14 21:37:47 +01:00
Bernhard Kirchen
cf27bd29d7 adjust (new) about texts to OpenDTU-OnBattery 2024-03-14 16:27:32 +01:00
Bernhard Kirchen
56353e4f00 implement Battery::needsCharging()
currently this is only supported by the Pylontech battery provider, as
it reports a "charge battery immediately" alarm. this will also be
implemented by the JK BMS provider, and possibly also by the smart shunt
provider.

the method will be used to determine whether or not to start charging
the battery using the (Huawei) charger.
2024-03-13 17:07:05 +01:00
Bernhard Kirchen
784e369482 optimize DPL thresholds MQTT integration
* fix logic in HomeAssistent handler
* also publish voltage thresholds (not just SoC thresholds)
* do not publish irrelevant thresholds to MQTT. if the inverter is
  solar-powered, no thresholds are effectively in use by the DPL and it
  therefore makes no sense to publish them to the broker. similarly, if
  no battery interface is enabled or the SoC values are set to be
  ignored, the SoC thresholds are effectively not in use and will not be
  published to the broker.
* make HA auto-discovery expire. this makes auto-dicovered items
  disappear from Home Assistent if their value is no longer updated.
  changes to settings which cause other thresholds to be relevant will
  then be reflected in Home Assistent even if some thresholds are no
  longer maintaned in MQTT.
* force HA update when related settings change enabling VE.Direct shall
  trigger an update since solar passthrough thresholds become relevant.
  similarly, enabling the battery interface makes SoC thresholds become
  relevant. there are more settings in the power limiter that also
  influence the auto-discoverable items.
* break very long lines
2024-03-10 22:10:02 +01:00
LukasK13
fba5c02346 Feature: Set powerlimiter thresholds via MQTT
publish DPL thresholds to MQTT, add support for setting powerlimiter
thresholds via MQTT, and make these auto-discoverable for Home
Assistent.
2024-03-10 22:10:02 +01:00
Bernhard Kirchen
803b30ca11 fix: wrong unit in battery HomeAssistent exp_aft
the exp_aft value is in seconds.
2024-03-10 21:20:56 +01:00
Bernhard Kirchen
19859ed601
Merge pull request #733 from helgeerbe/dpl-make-shutdown-partially-optional
DPL: Explicit Support for solar-powered inverters
2024-03-10 17:25:30 +01:00
Bernhard Kirchen
c46980d6af fix: more memory for onBattery live data
the allocated memory to create the JSON with onBattery-specific totals
for the live view was too little to contain all values, which are sent
regularly.
2024-03-10 15:57:50 +01:00
Bernhard Kirchen
80edbec769 Feature: DPL: keep inverter running if solar powered
avoid shutting down the inverter at all if the calculated power limit
falls below the lower power limit or if the power meter value is
outdated. do this only if the inverter is setup to be solar powered.
2024-03-09 15:42:09 +01:00
Bernhard Kirchen
91f8f61e63 Feature: DPL: explicit support for solar powered inverters
by default and until this change, we assumed that the inverter
controlled by the DPL is powered by a battery. not all users have a
battery in their system. they still use the DPL to achieve net-zero
export. those users can now tell the DPL that their inverter is powered
by solar modules rather than a battery and the DPL will behave
accordingly.
2024-03-09 15:40:13 +01:00
Bernhard Kirchen
be15050aed DPL: refactor code determining battery charge cycle state 2024-03-09 15:40:13 +01:00
Bernhard Kirchen
c6f81806d6 DPL: make "IsInverterSolarPowered" configurable through web app 2024-03-09 15:40:13 +01:00
Bernhard Kirchen
b11b1dbcba DPL: define IsInverterSolarPowered config switch 2024-03-08 22:53:34 +01:00
Bernhard Kirchen
490a38f909
Merge pull request #726 from helgeerbe/development
Prepare Release 2024.03.07
2024-03-07 22:23:46 +01:00
Bernhard Kirchen
c42d68812c DPL limit scaling: prevent division by zero
this check was removed on error when moving the scaling code into its
own function.
2024-03-07 21:18:11 +01:00
Bernhard Kirchen
b0795a2131 DPL limit scaling: only for supported models 2024-03-07 21:18:09 +01:00
Bernhard Kirchen
64ad4bded1 fix: DPL: limit scaling sanity checks
do not scale limit if inverter is not producing, as DC channel power is
expected to be close to zero anyways.

do not scale limit if current inverter limit is small, such that
channels might produce very little power exactly because the limit is so
low.

move the calculation out of setNewPowerLimit and into a new function, so
that we can make use of return statements there.
2024-03-07 20:54:16 +01:00
Bernhard Kirchen
50635ee2ce Feature: live view: update with respective frequency
the update frequency of Victron MPPT charger data, the battery Soc, the
huawei charger power, and the power meter differ from one another, and
differ in particular from the inverter update frequency.

the OnBattery-specific data is now handled in a new method, outside the
upstream code, which merely call the new function(s). the new function
will update the websocket independently from inverter updates. also, it
adds the respective data if it actually changed since it was last
updated through the websocket.

for the webapp to be able to recover in case of errors, all values are
also written to the websocket with a fixed interval of 10 seconds.
2024-03-05 11:31:44 +01:00
Bernhard Kirchen
e432f0eca3 make BateryStats::updateAvailable wrap-around-safe 2024-03-05 11:31:44 +01:00
Bernhard Kirchen
fe7e622e2d pull requests: use a meaningful branch when building
This change makes the build runner switch to a meaningful branch name, which will then appear as the "Firmware Branch" in the System Info of the web application. This helps users testing pull-request builds identify that they are actually using the changes from the respective pull request.
2024-03-05 11:26:38 +01:00
helgeerbe
78e70cc6c5
Merge pull request #708 from schlimmchen/dpl-manage-inverter-state
Fix: DPL: ensure inverter reaches requested state
2024-03-05 10:21:16 +01:00
Bernhard Kirchen
8b6e57cda7 Fix: DPL: ensure inverter reaches requested state
we previously only called commitPowerLimit() if the desired limit
changed such that the change was bigger than the hysteresis. we found
that if the limit update was not received and the desired limit would
not change much, the limit of the inverter was wrong for a long time.

to mitigate this, we introduced re-sending the limit update every 60
seconds, regardless of what the limit reported by the inverter was at
that time.

if the power-up command was not received, we also would repeat it only
once every 60 seconds.

this leads to a new kind of staleness and the actual inverter state was
still not matching the desired state.

this new approach effectively adds an additional control loop at the
start of the DPL loop(). that new function compares the requested
inverter state to the actual reported state. it sends updates (limit
update or power on state) until the desired inverter state is reached,
or until a (hard-coded) timeout occurs.

this approach also allows us to send power-up, power-down, and limit
update commands independent from one another and in a particular order.

this should make sure that the inverter is in the desired state even if
conditions change slowly and commands were not received as expected.
2024-03-02 23:39:43 +01:00
Bernhard Kirchen
c560d1d90e
fix: copy OnBattery-specific data from live view websocket (#696)
closes #685.
closes #682.
2024-02-29 17:09:18 +01:00
helgeerbe
f0f8702b4b Merge branch 'development' 2024-02-19 16:05:33 +01:00
helgeerbe
c72ae561c7 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2024-02-19 16:03:56 +01:00
helgeerbe
4e7dfba85c add webapp 2024-02-19 13:51:59 +01:00
helgeerbe
1eb75c322d
Merge pull request #679 from schlimmchen/dpl-voltage-features
DPL Voltage Features
2024-02-19 13:48:12 +01:00
Bernhard Kirchen
9240663552
Feature: show power grid usage on display (#658)
* make efficient use of available display area

fix calculation of the text baselines, using getAscent() in favor of
getMaxCharHeight(), which includes ascent and descent. this moves the
first text up and allows to insert margin between the lines until the
display area is fully utilized.

on large displays, if the small diagram is selected, keep the first line
rather low to avoid collision with the diagram y-axis label. in this mode,
there is still more space between the text lines as before, allowing for
improved readability.

* Feature: show power grid usage on display

if the power meter is enabled, the display will use two of three out
of every three-second time slot to show the grid consumption.

closes #620.
2024-02-19 13:38:57 +01:00
Bernhard Kirchen
c930018764 Feature: DPL: use best available voltage value
the DPL is interested in the battery's voltage to make decisions about
draining the battery or letting it charge (if the user opts to use
voltage thresholds rather than SoC thresholds). using the DC input
voltage reported by the inverter under control has disadvantages:

* the data might be quite old due to the communication protocol
  implementation. more inverters being polled means even more lag. the
  connection being wireless makes this even worse, due to the need
  to retry the occasional lost packet, etc.
* the data is not very accurate, since the DC input of the inverter is
  actually some cabling and a couple of junctions away from the actual
  battery. this voltage drop can mostly only be estimated and is worse
  with higher load. the load correction factor is there to mitigate
  this, but it has its own problems and is cumbersome to calibrate.

instead, this change aims to use more accurate battery voltage readings,
if possible. the DPL now prefers the voltage as reported by the BMS,
since it is for sure the closest to the battery of all measuring points
and measures its voltage accurately regardless of the load (the voltage
reading will still drop with higher loads, but this will be only due to
the battery's internal resistance, not that of cabling or junctions). if
no BMS voltage reading is available, the DPL will instead use the charge
controller's voltage reading, as it is available with much higher
frequency and is assumed to be more accurate as it offers a resolution
of 10mV. only if none of these two sources can be used, the inverter DC
input voltage is assumed as the battery voltage.

closes #655.
2024-02-18 22:17:15 +01:00
Bernhard Kirchen
6df358242c Feature: know and use SoC precision
the Victron SmartShunt communicates the SoC value in permille. this
should be displayed in the web UI accordingly. this is a good excuse to
fully move ownership of the SoC value to the BatteryStats base class and
add a precision indicator variable. this is required to be set each time
a derived class (a battery provider) wants to update the SoC value. the
precision is then used when populating the JSON data for the web UI
(live view).

related to #573.
2024-02-18 22:17:15 +01:00
Bernhard Kirchen
7c069b1cc4 replace BatteryStats::isValid() method
in the respective context, the DPL only needs to be sure that the SoC
value is not outdated. it should not even care about other values
reported by the battery interface. hence, the isValid() method shall be
concerned with the SoC value timestamp only. the method is renamed for
clarity.
2024-02-18 22:17:15 +01:00
Bernhard Kirchen
3595725f8a Feature: implement subscription to battery voltage MQTT topic
this extends the MqttBattery implementation by an additional topic which
allows to subscribe to receive battery voltage readings through the MQTT
broker. similar to the battery SoC topic, this allows to import a
critical battery data point for the DPL, in case the user chooses to use
voltage thresholds rather than SoC thresholds to control the DPL. if an
otherwise incompatible BMS is available which publishes the battery pack
voltage through MQTT, this can now be used to feed accurate voltage
readings to the DPL.
2024-02-18 22:17:15 +01:00
Bernhard Kirchen
30bfffb848 BatteryStats: manage battery pack voltage in base class
the BatteryStats base class shall be able to tell the total battery pack
voltage. for that reason, and to avoid code duplication, the voltage is
now handled in the base class and treated as a datum that is common to
all battery providers.
2024-02-18 21:49:12 +01:00
Bernhard Kirchen
921302bf73 Feature: DPL: add switch allowing to ignore SoC
unfortunately, the battery SoC values reported by battery BMSs are
unreliable, at least for some users, or at least without regular
(manual) full charge cycles to calibrate the BMS. it offers great
advantages to connect OpenDTU-OnBattery to a BMS (MQTT publishing of
values, Home Assistent integration, etc.), but previously the users
were then forced to configure the DPL by SoC values.

this change allows to configure the DPL such that SoC values are
ignored. instead, the voltage limits are used to make DPL decisions, as
if no SoC was available in the first place.

the SoC related setting are hidden from the DPL settings view if SoC
values are configured to be ignored.

closes #654.
2024-02-18 21:46:42 +01:00
helgeerbe
b3d8e984bc Merge branch 'development' 2024-02-09 20:35:46 +01:00
helgeerbe
b794f46ef0 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2024-02-09 20:14:58 +01:00
Bernhard Kirchen
7c84621ea9
update VE.Direct product IDsfixes typos and errors, and adds previously unknown PIDs.closes #641.
* update VE.Direct product IDs

fixes typos and errors, and adds previously unknown PIDs.

closes #641.
2024-02-09 19:50:36 +01:00
helgeerbe
df5b416b3f
Merge pull request #640 from schlimmchen:jkbms-home-assistent-pr
JK BMS Home Assistent Integration
2024-02-09 19:46:59 +01:00
Bernhard Kirchen
1865113842 Feature: JK BMS Home Assistent integration
* pylontech HA integration: remove unused method/variable

* make MqttHandlePylontechHassClass::publishConfig() private.
  there are no outside users of that method.

* rename to MqttHandleBatteryHass

* battery HA integration: merge methods and bring back forceUpdate().
  even though the forceUpdate() method was not in use before, it makes
  sense to implement it and use it when the battery config changes.
  rather than controlling a separate flag, it now changes the _doPublish
  flag of the class, which also triggers publishing the device config to
  Home Assistant when an MQTT connection problem was detected. since
  both situations are now handled similarly, we can merge the loop() and
  publishConfig() methods.

* battery: provider specific sensors for HA

* move Battery MQTT loop to BatteryStats
  the BatteryStats class should handle the MQTT publishing, including the
  interval. for the calculation of a reasonable Home Assistent expiration
  value this class now also knows the maximum publish interval.

* JK BMS: fix publishing values for Home Assistent
  Home Assistent values expire, because we set them to expire after three
  MQTT publish durations. for that reason, we need to re-publish all
  values after our self-inflicted full publish interval.

* define JK BMS sensors for Home Assistent

closes #482.
2024-02-05 21:22:07 +01:00
Bernhard Kirchen
c2b49931be Fix: must call Pylontech Home Assistent init() method
previously, the Pytlontech Home Assistent class implementation had an
init() method, that was never called, as it did nothing. the class
relied on its loop() method being called from the main loop(). after
switching to the TaskScheduler approach, the Pylontech Home Assistent
class init() method was adjusted to register a task that calls the
loop() method periodically. however, the init() method was still not
called.
2024-02-05 20:54:34 +01:00
helgeerbe
db51680712 Merge branch 'development' 2024-01-26 14:42:24 +01:00
helgeerbe
1916d6d6a8 check twice a second for new vedirect data on live viw 2024-01-26 14:32:44 +01:00
helgeerbe
7094bca7db fix: Inverter Labels in InverterTotalInfo 2024-01-26 13:34:03 +01:00
helgeerbe
ebacc2f25f Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2024-01-25 23:36:32 +01:00
helgeerbe
f709bb7a78 Merge branch 'development' 2024-01-17 14:18:14 +01:00
helgeerbe
f305b40be0 Merge pull request #612 from Fribur:development
- fix to HttpPowermeter not using explicitly specified non standard ports
- Revert back to using FirebaseJson instead of ArduinoJson
2024-01-17 13:26:52 +01:00
helgeerbe
7439ad0934
Merge pull request #612 from Fribur:development
- fix to HttpPowermeter not using explicitly specified non standard ports
- Revert back to using FirebaseJson instead of ArduinoJson
2024-01-17 13:24:08 +01:00
Fribur
75e3d03ea4 Revert back to using FirebaseJson instead of ArduinoJson
There is no convenient way to access arrays of Json objects and nested keys such as testarray/[2]/myvalue using ArduinoJson. It would at the very least to split the path into a number of individual strings and then access the value like json[testarray][2][myvalue] More details in https://arduinojson.org/v6/doc/deserialization/ . Conclusion: too complicated.
2024-01-16 19:32:02 -05:00
Fribur
8392c7cd8c fix to HttpPowermeter not using explicitly specified non standard
should resolve the connection refused issues reported in https://github.com/helgeerbe/OpenDTU-OnBattery/issues/611
2024-01-16 16:34:46 -05:00
helgeerbe
7c66965ced Merge branch 'development' 2024-01-16 17:48:46 +01:00
helgeerbe
63205f88be Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2024-01-16 17:39:45 +01:00
helgeerbe
b8fcdf3998 build: add manual lib dependency that ESPSoftwareSerial 8.0.1 can compile
Add lib dependency
https://github.com/dok-net/ghostl @ ^1.0.1
manually. ESPSoftwareSerial 8.0.1 has incorrect debendency configuration.
2024-01-16 12:55:38 +01:00
Fribur
e136e096f4
Merge branch 'development' into development 2024-01-08 11:00:59 -05:00
helgeerbe
ffd189c1f5 Merge branch 'development' 2024-01-07 19:09:26 +01:00
Bernhard Kirchen
a012d81427
avoid too frequent SmartShunt data copies (#596)
currently the whole SmartShunt data structure is copied to the
BatteryStats instance in every loop, even though the data cannot
possibly have changed. this is quite an expensive task to do in every
loop. this change tracks the last update timestamp and only does the
copy operation if an actual updated data structure was received from
the smart shunt.
2024-01-07 18:30:02 +01:00
helgeerbe
2806b6db86 set dependency for plerup/EspSoftwareSerial to 8.0.1
8.2.0 has unresolvable dependencys
2024-01-07 18:00:57 +01:00
helgeerbe
642f38ce51 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2024-01-07 11:01:30 +01:00
helgeerbe
44d207a5f5 add webapp 2024-01-07 10:31:22 +01:00
helgeerbe
dd5d5ce9fd Prevent empty HASS auto discovery topics if memory allocation fails (onBattery) 2024-01-07 10:31:06 +01:00
helgeerbe
d0e1da7b1d Prevent config corruption by checking whether memory allocation was successfull. (onBattery) 2024-01-07 10:30:12 +01:00
helgeerbe
d6d274f078 Prevent empty HASS auto discovery topics if memory allocation fails (onBattery) 2024-01-06 22:51:35 +01:00
helgeerbe
ec93004724 Better handling of out of memory situations in live data websocket (onBattery) 2024-01-06 22:42:34 +01:00
helgeerbe
210fce67ce Use auto keyword and references more often (onBattery) 2024-01-06 22:10:07 +01:00
Fribur
92de3e9f87 fixed a bug where under one condition DNS was not tried for resolving host IP 2024-01-05 20:54:53 -05:00
helgeerbe
67e2134f7e add webapp 2024-01-05 23:03:09 +01:00
helgeerbe
d5155a07be Remove F macro from onBattery extensions 2024-01-05 23:02:26 +01:00
Thomas Basler
bfcce16bc9 webapp: update dependencies 2024-01-05 22:04:54 +01:00
Thomas Basler
6573c51052 Use auto keyword and references more often 2024-01-05 22:04:54 +01:00
Thomas Basler
24c8a40fad Use Utils::checkJsonAlloc in ConfigurationClass 2024-01-05 21:56:34 +01:00
Thomas Basler
f968179b60 Better handling of out of memory situations in live data websocket 2024-01-05 21:56:34 +01:00
Thomas Basler
f00cd1bd61 Prevent empty HASS auto discovery topics if memory allocation fails 2024-01-05 21:55:15 +01:00
Thomas Basler
88d75673fc Prevent config corruption by checking whether memory allocation was successfull. 2024-01-05 21:55:15 +01:00
Thomas Basler
a65f1e48a5 Fix: Access Point not working after firmware update
Fixes #1613
2024-01-05 21:55:15 +01:00
Thomas Basler
2a15677923 Fix: Optimize network connection handling
This should provide a more reliable connection to several AP types. See #576
2024-01-05 21:55:15 +01:00
Bernhard Kirchen
2ed66eb992 Fix: define _TASK_THREAD_SAFE for TaskScheduler
the TaskScheduler runs in the context of a FreeRTOS thread/task. there
are other such threads (MQTT client and web server in particular). to
allow changing TaskScheduler task properties from different threads, we
need to enable the use of a mutex to protect the TaskScheduler.
2024-01-05 21:55:15 +01:00
Bernhard Kirchen
377406f10c
Feature: add heap details to system info and prometheus (#595)
this change adds the values of ESP.gteMaxAllocHeap() and
ESP.getMinFreeHead() to the prometheus metrics and the system
information object. the web UI uses these values to diplay the size of
the largest free contiguous block, calculate a rough estimate for the
level of fragmentation, and the maximum usage of heap memory since boot
in absolute and relative amounts.
2024-01-05 21:46:31 +01:00
Fribur
e09ffcbb53 shorter parameter names 2024-01-05 14:39:32 -05:00
Fribur
85d0f2a8fb HttpPowerMeterClass: change order of resolving hostname
OpenDTU console gets spammed with "WifiGeneric::hostByName() error when first trying to resolve the hostname via DNS. So reverse order: first try mDNS, if that fails try DNS. Also ensure that https bool is passed correctly to HTTPClient::begin(). Lastly, concatenate strings for building Digest authorization using "+" and not via snprintf.
2024-01-05 14:36:19 -05:00
Fribur
9ed5a78818 Reverted changes to PowerLimiter, adapted DNS and mDNS handling in HttpPowerMeter
For non IP address URLs, HttpPowerMeter now first tries DNS for resolution as done in WifiClient::connect, and only if that fails tries mDNS. For the latter to work mDNS needs to be enabled in settings. Log in console if mDNS is disabled. String building for Digest authorization still tries to avoid "+" for reasons outlined in https://cpp4arduino.com/2020/02/07/how-to-format-strings-without-the-string-class.html This should also be saver than just concatenating user input strings in preventing format string attacks. https://owasp.org/www-community/attacks/Format_string_attack
2024-01-05 10:13:16 -05:00
Fribur
bc38ce344f remove FirebaseJson from platfromio.ini, fix unintended change in PowerLimiter 2024-01-04 18:22:58 -05:00
Bernhard Kirchen
3c8b8d4427
use frozen::string and frozen::map where reasonable (#593)
this change utilizes some of the features from library "frozen", which
was included upstream for the grid profile parser. to improve code
maintainability, a couple of std::maps mapping strings to values or the
other way around were introduced in OpenDTU-OnBattery-specific code at
the expense of some flash and computing overhead.

library "frozen" offers constexpr versions of map and string, which
saves initialization code and offers slightly faster lookups. this
brings the binary size down by ~25kB and should provide a small
performance improvement at runtime.
2024-01-04 23:28:34 +01:00
Fribur
d5eba2392c fixed long/float parsing bug 2024-01-04 16:32:42 -05:00
Fribur
f5c69060f5 re-factoring of HttpPowerMeter
Added ability to deal with local host names (mDNS), remove use of FirebasedJson to save ~20kB build size, some changes to PowerLimiter to avoid setting new inverter power limits when not needed (=current limit as reported by inverter is within hysteresis)
2024-01-04 16:20:32 -05:00
helgeerbe
aa5a762d2a Merge branch 'development' 2024-01-04 15:47:46 +01:00
helgeerbe
e9def28f3e add webapp 2024-01-04 15:43:08 +01:00
Bernhard Kirchen
e7a005839b
Feature: implement MQTT-driven battery provider (#589)
this battery provider implementation subscribes to a user-configurable
MQTT topic to retrieve the battery SoC value. the value is not
re-published under a different topic. there is no card created in the
web app's live view, since the SoC is already part of the totals at the
top of the live view. that is the only info this battery provider
implements.

closes #293.
relates to #581.
2024-01-04 15:42:10 +01:00
helgeerbe
65319ed07e log if memory allocation for live view fails
Explained in #591
2024-01-04 15:38:28 +01:00
Bernhard Kirchen
4548fc2a00
remove description of DPL from README (#588)
the DPL is described in more detail in the Wiki, which is the canonical
source of information.
2024-01-04 15:17:43 +01:00
helgeerbe
7946dfc0c2 add webapp 2024-01-03 13:10:50 +01:00
helgeerbe
2c1e145575 Merge branch 'development' of https://github.com/helgeerbe/OpenDTU into development 2024-01-01 17:20:57 +01:00
helgeerbe
bbf0003d1c Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2024-01-01 17:20:27 +01:00
MalteSchm
5bd3ce5a8f
Reducing lower limit for AC-charger (#574) 2024-01-01 17:06:58 +01:00
helgeerbe
cbf7680836 merge v23.12.31 2024-01-01 14:50:51 +01:00
Bernhard Kirchen
8f5c4878c5
Fix: switch context when processing DPL MQTT requests (#572)
MQTT message callbacks are executed in the MQTT thread context. when
processing topics that control the DPL, we must avoid executing methods
that are not thread-safe. this change binds the methods to be called to
the respective parameters and executes them in the TaskScheduler
context, such that they no longer need to be thread-safe.
2023-12-31 14:49:39 +01:00
helgeerbe
ef1aec3b26
Merge pull request #571 from schlimmchen/switch-context-on-huawei-mqtt-message
Fix: switch context when handling AC charger MQTT messages
2023-12-31 14:49:00 +01:00
Bernhard Kirchen
463226082f clean up Huawei MQTT handler
* bind the callback to a topic (enum value) such that there is no need
  to tokenize the full topic (string) to find out what value is being
  processed. tokenizing is expensive.
* get rid of using the config in the callback, which improves
  thread-safety since the MQTT callback is running in the MQTT thread.
* prefer C++ method stof to convert MQTT value to a float, which saves
  us from using new and delete for a buffer in particular.
* prefer switch statements over if-else-trees.
* split long lines.
* get rid of topic #defines.
* fix indention.
2023-12-30 18:41:57 +01:00
Bernhard Kirchen
fe2f82e303 Fix: switch context when handling AC charger MQTT messages
MQTT message callbacks are executed in the MQTT thread context. when
processing topics that control the huawei AC charger, we must avoid
executing methods that are not thread-safe. this change bound the
methods to be called to the respective parameters and executes them
in the TaskScheduler context, such that they no longer need to be
thread-safe.
2023-12-30 18:08:06 +01:00
helgeerbe
08bc181a5e add webapp 2023-12-30 16:50:02 +01:00
Bernhard Kirchen
7928f2f8cf
Feature: JK BMS: add more values to live view (#552)
there are more interesting values available to display in the live view.
however, adding them made the list of values very long. this can be
mitigated by using a new column/card, which uses the available screen
space nicely on bigger screens.
2023-12-30 16:46:44 +01:00
Bernhard Kirchen
bb34fa74fd
Fix: use FormFooter in OnBattery-specific forms (#569)
the upstream project introduced a new Vue component "FormFooter", which
is used to end an input form, namely all settings forms. we should not
only use this component as well, but the save button on our forms
actually broke since the text dtuadmin.Save is replaced by base.Save.

also replace the use of dtuadmin.Seconds with base.Seconds, such that an
upstream change to dtuadmin.Seconds will not break the battery admin an
AC charger views.
2023-12-30 16:46:16 +01:00
Bernhard Kirchen
c7098b6c42
Fix: thread-safety and dynamic memory for MessageOutput (#567)
this commit re-introduces the changes from #418, which were effectively
reverted with d49481097 (merge commit introducing TaskScheduler).

these adjustments are important to guarantee unmangled log messages and
more importantly, to guarantee that all messages from a particular
component are printed to the web console, which most people use to copy
messages from when reporting issues.

* use dynamic memory to allow handling of arbitrary message lenghts.
* keep a message buffer for every task so no task ever mangles the
  message of another task.
* every complete line is written to the serial console and moved to
  a line buffer for sending them through the websocket.
* the websocket is always fed complete lines.
* make sure to feed only as many lines as possible to the websocket
  handler, so that no lines are dropped.
* lock all MessageOutput state against concurrent access.
* respect HardwareSerial buffer size: the MessageOutput class buffers
  whole lines of output printed by any task in order to avoid mangling
  of text. that means we hand over full lines to the HardwareSerial
  instance, which might be too much in one call to write(buffer, size).
  we now check the return value of write(buffer, size) and call the
  function again with the part of the message that could not yet be
  written by HardwareSerial.
2023-12-30 16:45:56 +01:00
helgeerbe
f89f9da67a Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-12-29 10:54:41 +01:00
Bernhard Kirchen
d769cdd30a
Fix: move battery's lock_guard to updateSettings() (#566)
the updateSettings method is called from the web server's context and
therefore accesses _upProvider in a different context than the
TaskScheduler. the lock_guard needs to protect _upProvider.
2023-12-29 10:48:25 +01:00
helgeerbe
3bf4d35db8 Merge branch 'development' 2023-12-27 13:37:09 +01:00
helgeerbe
6ab706c87d clean up merge conflicts 2023-12-27 13:16:37 +01:00
helgeerbe
367e0f9b6e Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-12-27 13:11:25 +01:00
helgeerbe
d494810975
merge V23.12.16 (#556)
* Optimize Sun data calculation

* Remove not required enum

* Split config struct into different sub structs

* Feature: Allow configuration of LWT QoS

* Made resetreason methods static

* Feature: Implement offset cache for "YieldDay"

Thanks to @broth-itk for the idea!
Fix: #1258 #1397

* Add Esp32-Stick-PoE-A

* remove broken LilyGO_T_ETH_POE config, use device profile instead

* Feature: High resolution Icon and PWA (Progressive Web App) functionality

Fix: #1289

* webapp: Update dependencies

* Initialize TaskScheduler

* Migrate SunPosition to TaskScheduler

* Migrate Datastore to TaskScheduler

* Migrate MqttHandleInverterTotal to TaskSchedule

* Migrate MqttHandleHass to TaskScheduler

* Migrate MqttHandleDtu to TaskScheduler

* Migrate MqttHandleInverter to TaskScheduler

* Migrate LedSingle to TaskScheduler

* Migrate NetworkSettings to TaskScheduler

* Migrate InverterSettings to TaskScheduler

* Migrate MessageOutput to TaskScheduler

* Migrate Display_Graphic to TaskScheduler

* Migrate WebApi to TaskScheduler

* Split InverterSettings into multiple tasks

* Calculate SunPosition only every 5 seconds

* Split LedSingle into multiple tasks

* Upgrade espMqttClient from 1.4.5 to 1.5.0

* Doc: Correct amount of MPP-Tracker

* Added HMT-1600-4T and HMT-1800-4T to DevInfoParser

Fix #1524

* Adjusted inverter names for HMS-1600/1800/2000-4T

* Add channel count to description of detected inverter type (DevInfoParser)

* Adjust device web api endpoint for dynamic led count

* Feature: Added ability to change the brightness of the LEDs

Based on the idea of @moritzlerch with several modifications like pwmTable and structure

* webapp: Update dependencies

* Update olikraus/U8g2 from 2.35.7 to 2.35.8

* Remove not required onWebsocketEvent

* Remove code nesting

* Introduce several const statements

* Remove not required AsyncEventSource

* Doc: Added byte specification to each command

* Feature: Added basic Grid Profile parser which shows the used profile and version

Other values are still outstanding.

* Optimize AlarmLogParser to save memory

* Add libfrozen to project to create constexpr maps

* Feature: First version of GridProfile Parser which shows all values contained in the profile.

* webapp: Update dependencies

* Apply better variable names

* Remove not required casts

* Add additional compiler flags to prevent errors

* Add const statement to several variables

* Replace NULL by nullptr

* Update bblanchon/ArduinoJson from 6.21.3 to 6.21.4

* Add const keyword to method parameters

* Add const keyword to methods

* Use references instead of pointers whenver possible

* Adjust member variable names in MqttSettings

* Adjust member variable names in NetworkSettings

* webapp: Update timezone database to latest version

* webapp: Beautify and unify form footers

* Feature: Allow setting of an inverter limit of 0% and 0W

Thanks to @madmartin in #1270

* Feature: Allow links in device profiles

These links will be shown on the hardware settings page.

* Doc: Added hint regarding HMS-xxxx-xT-NA inverters

* Feature: Added DeviceProfile for CASmo-DTU

Based on #1565

* Upgrade actions/upload-artifact from v3 to v4

* Upgrade actions/download-artifact from v3 to v4

* webapp: add app.js.gz

* Gridprofileparser: Added latest known values

Thanks to @stefan123t and @noone2k

* webapp: Fix lint errors

* Feature: Add DTU to Home Assistant Auto Discovery

This is based on PR 1365 from @CFenner with several fixes and optimizations

* Fix: Remove debug output as it floods the console

* Fix: Gridprofileparser: Add additional error handling if profile is unknown

* webapp: add app.js.gz

* Fix: Offset cache for "YieldDay" did not work correctly

* webapp: update dependencies

* webapp: add app.js.gz

* Fix: yarn.lock was outdated

* Fix: yarn build error

* Fix: Reset Yield day correction in combination with Zero Yield Day on Midnight lead to wrong values.

* Fix: Allow negative values in GridProfileParser

* Correct variable name

* Fix #1579: Static IP in Ethernet mode did not work correctly

* Feature: Added diagram to display

This is based on the idea of @Henrik-Ingenieur and was discussed in #1504

* webapp: update dependencies

* webapp: add app.js.gz

---------

Co-authored-by: Thomas Basler <thomas@familie-basler.net>
Co-authored-by: Pierre Kancir <pierre.kancir.emn@gmail.com>
2023-12-27 11:49:57 +01:00
helgeerbe
9ff39fde26 Merge branch 'development' 2023-12-15 12:00:57 +01:00
helgeerbe
fb2ca28692 add webapp 2023-12-15 11:03:48 +01:00
Bernhard Kirchen
6e78c5bd1c
Feature: JK BMS: export (more) data to live view and MQTT (#549)
* add more values to web app live view. this should add all interesting
  values for the web app live view. those include important values and
  values that change frequently.

* add more interesting JK BMS dummy messages: one has 0% SoC and an
  alarm (discharge undervoltage) set. the other has the undertemperature
  alarm set.

* add alarms and warnings to live view

* publish alarm and status bits through MQTT individually

* publish cell voltages to MQTT broker

* remove trailing spaces in BatteryStats class
2023-12-15 10:59:07 +01:00
helgeerbe
0c829a5947 Merge branch 'development' 2023-11-17 09:31:24 +01:00
helgeerbe
dd8446df0a Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-11-17 09:30:04 +01:00
helgeerbe
e44328bb13 Merge branch 'development' 2023-11-16 11:10:38 +01:00
helgeerbe
0085970567 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-11-16 09:20:08 +01:00
helgeerbe
a1d0dad128 Merge branch 'development' 2023-11-15 14:29:52 +01:00
helgeerbe
8cccce5875 add weapp 2023-11-15 14:26:54 +01:00
helgeerbe
6f7470feb4 fix: ModuleNotFoundError: No module named 'pkg_resources': 2023-11-15 14:07:27 +01:00
helgeerbe
28f46471bf set unique name for test_build action 2023-11-15 13:59:22 +01:00
helgeerbe
a004e94e57 create test_build action 2023-11-15 13:59:09 +01:00
helgeerbe
6e336f1117 set unique name for test_build action 2023-11-15 13:54:42 +01:00
helgeerbe
7637dc591b create test_build action 2023-11-15 13:52:35 +01:00
MalteSchm
3dceddfe49
Split Huawei setValue in private/public implementation. Prevent setting values when internal power control mode is enabled (#521) 2023-11-15 12:38:35 +01:00
helgeerbe
d4d42167ec Merge branch 'pr/philippsandhaus/510' into development 2023-11-15 12:33:50 +01:00
Al3x Zamponi
3af0437857
Update MqttHandlVedirectHass.cpp
Fix HASS unit
2023-11-13 17:12:19 +01:00
helgeerbe
eb578f08c5 Merge branch 'development' 2023-10-24 16:40:47 +02:00
helgeerbe
49a10305e2 fix: liveData is not updated
- due to roll back of upstream commit, duplicated code allocated json buffer twice
- enlarge json buffer to 4200
2023-10-24 15:25:31 +02:00
helgeerbe
9ca3655de6 add webapp 2023-10-23 13:32:12 +02:00
Bernhard Kirchen
254b95cb0f
Fix: replace links to upstream project where applicable (#514) 2023-10-23 13:24:05 +02:00
Bernhard Kirchen
0fa2745ace
Fix: VE.Direct refactor issues from #505 (#516)
* VE.Direct: return non-nullptr as a fallback

the changed return statement was supposed to return a shared_ptr to a
new and valid MPPT data struct as a fallback. however, it did return a
new shared_ptr that was initialized to nullptr.

* VE.Direct: make liveview total use total MPPT values

this change makes the call to VictronMppt.getData() obsolete, which in
turn will therefore not cause an error message on the console if
VE.Direct (MPPT) is not enabled. this change also takes care that once
multiple VE.Direct MPPT charge controllers are supported, the sums of
the respective total values are used in the web app totals.
2023-10-23 13:23:06 +02:00
Philipp Sandhaus
8ba9048f99
Show battery temperature when sensor is present (#511) 2023-10-23 13:22:10 +02:00
Al3x Zamponi
c5427dedce
Add calculated values to hass auto discovery (#509)
Add battery P and E as well as panel I to auto discovery values of HA
2023-10-23 13:18:24 +02:00
Philipp Sandhaus
b159c31258 Display efficiency measuremnt for Huawei AC charger in % in web interface 2023-10-20 22:14:45 +02:00
helgeerbe
b833d5ab52 add webapp 2023-10-19 16:24:19 +02:00
Bernhard Kirchen
4d4aadf8de
Feature: AC charger: configurable CAN controller frequency (#500) 2023-10-19 16:16:01 +02:00
Bernhard Kirchen
d23b991f5c
VE.Direct: Fix design issues and prepare support for multiple instances (#505)
* introduce VictronMpptClass

this solves a design issue where the loop() method of a static instance
of VeDirectMpptController, which is part of library code, is called as
part of the main loop() implementation. that is a problem because the
call to this loop() must be handled differently from all other calls:
the lib does not know whether or not the feature is enabled at all.
also, the instance would not be initialized when enabling the feature
during normal operation. that would even lead to a nullptr exception
since the pointer to the serial implementation is still uninitialized.

this new intermediate class is implemented with the support for multiple
Victron charge controllers in mind. adding support for more charge
controllers should be more viable than ever.

fixes #481.

related to #397 #129.

* VE.Direct: move get.*AsString methods to respective structs

those structs, which hold the data to be translated into strings, know
best how to translate them. this change also simplifies access to those
translation, as no parameter must be handed to the respective methods:
they now act upon the data of the instance they are called for. adds
constness to those methods.

* VE.Direct: simplify and clean up get.*AsString methods

use a map, which is much easier to maintain and which reads much easier.
move the strings to flash memory to save RAM.

* DPL: use VictronMpptClass::getPowerOutputWatts method

remove redundant calculation of output power from DPL. consider
separation of concern: VictronMpptClass will provide the total solar
output power. the DPL shall not concern itself about how that value is
calculated and it certainly should be unaware about how many MPPT charge
controllers there actually are.

* VE.Direct: avoid shadowing struct member "P"

P was part of the base struct for both MPPT and SmartShunt controller.
however, P was also part of the SmartShunt controller data struct,
shadowing the member in the base struct.

since P has slightly different meaning in MPPT versus SmartShunt, and
since P is calculated for MPPT controllers but read from SmartShunts, P
now lives in both derived structs, but not in the base struct.

* VE.Direct: isDataValid(): avoid copying data structs

pass a const reference to the base class implementation of isDataValid()
rather than a copy of the whole struct.

* VE.Direct: unify logging of text events

* VE.Direct: stop processing text event if handled by base

in case the base class processed a text event, do not try to match it
against values that are only valid in the derived class -- none will
match.

* VE.Direct MPPT: manage data in a shared_ptr

instead of handing out a reference to a struct which is part of a class
instance that may disappear, e.g., on a config change, we now manage the
lifetime of said data structure using a shared_ptr and hand out copies
of that shared_ptr. this makes sure that users have a valid copy of the
data as long as they hold the shared_ptr.

* VE.Direct MPPT: implement getDataAgeMillis()

this works even if millis() wraps around.

* VE.Direct: process frame end event only for valid frames

save a parameters, save a level of indention, save a function call for
invalid frames.
2023-10-19 16:15:29 +02:00
Bernhard Kirchen
917242909b
Fix: PowerMeter: update _lastPowerMeterUpdate for SOURCE_SML (#506)
closes #498.
2023-10-19 16:14:36 +02:00
helgeerbe
5bbbe6bfcb Merge branch 'development' 2023-10-10 09:27:21 +02:00
helgeerbe
b461aff622 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-10-10 09:26:43 +02:00
Bernhard Kirchen
116da05114
DPL: (re-)send power limits periodically (#483)
avoid staleness in case the same power limit is calculated over and over
again, hence no new power limit value is calculated and hence no power
limit command is sent to the inverter. staleness occurs in this case if
the first power limit command to establish the respective limit was not
received by the inverter. one can easily simulate a situation where the
same power limit is caluclated over and over again: with a battery above
the start threshold, set a very low upper power limit for the inverter
(DPL setting). that value will be used as the limit as long as the power
meter reading is larger than that.

we could also check the limit reported by the inverter. however, that
value is in percent of the inverter's max AC output, and is often not
the same value as we requested as the limit, but slightly off. we then
would have to decide how much deviation is okay, which is unreasonably
complicated.

closes #478.
2023-10-09 09:51:40 +02:00
helgeerbe
4324ae3081 Merge branch 'development' 2023-10-04 14:10:19 +02:00
Bernhard Kirchen
f7abbdbe06
Publish calculated MPPT metrics (#475)
* VE.Direct MPPT MQTT: remove trailing whitespace

* VE.Direct MPPT MQTT: publish P, IPV and E to MQTT

those values are calculated by OpenDTU-OnBatery and are part of the web
application live view, but were previously not published through MQTT.

closes #376.
2023-10-04 13:26:22 +02:00
helgeerbe
7fb26e1e81
Channel for AC must always be CH0 (#472) 2023-10-02 11:44:00 +02:00
MalteSchm
b1164d6c69
Move Huawei CAN bus communication to separate thread (#454) 2023-10-02 11:22:10 +02:00
helgeerbe
f0a55ea32b Merge branch 'development' 2023-09-28 13:17:59 +02:00
Philipp Sandhaus
4ee49a6ecb
Removed checking for valid tx pin for Victron (#455)
Please check wiki documentation, and change/extend it accordingly.
2023-09-28 13:13:32 +02:00
helgeerbe
933345d659 remove manual builds from action 2023-09-22 18:05:49 +02:00
helgeerbe
e937fd1cb8 add webapp 2023-09-22 17:27:17 +02:00
Philipp Sandhaus
7142921021
Integration of Victron SmartShunt via VE.Direct (#452)
* Move Mppt logic to subclass

* Added Definitions for Shunts and restructering

* First integration of SmartShunt data into Web Interface

* Code cleanup

* VE.Direct: whitespace cleanup

* VE.Direct: manage HardwareSerial in unique_ptr

* VE.Direct: _efficiency is only needed by MPPT

* VE.Direct: keep as many members private as possible

* VE.Direct: use int8_t for pins (as before)

* VictronSmartShunt: _verboseLogging is not used

* VE.Direct: OR (off reason) is MPPT specific

it also applies to Phoenix inverters and Smart BuckBoost, but since
there is no support for those, the code is moved to the MPPT controller.

* Added Shunt alarms to liveview
Changed from double to int for several readings

* Update build.yml to allow manual builds

---------

Co-authored-by: Philipp Sandhaus <philipp.sandhaus@cewe.de>
Co-authored-by: Bernhard Kirchen <schlimmchen@posteo.net>
2023-09-22 17:24:57 +02:00
helgeerbe
160d3f23bd fix: #450 power meter request fails when username or password contains @
user name and password is now encoded in the authorization header
2023-09-19 14:16:29 +02:00
helgeerbe
ae392329ea Merge branch 'development' 2023-09-19 13:06:46 +02:00
helgeerbe
b081845b95 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-09-19 12:28:00 +02:00
helgeerbe
ed2a189a61 Merge branch 'helgeerbe/issue438' into development 2023-09-18 17:56:58 +02:00
helgeerbe
f2893220a5 fix: #438 2023-09-18 17:56:22 +02:00
helgeerbe
4d0f958943 Merge branch 'development' 2023-09-18 10:48:16 +02:00
helgeerbe
9d6b459dc6 merge of v23.9.13 2023-09-18 10:46:43 +02:00
Bernhard Kirchen
b501d25ab6
JK BMS: fix SoC last update timestamp (#439)
for the MQTT integration, JkBms::DataPointContainer::updateFrom() was
changed such that the data points timestamp reflects the last change of
the data point value. that timestamp was used to set the SoC last update
timestamp. however, that timestampt must reflect the last time the SoC
was successfully received from the JK BMS so we could make sure the
value was up to date.
2023-09-18 10:30:03 +02:00
Bernhard Kirchen
954a98dbc8 JK BMS: Support for MQTT (#432)
* JK BMS: avoid trailing whitespace in debug output

* JK BMS: publish data points through MQTT

* JK BMS: updateFrom: skip data points with equal value

this changes the interpretation of the timestamp in data containers that
are merely updated from other data containers: this is the oldest
timestamp known where the value was as recorded by the data point in its
respective container.

the data container constructed from an answer will -- naturally -- have
the timetamps of its data points set to the time they were constructed.

* JK BMS: only publish changed values to MQTT broker

all values are still published once every minute if the MQTT retain flag
is NOT set. otherwise, the constant values are only published once on
startup.
2023-09-15 10:06:30 +02:00
helgeerbe
2f9539e4b3 print out debug messages 2023-09-15 09:57:25 +02:00
helgeerbe
f7bd4a40d8 revert Revert "Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development"
merge of v23.9.11 broke the system. As a workaround upgrade espressif32 from 6.3.2 to 6.4.0 is skipped. See #440
2023-09-14 13:45:23 +02:00
helgeerbe
4e489febfe Merge branch 'development' 2023-09-13 12:17:16 +02:00
Bernhard Kirchen
24018a1432
JK BMS: Support for MQTT (#432)
* JK BMS: avoid trailing whitespace in debug output

* JK BMS: publish data points through MQTT

* JK BMS: updateFrom: skip data points with equal value

this changes the interpretation of the timestamp in data containers that
are merely updated from other data containers: this is the oldest
timestamp known where the value was as recorded by the data point in its
respective container.

the data container constructed from an answer will -- naturally -- have
the timetamps of its data points set to the time they were constructed.

* JK BMS: only publish changed values to MQTT broker

all values are still published once every minute if the MQTT retain flag
is NOT set. otherwise, the constant values are only published once on
startup.
2023-09-13 12:14:29 +02:00
Bernhard Kirchen
88a5117007
VE.Direct: do not use debug buffer at all if verbose logging disabled (#437)
this avoids the debug buffer being overrun if verbose logging is
disabled in particular. that would happen because the buffer would
only be reset if verbose logging was enabled but filled in any case.
2023-09-13 12:13:56 +02:00
helgeerbe
2eeb5f1d19 Revert "Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development"
This reverts commit eb1c2dbd8c, reversing
changes made to 0cb42a6424.

merge of v23.9.11 broke the system
2023-09-12 20:18:10 +02:00
helgeerbe
a7ea15cde9 add missing Arduino.h 2023-09-12 19:43:30 +02:00
helgeerbe
eb1c2dbd8c Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-09-12 19:27:32 +02:00
MalteSchm
0cb42a6424
Reworked wifiClient handling in Power Meter httpRequest and smaller update to Power Meter updateValue method (#430) 2023-09-12 19:14:55 +02:00
Bernhard Kirchen
d984912d7c
Vedirect Cleanups (#417)
* VE.Direct: remove unused #defines

* VE.Direct: remove unused mStop member variable

* VE.Direct: whitespace cleanups

remove trailing whitespace and fix mixed indentation.
2023-09-12 19:13:28 +02:00
helgeerbe
8dd96c450b Merge branch 'development' 2023-09-04 14:26:22 +02:00
helgeerbe
3df47d1fee add webapp 2023-09-04 14:15:41 +02:00
Bernhard Kirchen
68783b450f
Messages: thread-safety and dynamic memory (#418)
* thread-safety and dynamic memory for MessageOutput

* use dynamic memory to allow handling of arbitrary message lenghts.
* keep a message buffer for every task so no task ever mangles the
  message of another task.
* every complete line is written to the serial console and moved to
  a line buffer for sending them through the websocket.
* the websocket is always fed complete lines.
* make sure to feed only as many lines as possible to the websocket
  handler, so that no lines are dropped.
* lock all MessageOutput state against concurrent access.

* MessageOutput: respect HardwareSerial buffer size

the MessageOutput class buffers whole lines of output printed by any
task in order to avoid mangling of text. that means we hand over full
lines to the HardwareSerial instance, which might be too much in one
call to write(buffer, size). we now check the return value of
write(buffer, size) and call the function again with the part of the
message that could not yet be written by HardwareSerial.
2023-09-04 14:08:30 +02:00
Bernhard Kirchen
ba303da742
VE.Direct: Reset state machine on timeout, fix and extend logging (#416)
* VE.Direct: reset state machine on timeout

there will never be a large gap between two bytes of the same frame.
if such a large gap is observed, reset the state machine so it tries
to decode a new frame once more data arrives.

this is helpful in case of corrupted data that prevents the state
machine of transitioning to the final state even though the VE.Direct
data producer is done sending bytes that belong to the same frame.

* VE.Direct: print problems to MessageOutput

this includes the web console in particular, where many users have
access to while the serial console is not attached or monitored.

* VE.Direct: collect serial input into buffer and print

should help debug issues for users.

* VE.Direct: implement and use verbose logging switch
2023-09-04 14:07:48 +02:00
Martin
a7a38e74a1
Feature: add nice Icons for HA autoconfiguration (#413)
* add Icons for Battery and Victron device sensors in Home Assistant
  overriding the boring default icon for many sensors

Signed-off-by: Martin Dummer <martin.dummer@gmx.net>
2023-09-04 14:07:24 +02:00
helgeerbe
8c36532cea add webapp 2023-08-31 16:23:06 +02:00
Bernhard Kirchen
f744629b0b
Support for Jikong JK BMS using serial connection (#319) 2023-08-31 16:21:32 +02:00
helgeerbe
2ba7ea2744 add filter for build action
- run build action only on master and development branch
- ignore v* tags from tbnobody
2023-08-30 09:54:30 +02:00
helgeerbe
d5308b1029 Merge branch 'development' 2023-08-29 10:16:33 +02:00
Bernhard Kirchen
929b477275
vite config: also proxy vedirect- and batterylivedata (#408) 2023-08-29 09:27:22 +02:00
helgeerbe
d4afc5940a Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-08-29 09:24:25 +02:00
Martin
88744bfa38
Fixes for HA autoconfig issues 378 379 (#394)
* fix: homeassistant autodiscovery topics

homeassistant autodiscovery topics contain not allowed characters, which
are now fixed.
Sidenote: when autodiscovery messages were retained, the badly formatted messages
must be removed from the mqtt server manually. Otherwise the error
messages in homeassistant will persist.

Closes: https://github.com/helgeerbe/OpenDTU-OnBattery/issues/378
Signed-off-by: Martin Dummer <martin.dummer@gmx.net>

* Fix: some homeassistant autoconf messages are wrong

Misc fixes for HA autoconfiguration:
* Entity sensor.panel_yield_today now uses state class "total"
* Entity sensor.panel_yield_yesterday now uses state class "total"
* Entity sensor.mppt_day_sequence_number_0364 now shows correct value
* sensor.panel_power duplicate removed

Closes: https://github.com/helgeerbe/OpenDTU-OnBattery/issues/379
Signed-off-by: Martin Dummer <martin.dummer@gmx.net>

---------

Signed-off-by: Martin Dummer <martin.dummer@gmx.net>
2023-08-28 13:21:20 +02:00
Bernhard Kirchen
96ee78156d
Fix DPL Mode 2 MQTT Status (#402)
* DPL MQTT handler: modernize

* there is no need to tokenize and check the topic of a received MQTT
  message if we only subscribe to a single topic. all messages will be
  for that topic. avoid testing the topic in the callback alltogether.
* use std::string and std::stoi over allocating and deleting a buffer
  and copying charactes around.
* use a switch statement to process the actual payload.
* break a long line.

* DPL: fix getMode() return value

getMode() returned a bool. probably its return type was not adjusted
when the third mode was introduced. this lead to mode 2 being cast to
true implicitly, which in turn was used to construct a String, such that
"1" was published as the DPL mode when in fact it was 2.

make the mode an enum class to avoid such problems in the future.

inline getMode() and setMode().

fix indention.
2023-08-28 13:20:56 +02:00
helgeerbe
ca308d0895 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-08-28 13:12:07 +02:00
helgeerbe
84647d80e2 dummy commit to check system info 2023-08-10 16:40:17 +02:00
helgeerbe
026bca9fe3 Merge branch 'development' 2023-08-10 14:49:33 +02:00
helgeerbe
be2846a07a on error assume branch is master 2023-08-10 14:48:52 +02:00
helgeerbe
487304125c Merge branch 'development' 2023-08-10 13:46:53 +02:00
Bernhard Kirchen
7c7a15e016
VE.Direct: websocket and status API response size (#371)
the size allocated for the HTTP request response was too little, while
the size for the buffer of the websocket output was increased already.

add a new member variable and use it in both context, such that
increasing the buffer size to accomodate more space (for the JSON data
in particular) will benefit both contexts in the future.
2023-08-10 13:40:09 +02:00
Bernhard Kirchen
b7214161b8
live view: remove disabled -OnBattery-specifc views from DOM (#372)
instead of hiding views, we can also avoid adding them to the DOM. this
has a couple of advantages:

* no HTTP request for data is sent and no websocket connection is
  established for disabled features.
* JavaScript that causes errors due to incomplete or incompatible data
  of features that are disabled anyways do not trigger the browser
  debugger.
2023-08-10 13:39:21 +02:00
helgeerbe
1100f10c99 fix use dulwich befor installation 2023-08-10 13:27:45 +02:00
helgeerbe
8e4f234517 action build on each push 2023-08-10 13:21:35 +02:00
helgeerbe
6601e9a44b catch exception during git access 2023-08-10 13:17:39 +02:00
helgeerbe
801fe5d027
Update test_build.yml 2023-08-10 13:08:16 +02:00
helgeerbe
c8a561dbd7 fix linting errors 2023-08-10 13:03:51 +02:00
helgeerbe
cc3ab9b14b
Create test_build.yml 2023-08-10 13:02:42 +02:00
helgeerbe
3702fb3eef
Delete test_build.yml 2023-08-10 13:01:49 +02:00
helgeerbe
bbc1319090
Create test_build.yml 2023-08-10 12:58:52 +02:00
helgeerbe
9475a78211 catch error if git repo can't be accessd 2023-08-10 10:53:33 +02:00
helgeerbe
5a72d74982
fix #362 system Info checks git head, instead of build branch (#374) 2023-08-09 17:45:54 +02:00
helgeerbe
40c720aa57 add webapp 2023-08-09 16:01:03 +02:00
helgeerbe
c06299878b compare git hash to branch 2023-08-09 15:59:11 +02:00
helgeerbe
940d1a6145 fix label for branch info 2023-08-09 15:47:31 +02:00
helgeerbe
401a3b86a8 show branch in system info 2023-08-09 15:32:08 +02:00
helgeerbe
21bbed9b8e Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-08-09 12:37:33 +02:00
helgeerbe
ec67fe1ff7 fix: Power meter value not displayed #355
Enlarge JsonBuffer for webservice response to 2048
2023-08-08 15:58:24 +02:00
Bernhard Kirchen
3db237c109
DPL: save verbose logging switch value to config (#363) 2023-08-07 18:20:45 +02:00
helgeerbe
c950eb7245 Merge branch 'development' of https://github.com/helgeerbe/OpenDTU into development 2023-08-06 17:02:53 +02:00
helgeerbe
3cb30b14cd add webapp 2023-08-06 17:02:44 +02:00
Bernhard Kirchen
e6eaa001e7
DPL: use testThreshold() in useFullSolarPassthrough() (#357) 2023-08-06 17:00:27 +02:00
Bernhard Kirchen
aff7924411
Inhibit solar passthrough while battery below stop threshold (#354)
* DPL: improve verbose logging

* shorten DPL log prefix
* canUseDirectSolarPower() was printed two times
* _batteryDischargeEnabled was printed two times
* convert boolean values to human-readable strings
* add units where possible
* split messages into block "before calculating new limit" and "after
  calculating new limit", as the latter cannot rely on _inverter being
  available.
* order messages such that variables whose value is derived from other
  variables are printed later than their dependencies.
* merge output into blocks (one instance near "Printout some stats")
* remove more redundant info (produced in functions outside loop())
* print target grid consumption

* DPL: inhibit solar passthrough while stop threshold reached

* DPL: implement and use isBelowStopThreshold()

we only want to inhibit solar passthrough if the SoC is *below* the stop
threshold, not if it is equal to the stop threshold. otherwise, when
discharging, we would discharge until the battery reached the stop
threshold, then we would also inhibit solar passthrough, until the
battery is charged to the SoC stop threshold plus one percent.
2023-08-04 12:35:37 +02:00
helgeerbe
5335ec1bde Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-08-04 12:21:16 +02:00
helgeerbe
69456affce Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-08-02 19:46:35 +02:00
Bernhard Kirchen
81864b3420
execute MQTT client synchronously in main loop() (#350)
processing a published valued on a subscribed topic is currently running
in a task that is not the task executing the main loop(). that's because
the espMqttClient(Secure) was constructed without arguments, which
selects the constructor with two arguments priority and core, both of
which have default values. that constructor selects
espMqttClientTypes::UseInternalTask::YES, causing a task to be created
in which context the MQTT client loop is executed.

MQTT subscribers assume they are running in the same context as the main
loop(). most code assumes exactly that. as the scheduler is preemptive
and very little (none at all?) code is interlocked, we have to make sure
to meet the programmer's expectations.

this changeset calls the MQTT client loop in the context of the main
loop() and enforces the use of espMqttClientTypes::UseInternalTask::NO.
2023-08-01 09:20:04 +02:00
helgeerbe
587b2dc553 add webapp 2023-07-31 14:21:24 +02:00
Bernhard Kirchen
2bce8311a7
MQTT verbose logging fixes (#341)
* MQTT verbose logging: fix typo

this typo caused that verbose logging was always disabled for MQTT and
could not be enabled.

* webapp status: show MQTT verbose logging setting
2023-07-31 14:16:35 +02:00
Bernhard Kirchen
6b425d96b0
PowerMeter fixes (#342)
* PowerMeter: gracefully handle non-float MQTT values

* PowerMeter: update _lastPowerMeterUpdate conservatively

update the timestampt only if the topic actually matched any
subscription and if the value could be parsed as a float.

* PowerMeter: unsubscribe before subscribing

* PowerMeter: organize subscriptions in a map

this allows for a slightly more elegant code and reduced amount of code
overall.

* PowerMeter: clean up header

* move private methods to private section of class declaration.
* remove unused member variable.
2023-07-31 14:16:06 +02:00
Bernhard Kirchen
9bc334e368
reset docs/MQTT_Topics.md to upstream version (#343)
MQTT topics that are specific to OpenDTU-OnBattery shall only be
documented once. They are documented in the project's wiki already.
2023-07-31 14:14:32 +02:00
helgeerbe
43e836ac41 resolve merge conflict with upstream/master 2023-07-24 13:23:50 +02:00
helgeerbe
2440028d38 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-07-24 13:16:15 +02:00
MalteSchm
18c464e524
SoC based threshold detection fix (#320) 2023-07-24 13:03:12 +02:00
helgeerbe
8b01fa07cc Replace Readme with wiki home page 2023-07-24 13:02:12 +02:00
Phantomias2006
e06740fbb8
Add data age at battery MQTT (#322)
* Add data age at battery MQTT

* Update README.md

typo

* Update MQTT_Topics.md

typo

* Update PylontechCanReceiver.cpp

typo
2023-07-19 09:47:37 +02:00
helgeerbe
cea1f94b5e add webapp 2023-07-18 10:01:36 +02:00
Bernhard Kirchen
f0def2ae89
Selective verbosity (#318)
* DPL: implement verbose logging switch

* MQTT: implement verbose logging switch

* power meter: implement verbose logging switch

* Hoymiles lib: implement verbose logging switch

* cpp linting: "final" makes "virtual" and "override" redundant

... however, using only "final" is not as verbose.
2023-07-18 09:57:03 +02:00
Phantomias2006
a7da000345
Feature: set Huawei offline parameters via MQTT (#315)
* Add Huawei offline parameters via MQTT

* Update README.md

Correction of the docu

* Update MQTT_Topics.md

Correction of the docu
2023-07-18 09:52:31 +02:00
Martin
0dd1566dc6
Fix: WebApp Live-View: adjust window-title and header (#317)
adjust window-title and header from OpenDTU to OpenDTU-onBattery
Additionally change orientation of battery symbol to vertically centered
(looks nicer)

Signed-off-by: Martin Dummer <martin.dummer@gmx.net>
2023-07-18 09:51:59 +02:00
Martin
c393e52185
Feature: add Home Assistant MQTT discovery for Pylontech battery (#314)
When OpenDTU has a Pylontech CAN Bus Battery connected and enabled, this
patch adds the discovery routine for Home Assistant

Signed-off-by: Martin Dummer <martin.dummer@gmx.net>
2023-07-17 09:50:58 +02:00
Bernhard Kirchen
cbc99d715f
DPL: increase backoff while inverter is kept shut down (#310)
if the new calculated power limit is below the minimum power limit
setting, the inverter is shut down. the shutdown() function is called
every time this condition is detected, which is also true if the
inverter is kept shut down for longer. that happens while the battery
is charging in particular (solar passthrough off). there are other
cases.

in such cases we still want to get into the DPL status "stable". to be
able to determine this stable state, we must know if the call to
shutdown did actually initiate a shutdown or if the inverter is already
shut down.

we then can forward this "changed" or "not changed" info up the call
chain, where the loop() will know that the system is actually stable.
2023-07-17 09:40:18 +02:00
helgeerbe
f68f68be77 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-07-13 12:19:21 +02:00
helgeerbe
633ef88296 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-07-12 13:45:19 +02:00
Bernhard Kirchen
f3297930b5
DPL: account for solar passthrough losses (#307)
* fix another fixable "passtrough" typo

the typo in the config's identifier is not changed to preserve
compatibility while not spending the effort to migrate the setting.

* webapp language: prefer SoC over SOC

* DPL: implement solar passthrough loss factor

in (full) solar passthrough mode, the inverter output power is coupled
to the charge controler output power. the inverter efficiency is already
accounted for. however, the battery might still be slowly discharged for
two reasons: (1) line losses are not accounted for and (2) the inverter
outputs a little bit more than permitted by the power limit.

this is undesirable since the battery is significantly drained if solar
passthrough is active for a longer period of time. also, when using full
solar passthrough and a battery communication interface, the SoC will
slowly degrade to a value below the threshold value for full solar
passthrough. this makes the system switch from charging the battery
(potentially rapidly) to discharging the battery slowly. this switch
might happen in rather fast succession. that's effectively
trickle-charging the battery.

instead, this new factor helps to account for line losses between the
solar charge controller and the inverter, such that the battery is
actually not involved in solar passthrough. the value can be increased
until it is observed that the battery is not discharging when solar
passthrough is active.
2023-07-12 13:20:37 +02:00
MalteSchm
95d7ac7adf
Disable debug mode, increasing power threshold for active channel detection (#301) 2023-07-12 13:19:24 +02:00
helgeerbe
1d559c1c40 add webapp 2023-07-09 17:11:09 +02:00
MalteSchm
475aab1e9a
floating point input (#302) 2023-07-09 17:09:33 +02:00
Martin
cdf5c85510
fix: Home Assistant MQTT-Auto-Discovery with VE.Direct (#297)
In Home Assistant, when Home Assistant MQTT-Auto-Discovery is active,
almost all Sensors of the auto-discovered Victron device in Home
Assistant become "unavailable" after a short time - except those
Sensors with frequent changes like battery voltage or panel voltage.

This patch introduces regular mqtt updates for all VE.Direct sensors
when MQTT-Auto-Discovery is enabled.

Signed-off-by: Martin Dummer <martin.dummer@gmx.net>
2023-07-09 17:08:50 +02:00
Bernhard Kirchen
23ff4ef22a
DPL: do not use nullptr when printing debug messages (#303) 2023-07-09 16:42:50 +02:00
helgeerbe
2a858e096b fix: typo 2023-07-07 17:22:04 +02:00
helgeerbe
3fb062b5cc fix: typo 2023-07-07 17:08:51 +02:00
helgeerbe
ffa9be0835 fix: #294 Passthrough Spelling 2023-07-07 17:05:44 +02:00
helgeerbe
344498d440 fix: linting error 2023-07-04 14:55:55 +02:00
helgeerbe
d3adc65d11 add webapp 2023-07-04 12:07:50 +02:00
Bernhard Kirchen
107182f948
DPL: check more requirements and fix backoff initialization (#290)
* DPL: wait for valid time information

we know that the Hoymiles library refuses to send any message to any
inverter until the system has valid time information. until then we can
do nothing, not even shutdown the inverter.

* DPL: wait for device info to be ready

a calculated power limit will always be limited to the reported
device's max power. that upper limit is only known after the first
DevInfoSimpleCommand succeeded. wait for that information to be
available.

* DPL: fix initial calculcation backoff

if the calculation backoff is initialized to zero, the backoff will be
doubled to zero until a new, different power limit was calculated for
the first time. this lead to the DPL recalculating a power limit
hundreds of times without a backoff after startup.
2023-07-04 12:05:10 +02:00
Bernhard Kirchen
e457ab73f9
Fix vedirect polling (#291)
* VE.Direct: remove polling interval

the polling interval was meant to limit the amount of MQTT updates.
however, that is already controlled by the global MQTT publish interval.
the removed interval was instead used to limit polling of the VE.Direct
UART for incoming data.

the Victron device sends data unsolicited. the VeDirectFrameHandler does
not implement any polling mechanism. no data is ever sent to the Victron
device.

what the removed polling interval did was cause a buffer overrun of the
HardwareSerial class, since the incoming data was not processed in time.
so every five seconds, we read a whole valid VE.Direct frame, plus some
old data, which was not a whole frame, leading to VE.Direct error
messages to pop up.

with the polling interval removed, no framing errors are reported, and
instead we gain new data from the charge controller approximately ever
two seconds -- for free.

* VE.Direct: change texts to correct VE.Direct capital letters

* VE.Direct: improve "UpdatesOnly" switch labels

especially since the publish interval setting is gone, the label makes
it hard to comprehend what the switch does. update the texts to better
explain what the switch is used for.

use the same text on the VE.Direct info view.

* VE.Direct: use StatusBadge on info view

there were custom badges to indicate the VE.Direct settings. replace
those by the common StatusBadge to make then look the same as the other
badged on the info views.
2023-07-04 12:04:38 +02:00
helgeerbe
006f63ed02 feature: add digest auth on power meter
Power Meter -> HTTP(S) + Jason configuration allows now basic and digest authentication (all Shelly Gen2 devices)
2023-07-04 11:54:46 +02:00
helgeerbe
9a4eb75160 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-07-02 14:32:12 +02:00
helgeerbe
1f39ed7b9b Merge branch 'pr/MalteSchm/288' into development 2023-07-02 14:28:51 +02:00
helgeerbe
afd8790c3c Merge branch 'pr/schlimmchen/287' into development 2023-07-02 14:20:25 +02:00
Bernhard Kirchen
99876d7c6b
DPL: fix the configured upper limit not being respected (#286)
the requested newPowerLimit was indeed limited to the configured maximum
inverter output and the result was stored in effPowerLimit. later,
however, effPowerLimit must be scaled up (if necessary), not
newPowerLimit. the latter variable is not respecting the configured
maximum inverter output.
2023-07-02 14:11:44 +02:00
Bernhard Kirchen
97f58eeba7
explain DPL modes set through MQTT in more detail (#282)
* explain DPL modes set through MQTT in more detail

* docs: DPL mode change through MQTT does not enable DPL
2023-07-02 14:11:23 +02:00
Bernhard Kirchen
097d464bbb
DPL: use vedirect isdatavalid (#283)
* DPL: use VeDirect.isDataValid()

in case the communication to the Victron charger is disrupted, the
respective values in VeDirect.veFrame are not invalidated such that we
would notice a problem. instead, isDataValid() should be used to make
sure that the Victron charger is actually alive and that we can trust to
use the reported values.

* DPL: simplify canUseDirectSolarPower return statement
2023-07-02 14:10:47 +02:00
MalteSchm
e279ce08c4 Dynamic power control and more power control modes on the Huawei PSU 2023-07-01 12:37:55 +02:00
Bernhard Kirchen
9aeb1583b5 DPL: consider the system stable when reusing the old limit
a new status is needed to communicate that no update was sent to the
inverter because its power limit is still valid. in this case,
calculating a new power limit is delayed by an exponentially increasing
backoff. the maximum backoff time is ~1s, which is still plenty fast.

the backoff is actually necessary for another reason: at least
currently, a lot of debug messages are printed to the console. printing
all that information in every DPL loop() is too much.
2023-06-30 21:00:51 +02:00
Bernhard Kirchen
461fce8ff4 DPL: do not repeat being disabled 2023-06-30 21:00:51 +02:00
Bernhard Kirchen
9bab740c43 DPL: replace _plState enum with a simple boolean switch 2023-06-30 21:00:51 +02:00
Bernhard Kirchen
b2d58af5e8 DPL: separate unconditional solar passthrough mode
the unconditional solar passthrough mode, configured using MQTT, works
differently than the normal mode of operation. it is also independent
from the power meter reading. if this mode is active, a shortcut is
taken to a function that implements the actions for this mode. this is
convenient since we don't have to consider special cases in the code
that handles normal mode of operation.
2023-06-30 21:00:51 +02:00
Bernhard Kirchen
71079fa0cc DPL: work on internal copy of pointer to inverter
the DPL already took care to shut down the inverter if anything fishy
was going on, mainly to make sure that the battery is not drained.
however, some cases were missed:

* if the configuration changed such that another inverter is now
  targeted, the one the DPL controlled previously was not shut down.
* if the configuration changed such that another inverter (different
  serial number) was configured at the same index, the previous one
  was not shut down.

this change corrects these problems by making the DPL keep a copy of the
shared_ptr to the inverter. the shared_ptr is only released once the DPL
shut the respective inverter down.
2023-06-30 20:04:39 +02:00
Bernhard Kirchen
2970e84193 DPL requirement: disabled inverter commands
if the inverter is not configured to be sent commands to, the DPL is
unable to control it, so the loop is aborted.
2023-06-30 20:04:39 +02:00
Bernhard Kirchen
18b1076660 DPL: improve responsiveness
this implementation checks all requirements for a new power limit to be
calculated, one after the other. if any requirement is not met, a
respective status is announced.

status messages are communicated on the (serial) console. these can also
be displayed easily on the web app in the future. the status texts
explain clearly what the DPL is currently doing, which aids
understanding how the DPL works. the status is only announced if it
changes, or after a fixed interval.

as each requirement is checked individually, the code readability is
improved as well. previously, all the respective conditions had to be
checked as well, but the statements were more complex.

the DPL loop is now executed with high frequency, i.e., it does not wait
for a fixed timespan to pass before checking requirements. it always
aborts on the first unmet requirement. this should improve responsiveness,
as the DPL checks all requirements more often.

the DPL now waits for all power commands and power limit updates to
complete. when that is the case, a settling time elapses. after the
settling phase, the DPL waits for a new update from the inverter and
from the power meter. now it can be assumed that the values are in sync.
it then makes sense to calculate a new power limit immediately, which
the DPL then does.
2023-06-30 20:04:39 +02:00
Bernhard Kirchen
0b0bcf1dfb fix typo: getLastRequestedPowe*w*rLimit() 2023-06-30 20:04:39 +02:00
Bernhard Kirchen
fd208cf6bb DPL: remove unused member variables 2023-06-30 20:03:21 +02:00
Bernhard Kirchen
8b23324693 ensure only one PowerCommand and ActivePowerCommand is queued
neither a PowerCommand nor an ActivePowerCommand shall be enqueued if
another one is already pending.
2023-06-30 20:03:21 +02:00
Martin
8433820529
Fix typo in error messages (#280)
Signed-off-by: Martin Dummer <martin.dummer@gmx.net>
2023-06-29 13:06:28 +02:00
helgeerbe
0caffe9411 fix: Allow use GPIO0 as onBattery pins 2023-06-29 13:04:35 +02:00
helgeerbe
6ffb5bb897 fix: undefined pin is shown as -1 instead of 255
change pin type for onBattery pins from uint8_t to int8_t.  Negativ pin values means not defined
2023-06-29 13:02:38 +02:00
helgeerbe
2223afac62 add webapp 2023-06-26 12:30:30 +02:00
helgeerbe
acbf80d196 Merge branch 'helgeerbe/issue274' into development 2023-06-26 12:28:28 +02:00
helgeerbe
6ed3ce7417 Fix: #274
Iverter is behind power meter - switch does not work
2023-06-26 12:27:26 +02:00
Bernhard Kirchen
d6163ddb7d
set default values for pin mappings if not provided (#275) 2023-06-26 11:57:29 +02:00
Bernhard Kirchen
07bb0b03f7
DPL hysteresis fix and refactor of setNewPowerLimit() (#264) 2023-06-26 11:52:33 +02:00
helgeerbe
7d73ae3c20 Fix: linting error 2023-06-22 21:55:58 +02:00
Bernhard Kirchen
4d05035661
fix DPL not reading full solar passthrough voltage thresholds (#265)
the defaults for solar passthrough voltage thresholds shall be floats,
so the user can store float values to the config. otherwise, float
values can and will be stored, but when reading them, the defaults will
be applied as the defaults are of an incompatible type.
2023-06-22 21:33:38 +02:00
Bernhard Kirchen
9995c1172e
VE.Direct live view enhancements (#269)
* add calculated values to VE.Direct data

solar current, battery output power, and the charger's efficiency can be
calculated from the values reported by the charger. the efficiency must
be taken with a grain of salt. it seems that the solar power value and
the battery output voltage/current are not always in sync. for that
reason a moving average is used to smooth out the calculated efficiency
value.

* show calculated VE.Direct values in web live view

order the values and translations similarly for the input and output,
starting with power at the top, then voltage, then current as the last
of these three.

* VE.Direct live view: use 'd' as unit for days

'd' is the SI unit symbol for days and does not need translation, which
is desirable as units are not translated throughout the project.

* refactor VE.Direct live view

* move Dynamic Power Limiter data into its own type.
* split VE.Direct data into three types: "device", "input", and
  "output". hence all input and output values are now ValueObject, which
  allows to iterate over them using a loop without typing issues.
* generate the tables with input and output values using a loop, rather
  than defining each row individually.
* localize numbers using $n (vue method), which fixes switching the
  number format (dot vs. comma) when switching the language.
* use no decimal point for power values (they are integers), three
  decimal points for kWh values (charger only reports two decimal
  places, but three are easier to read since the unit is *kilo* Wh), one
  decimal point for the efficiency, and two for voltage and current.
* update language tokens to avoid mapping JSON keys to language keys
  (use the JSON keys to access the language tokens).
* re-structure language tokes so the brief keys took over from
  VeDirectFrameHandler always make sense (nest into "input" and
  "output").
* order values similarly from top to bottom: power, then voltage, then
  current. this is following the order of the inverters' details.
* group values by type/unit (yield and max. power) and order them
  "newest" to "oldest" from top to bottom.
* increase the DynamicJsonDocument as it was too small to hold the newly
  added data.

* update webapp_dist to include VE.Direct live view refactoring
2023-06-22 21:32:20 +02:00
Bernhard Kirchen
016e30ec00
DPL: fix efficiency calculation (#270)
there is no need to assume and hardcode a fixed efficiency for the
Victron solar charger. the charger reports the voltage and current at
its battery terminal, which can be used to calculate the charger's
actual power output.

the fallback to 100% for the efficiency of the Hoymiles inverter, in
case it is not producing power, is too optimistic. this commit proposes
to use 96.7% as the efficiency for that case, which is the peak
efficiency for many (all?) Hoymiles inverters as per datasheet. that
value should be closer to the real efficiency that will be achieved once
the inverter is turned on.
2023-06-22 21:30:33 +02:00
helgeerbe
cd1db49a98 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-06-22 21:14:12 +02:00
helgeerbe
f018a0136e Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-06-21 16:52:02 +02:00
helgeerbe
fd58ad2003 feat(add ability to do a polite power meter read): 2023-06-14 16:21:58 +02:00
helgeerbe
080a3eb29e Merge branch 'PM_onDemand' into development 2023-06-14 13:22:19 +02:00
helgeerbe
da7628dafc Fix: Respect minimum on demand poll rate for power meter 2023-06-14 13:21:50 +02:00
helgeerbe
ceb28030a4 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-06-14 13:02:00 +02:00
helgeerbe
ded5ceec58 Fix: Make power limiter intervall configurable
Allows to change the power limiter intervall settings in the config file.
2023-06-14 12:49:54 +02:00
helgeerbe
86ee7e1a64 perf(PowerMeter): query PowerMeter on demand
In addition to the cyclic query of the power meters, they will be queried on demand when total power is requested and last update is older than 1 second.
2023-06-14 11:25:54 +02:00
helgeerbe
8dac88e7b9 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-06-12 13:07:44 +02:00
helgeerbe
0fd7b75e83 Fix(Power Limiter): hysteresis is not repected properly 2023-06-12 13:06:31 +02:00
helgeerbe
51a21de189 Fix: broken github build action, due to merge with core 2023-06-09 13:37:57 +02:00
helgeerbe
e68baa3086 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-06-09 13:16:56 +02:00
MalteSchm
e0a8da84d7 full solar passthrough 2023-06-09 13:09:58 +02:00
helgeerbe
a6f5e8a3a2 add webapp 2023-06-05 10:13:18 +02:00
helgeerbe
8298a0c36f
Merge pull request #255 from madmartin:inverter_restart
Feature: add daily restart for the inverter
2023-06-05 10:10:14 +02:00
Martin Dummer
c727e21a49
Feature: add daily restart for the inverter
Add a configurable restart option for the inverter to set the "YieldDay"
values to zero.

Signed-off-by: Martin Dummer <martin.dummer@gmx.net>
2023-06-04 09:14:42 +02:00
helgeerbe
91ce844b36 fix(linter warining): use snprintf istead of strcopy 2023-06-02 13:12:42 +02:00
MalteSchm
e7c8a89bd3 inital version of full solar passthrough
Webinterface change to set full solar passthrough values

Adding webapi and config changes to enable full solar passthrough over certain battery Soc

inital version of full solar passthrough in power limiter

Passthrough mode can be enabled via MQTT

translations

re-enable comment

remove unused variable
2023-06-02 12:49:24 +02:00
helgeerbe
2e811b7ab1 fix(action): ignore tags starting with v 2023-05-24 12:30:37 +02:00
Martin
52af52eb3a
Pylontech enhancement (#239) 2023-05-24 09:17:38 +02:00
helgeerbe
a3e7439181 fix(action): sort tags in chronological order 2023-05-24 09:13:04 +02:00
helgeerbe
9f511fb985 fix(action): sort tags in chronological order 2023-05-23 11:43:26 +02:00
helgeerbe
8b64671151 doc: warning, that only 5 inverters are supported at max 2023-05-23 11:04:56 +02:00
helgeerbe
13b318690d fix: linting errors 2023-05-23 10:24:06 +02:00
helgeerbe
47f81f2579 breaking change: Reduce maximum number of invertes to 5 to keep livedata working
livedata json is now calculated as 4096 * MaxInvertes
2023-05-23 10:15:37 +02:00
Martin
3a991708d0
docs/Web-API.md: fix vedirectlivedata typo (#238)
- fix vedirectlivedata typo
- add some more api urls

Signed-off-by: Martin Dummer <martin.dummer@gmx.net>
2023-05-23 09:25:36 +02:00
helgeerbe
b7ac70b1ca Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-05-23 09:19:57 +02:00
helgeerbe
1c7b7d3cdf Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-05-10 18:16:25 +02:00
helgeerbe
556f3e0abf docs: Show as badge in readme original openDTU release number
Badge shows the openDTU release which is used as core to build openDTU-onBattery's release
2023-05-10 15:27:03 +02:00
helgeerbe
40a65198fe add webapp 2023-05-08 13:27:24 +02:00
MalteSchm
065c169b20
Set initial PL state in init() to avoid inverter shutdown on reboot... (#224) 2023-05-08 13:25:02 +02:00
helgeerbe
178d40d5b4 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-05-04 20:24:29 +02:00
MalteSchm
569edbe69e
static casts set last requested limit for all cases where inverter power is changed (#213) 2023-05-01 21:20:49 +02:00
helgeerbe
33423dfc2f Merge branch 'pr/MalteSchm/199' into development 2023-04-30 14:11:57 +02:00
MalteSchm
6fe19f00a7 drafted description of power limiter operation
Rework to include start threshold for solar passthrough off case, simplfied text as there are now more similarities

adding discharge at night text
2023-04-30 13:23:01 +02:00
helgeerbe
d1885b6177 workaround for faild builds on github actions 2023-04-29 23:19:08 +02:00
helgeerbe
8021052cfd PL remove debug messages 2023-04-29 22:53:50 +02:00
helgeerbe
e5af5be70a remove comment 2023-04-29 21:53:05 +02:00
MalteSchm
db4125ae7a debug printouts, removed unnecessary check causing inverter to toggle 2023-04-29 14:28:24 +02:00
helgeerbe
dae4c6fbf5 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-04-28 22:38:39 +02:00
helgeerbe
2d14d9f69e Merge branch '198-enhancement-simplifying-the-settings-menu' into development 2023-04-28 20:11:07 +02:00
helgeerbe
3f0291dce0 : Enhancement - Simplifying the settings menu #198 2023-04-28 20:08:36 +02:00
helgeerbe
5d4c6866da
fix: ignore MQTT messages for disabled components #203 (#204) 2023-04-28 19:26:58 +02:00
MalteSchm
c621f2d3e3
Power limiter fixes (#201) 2023-04-28 13:59:18 +02:00
helgeerbe
80f1af32f8 Fix: Link to git hash references onBattery repo now 2023-04-27 21:55:07 +02:00
helgeerbe
46c733ca31 Revert "Fix: Link to git hash references onBattery repo"
This reverts commit 23b35d9b00.
2023-04-27 21:45:50 +02:00
helgeerbe
32913c2b2e Revert "fix: change url for firmware info to"
This reverts commit dfa0a1c98b.
2023-04-27 21:45:50 +02:00
helgeerbe
dfa0a1c98b fix: change url for firmware info to
openDTU-onbattery
2023-04-27 21:31:38 +02:00
helgeerbe
cc32e3973e Fix: Link to git hash references onBattery repo
now

add webapp
2023-04-27 20:32:41 +02:00
helgeerbe
23b35d9b00 Fix: Link to git hash references onBattery repo
now
2023-04-27 20:15:57 +02:00
helgeerbe
4abc89d43c build release only if tag starts with 2 2023-04-27 19:59:18 +02:00
MalteSchm
70060559da
Bring back the sun indicator (#195)
* Adding states to display in UI

* Adding states to display in UI
2023-04-27 19:10:12 +02:00
MalteSchm
6b437b5ea1
Inverter toggle fix (#196)
* updating the interface to calcPowerLimit to include both energy sources

* Fixing definition
2023-04-27 19:09:48 +02:00
helgeerbe
9ff9a8a6d6
Merge pull request #192 from madmartin/build-test
Fix: github build create opendtu-onbattery.* files
2023-04-27 19:09:21 +02:00
Martin Dummer
00490b80af build test 2023-04-27 06:50:30 +02:00
helgeerbe
89209b6bf7 Actions and badges reflects openDTU-onBattery now 2023-04-26 15:23:17 +02:00
helgeerbe
83b42a36b9 Merge branch 'development' 2023-04-26 15:02:40 +02:00
helgeerbe
e65b2196bf add webapp 2023-04-26 12:53:50 +02:00
helgeerbe
d4a5570806 Feature: Add battery Icon on Navbar, to
indicate openDTU-onBattery is running
2023-04-26 12:52:28 +02:00
helgeerbe
c337df605c Merge branch 'pr/MalteSchm/172' into development 2023-04-26 12:37:31 +02:00
helgeerbe
e91935ab38 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-04-26 11:47:19 +02:00
helgeerbe
37b5edb010 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-04-25 18:51:18 +02:00
helgeerbe
71dda4b89c Merge branch 'pr/qubeck/156' into development 2023-04-25 18:39:02 +02:00
MalteSchm
c7505eaae6 useSolarPowerOnly did not take current consumption into account. fixed 2023-04-25 12:07:24 +02:00
MalteSchm
0a0488f73a refactored use solar power code 2023-04-25 12:03:41 +02:00
MalteSchm
a8554f97b0 refactored use solar power code 2023-04-25 11:05:08 +02:00
MalteSchm
322f532ac0 Proper handling of use solar power only case 2023-04-25 10:41:35 +02:00
helgeerbe
8f386c8611
Merge pull request #182 from MalteSchm:readme_refactor
Documentation updates / Refactored Readme
2023-04-24 10:54:26 +02:00
MalteSchm
e2a6468304 Webapp build 2023-04-23 18:50:24 +02:00
MalteSchm
8764809259 Webapp update do display power values 2023-04-23 18:49:32 +02:00
MalteSchm
3861ab89f1 WebApi update to include power values 2023-04-23 18:49:32 +02:00
helgeerbe
75a59f5d1d
Merge pull request #175 from MalteSchm:huawei_spi_interface_fix
Changing Huawei SPI interface
2023-04-23 18:32:36 +02:00
MalteSchm
8c9afbcdc0 fix an issue if inverter is behind power meter 2023-04-23 11:30:08 +02:00
MalteSchm
422ad3f909 reverting to old Readme, moving documentation to new Readme 2023-04-22 12:24:24 +02:00
MalteSchm
71da704d38 Refactor readme, factored out hardware and flashing in separate document. Integrated MQTT, Webapi, PinMappings related text in the correct sections. Updated documentation / diagrams to reflect the hardware required 2023-04-22 10:47:00 +02:00
MalteSchm
1e30323915 Renaming readme 2023-04-22 10:43:45 +02:00
helgeerbe
38b990fbbc Fix: PowerLimiter is now casted to signed int to allow negativ values. 2023-04-20 20:02:59 +02:00
helgeerbe
41cc5ff4f9 add webapp 2023-04-19 14:34:07 +02:00
helgeerbe
84a6ed540d Merge branch 've.direct-hex' into development 2023-04-19 14:33:00 +02:00
helgeerbe
943a0eb58e Fix: [Request] rename "Erlaubter Stromverbrauch" to "Angestrebter Netzbezug" helgeerbe/OpenDTU-OnBattery#178 2023-04-19 14:25:24 +02:00
helgeerbe
6a9585cd1e Fix: Power Limiter Settings / cannot set permitted current below zero helgeerbe/OpenDTU-OnBattery#171 2023-04-19 14:19:26 +02:00
helgeerbe
545a18db6f Merge branch 'development' 2023-04-18 12:09:48 +02:00
helgeerbe
a1b3bdfee8 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-04-18 12:09:22 +02:00
helgeerbe
8cc548d13e add pid 0XA110 SmartSolar MPPT RS 450|100 2023-04-17 22:33:11 +02:00
helgeerbe
b8e06bf824 ve.diect with hex asnync messages
ignore async hex messeage on older devices
2023-04-17 22:18:20 +02:00
helgeerbe
1e16eca16e Merge branch 'development' 2023-04-17 11:14:03 +02:00
helgeerbe
40cee1f9ca Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-04-17 11:08:55 +02:00
MalteSchm
3504924bb0 Changing SPI interface 2023-04-16 18:34:09 +02:00
MalteSchm
82699b1c88 Merge branch 'mqtt_power_limiter_enable' of https://github.com/MalteSchm/OpenDTU-OnBattery into mqtt_power_limiter_enable 2023-04-16 18:30:47 +02:00
MalteSchm
4ddaa7643b make sure that PL is enabled if user uses webinterface 2023-04-16 18:30:21 +02:00
MalteSchm
0d0a624fe2 make sure that PL is enabled if user uses webinterface 2023-04-16 18:28:49 +02:00
MalteSchm
a306bc1351 Bugfix for Mqtt enable / disable (was float, uses int now) 2023-04-16 18:28:09 +02:00
helgeerbe
b00ca02aac fix layout for vedirect live data on small devices 2023-04-13 11:38:20 +02:00
helgeerbe
1a0f44dac4 Merge branch 'development' 2023-04-13 10:21:23 +02:00
helgeerbe
1dc73f91ee Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-04-13 10:18:49 +02:00
MalteSchm
3c5082287e remove a leftover debug message 2023-04-13 10:07:25 +02:00
MalteSchm
690025e5fd fixing a bug from merging and remove a leftover debug message 2023-04-13 10:06:06 +02:00
MalteSchm
be7a43fbfb Removing un-necessary timestamp, commenting code and cleanups 2023-04-13 09:47:12 +02:00
MalteSchm
01849dc90a adding Mqtt handling to main.cpp 2023-04-13 09:47:10 +02:00
MalteSchm
ee376827fd merging functionality from PL refactor 2023-04-13 09:47:07 +02:00
MalteSchm
9999fa28e8 refactor state machine
merging
2023-04-13 09:46:35 +02:00
MalteSchm
ee82c8c9b8 adding option to disable power limiter via mqtt
adding option to disable power limiter via mqtt - adding missing file
2023-04-13 09:44:48 +02:00
MalteSchm
9efe076cc2 resolving merge conflict
adding missing statement from merge

fixing a bug introduced in merge
2023-04-13 09:39:04 +02:00
MalteSchm
fc5089e70b resolving merge conflict 2023-04-13 09:36:00 +02:00
MalteSchm
9ff1885d5a Removing un-necessary timestamp, commenting code and cleanups 2023-04-13 07:45:26 +02:00
qubeck
b79619bf8b add explicit checks to avoid potential div. by zero on application of artificially increased power limit if channel power becomes zero 2023-04-12 20:44:38 +02:00
helgeerbe
1970b939e0 add webapp 2023-04-12 17:50:39 +02:00
helgeerbe
1d62e36303 Merge branch 'development' 2023-04-12 17:49:31 +02:00
MalteSchm
03ce71519d Merge remote-tracking branch 'upstream/development' 2023-04-12 16:16:21 +02:00
MalteSchm
74dcddaa1a merging linting error fixes 2023-04-12 16:14:33 +02:00
Bernhard Kaszt
616d0425db
Merge pull request #170 from MalteSchm/only_linting_error_fix
webapp / yarn linting issues
2023-04-12 16:13:19 +02:00
MalteSchm
43f7553cdd removing commented lines 2023-04-12 15:56:33 +02:00
MalteSchm
72289cda1a fixed linting issues, pushing to github for test run 2023-04-12 13:21:32 +02:00
MalteSchm
d2fc00b7d6 Merge branch 'master' of https://github.com/MalteSchm/OpenDTU-OnBattery 2023-04-12 13:06:21 +02:00
MalteSchm
f99f80159d Merge remote-tracking branch 'upstream/development' 2023-04-12 13:05:53 +02:00
MalteSchm
c430b69322
Merge branch 'helgeerbe:master' into master 2023-04-12 13:02:10 +02:00
MalteSchm
79834e4d47 adding Mqtt handling to main.cpp 2023-04-12 12:51:24 +02:00
MalteSchm
1b29133ee0 merging functionality from PL refactor 2023-04-12 12:21:52 +02:00
Bernhard Kaszt
1d29781804
Merge pull request #167 from MalteSchm/only-typo-fix
Typo fix
2023-04-12 11:00:35 +02:00
Bernhard Kaszt
d28c9dbc4b
Merge pull request #168 from MalteSchm/only-mppt-150-fix
Mppt string for SmartSolar 150/48 is wrong
2023-04-12 10:59:51 +02:00
MalteSchm
515bb1c7ce merging mqtt enable/disable with new refactored state machine code 2023-04-12 10:35:55 +02:00
MalteSchm
1e4337e900 merging 2023-04-12 10:12:47 +02:00
MalteSchm
a893260de0 refactor state machine 2023-04-12 10:05:17 +02:00
MalteSchm
97a8545d78 merging branches to prepare mqtt enablement 2023-04-12 09:41:40 +02:00
MalteSchm
b6edc11eb2 adding option to disable power limiter via mqtt - adding missing file 2023-04-12 06:46:38 +02:00
MalteSchm
4bff31e3b1 adding option to disable power limiter via mqtt 2023-04-12 06:45:41 +02:00
MalteSchm
06f6a4da8b fixing mppt string for SmartSolar 150/48 2023-04-11 19:23:49 +02:00
MalteSchm
2d7115e1e8 typo 2023-04-11 18:58:18 +02:00
MalteSchm
f6d0b0997f typo 2023-04-11 18:57:57 +02:00
helgeerbe
9da5be7fd8 Merge branch 'development' 2023-04-11 10:22:27 +02:00
helgeerbe
71128e5a55 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-04-11 09:37:56 +02:00
MalteSchm
406332f6cd fixed linting issues, pushing to github for test run 2023-04-09 23:09:16 +02:00
MalteSchm
a1252c5701 Merge branch 'typo_fix' 2023-04-09 22:10:05 +02:00
MalteSchm
28e204fd80 typo 2023-04-09 22:06:47 +02:00
MalteSchm
86ecc62b33 fixing mppt string for SmartSolar 150/48 2023-04-09 21:58:41 +02:00
MalteSchm
e3964f8bbe typo 2023-04-09 21:55:45 +02:00
MalteSchm
869d8e6d8b fixing a bug introduced in merge 2023-04-07 20:37:26 +02:00
berni2288
19b2dd4c7a
PowerMeter: Whitespace and {} fixes 2023-04-07 20:22:35 +02:00
qubeck
00def1d8d1
Generic SML based power meters support (#146)
* add support for energy & power readings on SML based power meters, taking OBIS 16.7.1 for power (using mod. SML Parser lib. by olliiiver)

* switched SML read to use software serial

* made total power meter response controled by meter source to obtain either the sum of phase powers or explicit total power provided by meter

* made mqtt subscriptions to power meter topics meter source dependend

* simplified SML read loop and OBIS handler registration, + minor refactoring

* minor cleanup/style changes and optim. PowerMeter

* fixed build, add SOURCE_SML == 4

* removed optional usage of HW serial for SML power meter

* switched to usage of _powerMeter1Power for SML power reading to allign better with existing code

---------

Co-authored-by: helgeerbe <helge@erbehome.de>
2023-04-07 20:20:00 +02:00
MalteSchm
734d34b7a8 make calcPowerLimit respect DTU poll interval 2023-04-07 19:08:27 +02:00
MalteSchm
f84bdf7287 adding missing statement from merge 2023-04-07 19:01:00 +02:00
MalteSchm
cb6b98499a merging 2023-04-07 18:26:44 +02:00
MalteSchm
9521deea25 refactoring calcPowerLimit and setPowerLimit 2023-04-07 18:16:26 +02:00
helgeerbe
fd94a69ff8
Update Readme
add badge, which shows the original OpenDTU release number
2023-04-06 19:07:25 +02:00
helgeerbe
e4822f00ec Merge branch 'development' 2023-04-06 18:33:19 +02:00
helgeerbe
e29708f871 fix infinite state switch if battery is above
startThreshold and solar passtrhough is enabled
2023-04-06 18:32:50 +02:00
helgeerbe
724f1cf713 Merge branch 'development' 2023-04-06 13:57:57 +02:00
helgeerbe
4eec055f84 fix lint errors 2023-04-06 12:08:10 +02:00
helgeerbe
ef51d75f2c Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-04-06 10:46:02 +02:00
tbnobody
d6ff90260e Undo latest change 2023-04-06 10:37:05 +02:00
tbnobody
7f17176462 Test multpile commits 2023-04-06 10:37:05 +02:00
berni2288
0ff9ebfac9
Rebuild webapp 2023-04-05 22:28:20 +02:00
berni2288
09fb0618b4
Merge branch 'MalteSchm-webinterface_summary_updates' into development 2023-04-05 22:27:17 +02:00
helgeerbe
2a5f9776a3 add webapp 2023-04-05 11:58:11 +02:00
Thomas Basler
52deea3033 Added config for blinkyparts kit 2023-04-05 11:56:59 +02:00
Thomas Basler
ede572f6e3 Status LED's: Implemented
They can be activated using device profiles.
2023-04-05 11:56:59 +02:00
Thomas Basler
ca8c9b55ae TimeoutHelper: Allow resetting the last value 2023-04-05 11:54:50 +02:00
Thomas Basler
ca0b8e9afc Remove not required F() macro. Frees ~20kb flash. 2023-04-05 11:54:50 +02:00
Thomas Basler
dab5b4d723 webapp: Show inverter status in grey if polling is disabled (e.g. at night) 2023-04-05 11:52:35 +02:00
Thomas Basler
9eb15274a2 webapp: Update dependencies 2023-04-05 11:52:34 +02:00
Thomas Basler
e0150a8962 Increase platform package version from 6.0.1 to 6.1.0 2023-04-05 11:52:34 +02:00
Thomas Basler
f37b23b706 webapp: Update dependencies 2023-04-05 11:51:09 +02:00
Thomas Basler
53b965651d webapp: Implemented reload button for info views 2023-04-05 11:51:09 +02:00
Thomas Basler
d501c4b836 Uses VSPI instead of HSPI and compatibility for C3, S2 and S3 MCUs 2023-04-05 11:51:09 +02:00
Thomas Basler
7d50fa373e Allow use GPIO0 as NRF pins 2023-04-05 11:51:09 +02:00
Thomas Basler
f45de7ba36 Added additional compatible inverter 2023-04-05 11:51:09 +02:00
Thomas Basler
3e208219b6 Fix #753: Only apply offset if data is in the buffer to prevent negative numbers 2023-04-05 11:51:09 +02:00
Thomas Basler
edaa223856 Fixed typo in comment 2023-04-05 11:51:09 +02:00
Thomas Basler
d8a15c39cb webapp: Fix typos 2023-04-05 11:51:09 +02:00
MalteSchm
a1b63b61d7 webapp build 2023-04-05 10:51:33 +02:00
MalteSchm
801ad469c5 corrected day yield 2023-04-05 10:51:07 +02:00
Bernhard Kaszt
8ca664a8fe
Use AC power for limit calculation (= Support directly connected PV panels with Power Limiter) (#154)
* Power limiter: Use the actual AC power for limit calculation

instead of the last set limit.

In order support setups without battery connected (sources that don't exhaust the limit)
2023-04-05 10:30:03 +02:00
helgeerbe
c0dff1e7df catch bad_alloc for Huawei and Pylontech WebApi_ws 2023-04-05 09:48:38 +02:00
MalteSchm
459f9ffc2c merging branches 2023-04-04 21:43:24 +02:00
MalteSchm
2950f55879 Webinterface updates 2023-04-04 21:32:40 +02:00
MalteSchm
169ea3d5d5 Api extensions 2023-04-04 21:31:32 +02:00
MalteSchm
abffc38c11 fixing typos 2023-04-04 21:30:53 +02:00
helgeerbe
0c34554b9c don't set newPowerlimit if newLimit AND lastLimit in target window 2023-04-04 18:20:06 +02:00
helgeerbe
160b5b5b01 handle bad_alloc for vedirect status 2023-04-04 18:17:53 +02:00
qubeck
cd4a327671 limiting the artificialy increased power limit to inverter maximum power 2023-04-04 17:11:59 +02:00
berni2288
98faffc3ca
Build app.js.gz 2023-04-03 21:38:26 +02:00
Bernhard Kaszt
e35254c8f2
New Power meter support: HTTP(S) + JSON (Shelly 3EM, Tasmota, Volkszähler etc.) (#153)
* Implement HTTP(s) + JSON type Power Meter support

---------

Co-authored-by: Bernhard Kaszt <berni@bcserv.eu>
2023-04-03 21:36:20 +02:00
qubeck
bd57d0f19a fixed casting issues 2023-04-02 22:26:44 +02:00
qubeck
a1da3f9842 producing DC channel aware artificial increase of applied power limit to mitigate fixed distribution of applied power limit across all channels 2023-04-02 22:13:43 +02:00
MalteSchm
20bb7fc372
Show (Pylontech) battery infos in Live View
* adding data age to battery data

* Add battery enabled flag

* Webapi and websocket api for Battery

* Webinterface for battery

* fixed bug due to naming inconsistencies

* cleaned up rounding

* dist update

* change typename to uppercase

* reverting to original file
2023-04-02 20:58:28 +02:00
MalteSchm
90f5ed4251 reverting to original file 2023-04-02 14:43:45 +02:00
MalteSchm
4a664a7b3d change typename to uppercase 2023-04-02 14:41:50 +02:00
MalteSchm
6b85b8d4a2 Merge remote-tracking branch 'upstream/development' into battery_webinterface 2023-04-02 14:28:37 +02:00
MalteSchm
a4053dcf19 dist update 2023-04-02 14:18:37 +02:00
MalteSchm
7dee289b5b cleaned up rounding 2023-04-02 14:18:13 +02:00
MalteSchm
78838585f7 fixed bug due to naming inconsistencies 2023-04-02 14:17:23 +02:00
MalteSchm
fa5b52210a Webinterface for battery 2023-04-02 13:03:03 +02:00
MalteSchm
59c84bcb85 Webapi and websocket api for Battery 2023-04-02 13:00:46 +02:00
Bernhard Kaszt
4de043f3d4 Fix crash when AC charger is enabled but initialization fails 2023-04-02 10:46:07 +02:00
Bernhard Kaszt
c86c5133f0 Fix code style in Huawei files 2023-04-02 10:42:36 +02:00
Bernhard Kaszt
c7ef661db7 platformio.ini: Fix indentation 2023-04-02 10:14:34 +02:00
MalteSchm
a0bbf61db2 Add battery enabled flag 2023-04-02 10:11:48 +02:00
Bernhard Kaszt
ebaf5c4565
Merge pull request #151 from MalteSchm/huawei_lint_pin_mgr_fixes
Huawei lint pin mgr fixes
2023-04-02 10:04:53 +02:00
MalteSchm
e514ef744b adding data age to battery data 2023-04-01 15:34:32 +02:00
MalteSchm
d1e43c11b9 Run Huawei code only when enabled 2023-04-01 14:43:24 +02:00
MalteSchm
1f6301c2c0 Check for empty MQTT topic
Please enter the commit message for your changes. Lines starting
2023-04-01 14:02:09 +02:00
MalteSchm
f4455ccb93 reverting 2023-04-01 13:50:04 +02:00
MalteSchm
a091e80ed0 deal with un-initialized values 2023-03-29 19:07:16 +02:00
MalteSchm
a968f09d73 Pin config 2023-03-29 19:06:41 +02:00
MalteSchm
5f42f66c02 fix indent error 2023-03-29 19:05:31 +02:00
MalteSchm
ece131995a linting errors 2023-03-29 19:04:41 +02:00
helgeerbe
b3c17c8ee8 Merge branch 'pr/MalteSchm/144' into development 2023-03-27 22:16:53 +02:00
helgeerbe
6f2901b324 Merge branch 'development' 2023-03-27 21:14:40 +02:00
helgeerbe
e0a80734f3 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-03-27 21:10:03 +02:00
MalteSchm
2aad13dc72 Changing SPI interface 2023-03-27 20:47:35 +02:00
helgeerbe
7ba2058625 Merge branch 'development' 2023-03-27 10:59:51 +02:00
MalteSchm
07ea03d12b fixing linting issue 2023-03-26 15:46:33 +02:00
MalteSchm
06d5da50a2 the implementation has used HSPI with the default VSPI pins for a long time. Change VSPI and HSPI to use the right pins in order to avoid confusing 2023-03-26 12:36:21 +02:00
MalteSchm
d80f62d1b9 disable Mqtt output when disabled 2023-03-26 12:13:06 +02:00
MalteSchm
26eedc9701 align mqtt format with the other sources 2023-03-26 11:56:41 +02:00
MalteSchm
89be653a51 inverse logic for power pin (active high) 2023-03-26 11:56:11 +02:00
MalteSchm
a4767827b4 merging master 2023-03-26 11:25:08 +02:00
MalteSchm
0be574809b adding power pin 2023-03-26 11:09:08 +02:00
MalteSchm
56e52156e5 webapp dist 2023-03-26 11:07:33 +02:00
MalteSchm
0e2b7767c7 Webapp changes to display Huawei PSU values and to enable/disable the unit 2023-03-26 11:06:51 +02:00
MalteSchm
3b57550ead adding basic mqtt support 2023-03-26 11:02:40 +02:00
MalteSchm
bbf3d44d69 working with a reference 2023-03-26 11:01:32 +02:00
MalteSchm
0b5c47cd2e Adding enable/disable option and pin to control a switch/relais to power the Huawei PSU 2023-03-26 11:00:37 +02:00
helgeerbe
7d48e426dc free JsonDocument for livedata as soon as possible 2023-03-24 15:39:03 +01:00
MalteSchm
db7ad52a4d platformio ini file changes 2023-03-24 10:05:46 +01:00
MalteSchm
1aab888bf2 Documentation updates 2023-03-24 10:04:08 +01:00
MalteSchm
8576034b77 Adding Huawei CAN interface, web-api, websocket and Mqtt extensions to access the data 2023-03-24 10:03:22 +01:00
helgeerbe
3f8226c36c Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-03-23 19:14:14 +01:00
helgeerbe
0832ef86e4 reboot after power meter settings 2023-03-23 19:10:29 +01:00
helgeerbe
55dc4dbdfc reinitialize mqqt subscription after reconnect 2023-03-22 17:47:15 +01:00
helgeerbe
5d5124dd5b fix last power meter update time 2023-03-22 14:53:36 +01:00
helgeerbe
a441a6eaf7 fix mqqt supscibe in power meter 2023-03-22 14:11:55 +01:00
helgeerbe
aaa9f5cd98 power meter settings were not restored. On reboot only default were set 2023-03-22 12:34:47 +01:00
helgeerbe
cf7f815eba add webapp 2023-03-22 12:08:23 +01:00
helgeerbe
d96be997ac hide power meter settigs if disabled 2023-03-22 12:07:43 +01:00
helgeerbe
080ef03eec correct labels for power meter 2023-03-22 11:47:18 +01:00
helgeerbe
41da6d489d add missing webapi init for powermeter 2023-03-22 11:34:55 +01:00
helgeerbe
c06be1c56c add webapp 2023-03-22 10:17:13 +01:00
helgeerbe
e6963822e9
Merge pull request #140 from Adminius:development
PowerMeter Class + SDM PowerMeter support
2023-03-22 10:14:52 +01:00
Eugen
effd4e89ab
BF: get powerTotal from PowerMeter 2023-03-22 08:21:34 +01:00
Adminius
05a5b2367b PowerMeter Class + SDM PowerMeter support 2023-03-21 23:46:54 +01:00
helgeerbe
dbb548e731
Update README.md 2023-03-21 10:53:13 +01:00
helgeerbe
caeff77dae update state diagram 2023-03-21 10:50:27 +01:00
helgeerbe
a4c8e85be5 add webapp 2023-03-20 15:39:57 +01:00
helgeerbe
19252629cb minimum panel DC power 20W 2023-03-20 15:17:07 +01:00
helgeerbe
a6c852a82c if new limit too low, turn inverter off
within state
2023-03-20 12:35:29 +01:00
helgeerbe
2dc7089aa6 Start at night 2023-03-19 22:53:24 +01:00
helgeerbe
b9acbe6f2c off when newPowerLimit < lowerLimit 2023-03-18 23:50:34 +01:00
helgeerbe
06370baa0e Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-03-18 22:42:05 +01:00
helgeerbe
c95468f972 fix comparison between int and double 2023-03-18 14:44:28 +01:00
helgeerbe
46ce6ad50f Implement battery drain strategies:
- empty when full
- empty at night
2023-03-16 17:48:22 +01:00
helgeerbe
32a96bbd06 change all watt related vars to
int32_t to avoid cast problems with negativ values.
2023-03-16 12:34:14 +01:00
helgeerbe
04c7e4fa01 fix cast error with unsigned int
which results in wrong power limit settings if values become < 0 due
 to power export to the grid.
2023-03-16 12:17:14 +01:00
helgeerbe
9214897245 for watt use uint32_t as std type to
avoid cast problems to uint16_t
2023-03-13 11:38:26 +01:00
Bernhard Kaszt
e95acbec46 PylontechCanReceiver.cpp: Cosmetic changes 2023-03-12 18:47:29 +01:00
Bernhard Kaszt
c7f6aea763
Merge pull request #135 from MalteSchm/pylontech_can_bus_lib_change
swap CAN bus library to better support newer ESPs
2023-03-12 18:24:00 +01:00
MalteSchm
63c956af15 swap can library to better support newer ESPs 2023-03-12 17:27:41 +01:00
helgeerbe
6bc6796c23 add webapp 2023-03-10 16:29:51 +01:00
helgeerbe
01a2ffaed5 [Request] Show actual power limiter state in live view helgeerbe/OpenDTU-OnBattery#134 2023-03-10 16:29:00 +01:00
helgeerbe
7952becd17 New Dark Mode does not look nice for victron live view
helgeerbe/OpenDTU-OnBattery#133
2023-03-10 09:48:58 +01:00
helgeerbe
fe9959cc97
Update README.md 2023-03-09 13:05:20 +01:00
helgeerbe
a043d55b01 typos PowerLimiterStates diagram 2023-03-09 13:05:06 +01:00
helgeerbe
1b2d1afb9b add webapp 2023-03-09 12:44:31 +01:00
helgeerbe
43dc10b868 Merge branch 'inverter-settings' into development 2023-03-09 12:43:03 +01:00
helgeerbe
26dc262641 add Powerlimiter State diagram to docs 2023-03-09 12:20:18 +01:00
helgeerbe
a79f7b2026 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-03-09 10:52:46 +01:00
helgeerbe
a3f35f2491 add webapp 2023-03-09 10:49:28 +01:00
helgeerbe
4cf4594945
Merge pull request #131 from helgeerbe/hide-vedirect-livestatus-when-disabled 2023-03-09 10:34:51 +01:00
helgeerbe
87794e0793
Merge pull request #132 from helgeerbe/fix-battery-pinmapping-missing-in-ui 2023-03-09 10:31:40 +01:00
helgeerbe
8df2c16cce ensure STATE_DISCOVER if PowerLimter is enabled 2023-03-09 10:22:08 +01:00
helgeerbe
418fea2cfc fix veStruct
* PPV is int not double
* fix: initialize veStruct to 0 to avoid random data on startup
2023-03-09 10:16:55 +01:00
helgeerbe
6f3e33c0b1 exit loop after turn inverter on or off 2023-03-08 22:11:01 +01:00
Bernhard Kaszt
5b0e627f6d Fix battery pin mapping not being shown in device manager UI
(Pylontech Battery CAN Bus)
2023-03-08 20:29:08 +01:00
Bernhard Kaszt
a66f818e75 Hide Victron Ve.direct UI elements from Live page when disabled
Resolves #91
2023-03-08 19:54:19 +01:00
helgeerbe
1e6e40a3ab add Pylontech pins to d1 mini config 2023-03-07 21:27:51 +01:00
helgeerbe
83d61b1b80 mention in readme that pin
mapping is supported for Pylontech
2023-03-07 21:24:10 +01:00
helgeerbe
08cf6a523d add webapp 2023-03-07 21:18:05 +01:00
helgeerbe
6a89ae986e Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-03-07 21:17:48 +01:00
helgeerbe
6dd34a8401
Merge pull request #125 from helgeerbe/powerlimiter
Translations for powerlimiter admin; Pylontech CAN Pins in Device manager; other fixes & enhancements
2023-03-07 21:13:48 +01:00
helgeerbe
716fc867a1 powerLimiter with state machine 2023-03-07 21:04:19 +01:00
Bernhard Kaszt
44a770be0e Add Pylontech PIN numbers to platformio.ini 2023-03-05 21:22:10 +01:00
Bernhard Kaszt
06a0f76fed Fix local build working but Github build failing 2023-03-05 20:45:27 +01:00
Bernhard Kaszt
06fbdf1f12 Add Pylontech battery to device pin manager 2023-03-05 19:55:56 +01:00
Bernhard Kaszt
43436e19b7 Translate all remaning Powerlimiter settings 2023-03-05 19:17:30 +01:00
Bernhard Kaszt
304d90062d Revert broken change in condition that sets _consumeSolarPowerOnly
6709338dbd
2023-03-05 16:30:53 +01:00
Bernhard Kaszt
a6e720f154 Powerlimiter: Remove MQTT Topic debug message 2023-03-05 15:41:21 +01:00
Bernhard Kaszt
804f225908 Fix _consumeSolarPowerOnly sometimes being set to false when it shouldn't 2023-03-05 13:32:18 +01:00
helgeerbe
b70407d7fe Initilise variables 2023-03-04 13:02:30 +01:00
helgeerbe
3ffc1c947d Merge branch 'inverter-settings' into development 2023-03-04 12:44:19 +01:00
helgeerbe
9391b11403 fix start inverter 2023-03-04 11:59:10 +01:00
helgeerbe
b7dda83545 use efficiency factor if limit is set to
victronChargePower
2023-03-03 20:34:11 +01:00
helgeerbe
cf4a35e148 fix starting and stopping inverter 2023-03-03 19:51:04 +01:00
helgeerbe
9f2d79f2dc Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-03-03 10:53:43 +01:00
helgeerbe
6709338dbd fix setting consumeSolarPowerOnly 2023-03-02 16:18:44 +01:00
helgeerbe
67a5217482 start inverter if power consumption greater or equal lower power limit 2023-03-02 14:27:42 +01:00
helgeerbe
260f4ccdb8
Update README.md 2023-03-01 15:28:54 +01:00
helgeerbe
c36e4cc3da
Update README.md 2023-03-01 15:23:25 +01:00
helgeerbe
028b43f4d1
Update README.md 2023-03-01 15:14:07 +01:00
helgeerbe
c0735c5b1f
Update README.md 2023-03-01 14:46:06 +01:00
helgeerbe
3e38941b57 start inverter only if not exporting to grid 2023-03-01 13:34:27 +01:00
helgeerbe
6241a31e8c comment out full wifi scan 2023-03-01 12:09:53 +01:00
helgeerbe
4a51ae5038 victron charge power is integer 2023-03-01 10:23:37 +01:00
helgeerbe
26a8809121 stop inverter, if exporting power to grid 2023-02-28 13:09:10 +01:00
helgeerbe
4a49f48969 update webapp 2023-02-27 20:12:31 +01:00
helgeerbe
4feaa1db98 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-02-27 20:11:06 +01:00
helgeerbe
3b62d5708a Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-02-27 07:42:46 +01:00
helgeerbe
d146adcc3b add webapp 2023-02-25 22:33:29 +01:00
helgeerbe
e9fce49fee fix lint error 2023-02-25 22:23:05 +01:00
helgeerbe
32342dcd5d fix voltage threshhold to 2 decimal digits 2023-02-25 22:12:44 +01:00
helgeerbe
17dd9db946 set min target power consumption to 0 2023-02-25 20:17:25 +01:00
helgeerbe
3ed61319ad fix config read for power consumption 2023-02-25 18:58:41 +01:00
helgeerbe
43815f6711 power limiter make fields mandtory 2023-02-25 17:25:02 +01:00
helgeerbe
c2ba4a334e build webapp 2023-02-25 16:29:25 +01:00
helgeerbe
1e968a1713 Add target for power consumption 2023-02-25 16:28:21 +01:00
helgeerbe
3f3540bd33 print ve.direct rx, tx pins on init 2023-02-25 15:29:00 +01:00
helgeerbe
28b24d01ad implement UI for target power consumption 2023-02-24 16:13:32 +01:00
helgeerbe
cd7ece7caf fix hard coded channel settings 2023-02-24 10:45:59 +01:00
helgeerbe
10f907477d fix name for inverter id and channel id 2023-02-23 22:41:37 +01:00
helgeerbe
56151b0d12 make inverter and channel selectable 2023-02-23 21:46:59 +01:00
helgeerbe
73fe3bfb96 Add missing can lib 2023-02-23 12:05:50 +01:00
helgeerbe
a2473645a5 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-02-23 12:01:34 +01:00
helgeerbe
1caeb1d88b fix logging for out of ressources 2023-02-23 09:46:24 +01:00
helgeerbe
a758d894f6 LiveService if out of ressources close websocket 2023-02-22 15:46:52 +01:00
helgeerbe
0f80a25937 send http 429 "Too Many Requets",
if prometheus api is out of resources
2023-02-22 14:13:17 +01:00
helgeerbe
9e1df83a87 remove map.h include 2023-02-21 22:12:15 +01:00
helgeerbe
1e7f6b8f0f first version 2023-02-21 22:06:47 +01:00
helgeerbe
30440472f7 fix lint errors 2023-02-20 21:04:43 +01:00
helgeerbe
f973de4ab6 aktivate prometheus interface 2023-02-20 21:00:07 +01:00
helgeerbe
f560f25302 initial merge of power_limiter * missing is inverter and channel setting in gui
* due to bug _webApiPrometheus.init is commented out
2023-02-20 15:56:02 +01:00
helgeerbe
09942e8e18 Connect Wifi to strongest AP
* do full channel scan
  * connect to strongest AP
2023-02-20 09:52:40 +01:00
helgeerbe
cb7874ac8d remove serial ports from platformio
* use auto detect
2023-02-20 09:43:31 +01:00
helgeerbe
ce752f0d75 Dont print that VeDirect data is unvalid 2023-02-17 15:08:31 +01:00
helgeerbe
6e0ae6d152 Extent JSON_BUFFER_SIZE 7000 2023-02-17 15:06:43 +01:00
helgeerbe
27f20a76f0 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-02-16 12:35:18 +01:00
helgeerbe
1a4a8cc921 fix age critical 2023-02-16 12:24:11 +01:00
helgeerbe
dc44cc8e1f create VeDirect.isDataValid() 2023-02-16 12:17:47 +01:00
helgeerbe
0ac529146e VedirectView becomes component
* move Vedirect in the component folder
* VedirectView is now component of BasePage
* /vedirectlivedata connectable through vue proxy
2023-02-15 15:01:59 +01:00
helgeerbe
a529c91254 Merge remote-tracking branch 'tbnobody/OpenDTU/master' into development 2023-02-14 10:10:10 +01:00
helgeerbe
7022ed95b6 copy vedirect data if send only
updated data is true
2023-02-14 09:54:40 +01:00
helgeerbe
43bfee4d55 Stopp publishing vedirect if data becomes too old 2023-02-13 14:01:10 +01:00
helgeerbe
957bc91828 Force hass discovery update for vedirect
if mqtt settings changes
2023-02-13 12:00:25 +01:00
helgeerbe
17e564a094 simplify main for vedirect 2023-02-13 11:41:51 +01:00
helgeerbe
f89ccdd2ee
Update README.md 2023-02-08 16:27:44 +01:00
helgeerbe
e5fa0050cd
Update README.md 2023-02-08 16:06:13 +01:00
helgeerbe
ed12f814dd mqqt hass discovery complete 2023-02-08 14:09:16 +01:00
helgeerbe
997023e52f Add Hass sensors Battery voltage and Current 2023-02-07 16:51:30 +01:00
helgeerbe
4f0a45c902 Add serial number to deviced name 2023-02-07 13:59:03 +01:00
helgeerbe
82ecf6cd6d Add victron serial to mqtt topics 2023-02-07 13:36:37 +01:00
helgeerbe
37d3bb0eb0 vedirect hass autodiscovery:
load output state as binary sensor
2023-02-06 17:35:01 +01:00
helgeerbe
1c01e927f9 Add victron pins to device manager 2023-02-02 23:01:22 +01:00
helgeerbe
9e79f02787 a 2023-02-02 21:00:26 +01:00
helgeerbe
9e70d2dfc6 Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2023-02-02 20:58:06 +01:00
helgeerbe
bec6c20531 Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2023-01-26 10:30:55 +01:00
helgeerbe
9a7a0d293e Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2023-01-20 14:54:04 +01:00
helgeerbe
ddb6346087 Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2023-01-03 10:51:56 +01:00
helgeerbe
035251c04c Ui text for vedirect 2022-12-28 14:26:07 +01:00
helgeerbe
e49bbe0faf Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2022-12-27 14:17:24 +01:00
helgeerbe
df5cde2e82 Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2022-12-05 11:46:31 +01:00
helgeerbe
a213082c4d add webapp 2022-11-26 17:49:54 +01:00
helgeerbe
f740dceb78 removed unused isLoggedIn function 2022-11-26 17:49:00 +01:00
helgeerbe
0a0cb9905e live view - change order of mppt columns 2022-11-26 17:40:14 +01:00
helgeerbe
ae414c42c1 merge with remote master 2022-11-26 16:31:41 +01:00
helgeerbe
2109520bde Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2022-11-26 16:31:16 +01:00
helgeerbe
1fc0e76c41 Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2022-11-22 17:10:53 +01:00
helgeerbe
2c6dff3714 Password protection for vedirect settings API 2022-11-19 14:34:59 +01:00
helgeerbe
f35395e76f Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2022-11-16 16:39:01 +01:00
helgeerbe
407e0ee8c1 vedirect vue code follows master 2022-11-03 19:07:57 +01:00
helgeerbe
b3295f5f33 Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2022-11-03 09:27:56 +01:00
helgeerbe
6b4129c400 Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2022-10-31 14:37:44 +01:00
helgeerbe
a6d734018a Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2022-10-26 14:20:44 +02:00
helgeerbe
61fd54b026 png of component diagram 2022-10-26 13:09:40 +02:00
helgeerbe
38e159d5e6 add missing icons to component diagram 2022-10-26 12:56:20 +02:00
helgeerbe
cbc64936e6 components as png 2022-10-26 12:41:26 +02:00
helgeerbe
8a5d027374 add component diagram 2022-10-26 12:34:04 +02:00
helgeerbe
596f1f1183 webapp 2022-10-20 23:39:17 +02:00
helgeerbe
a9336968c7 Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2022-10-20 23:39:05 +02:00
helgeerbe
48ef4c6c04 vedirect live view without button 2022-10-20 23:33:01 +02:00
helgeerbe
cd219e4fa8 vedirect with button 2022-10-20 23:10:07 +02:00
helgeerbe
3617e9a260 add vedirect to config import / export 2022-10-20 16:59:23 +02:00
helgeerbe
732ab3e5c6 remove button in live view ve.direct 2022-10-20 16:39:59 +02:00
helgeerbe
c3df1e7328 Changes to keep ve.direct working 2022-10-20 13:35:14 +02:00
helgeerbe
905dc359a5 Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2022-10-20 13:34:31 +02:00
helgeerbe
d5740e4851 clear tmp ve.direct buffer on checksum error 2022-10-17 14:58:33 +02:00
helgeerbe
9f2bbe3c03 webapp 2022-10-17 10:10:53 +02:00
helgeerbe
5de35ee353 Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2022-10-17 10:10:34 +02:00
helgeerbe
c8b9288f1c webapp 2022-10-10 11:08:50 +02:00
helgeerbe
3febc28c78 Merge remote-tracking branch
'tbnobody/OpenDTU/master'
2022-10-10 11:08:36 +02:00
helgeerbe
366e7dc409 Merge remote-tracking branch
'tbnobody/OpenDTU/master'
2022-10-07 10:32:51 +02:00
helgeerbe
25285b10ee fix cpplint errors 2022-10-04 12:00:29 +02:00
helgeerbe
ba3183e10b Clear map for recieved key, value pairs 2022-10-04 11:34:30 +02:00
helgeerbe
9c71652b2c weapp 2022-10-03 13:34:31 +02:00
helgeerbe
fb638cba43 type 2022-10-03 13:34:16 +02:00
helgeerbe
df7c821bd4 Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2022-10-03 13:34:05 +02:00
helgeerbe
6587282ac2 Fix typo in victron error message 2022-09-30 10:40:41 +02:00
helgeerbe
4b02426ab2 Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2022-09-26 09:38:14 +02:00
helgeerbe
3843a46de9 Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2022-09-22 09:36:36 +02:00
helgeerbe
e6b2be6fdf Change WebSocketMessageBuffer to String 2022-09-19 10:58:56 +02:00
helgeerbe
bee600bfd8 Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2022-09-19 10:49:41 +02:00
helgeerbe
5a25ce50db Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2022-09-06 10:14:18 +02:00
helgeerbe
41eb94e5d9 Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2022-09-01 10:09:50 +02:00
helgeerbe
f8e7db1ba7
Update README.md 2022-08-29 16:32:22 +02:00
helgeerbe
b21990e1d1
Update README.md 2022-08-29 16:29:30 +02:00
helgeerbe
12fc34d4de add victrons rx, tx pins 2022-08-29 16:19:47 +02:00
helgeerbe
723fc96ec5 Merge remote-tracking branch 'tbnobbuody/OpenDTU/master' 2022-08-29 15:55:52 +02:00
helgeerbe
3e4e1bcea4 Merge remote-tracking branch 'tbnobody/OpenDTU/master' 2022-08-24 15:59:21 +02:00
helgeerbe
ece592287c Add ve.direct live view 2022-08-23 22:55:47 +02:00
helgeerbe
63ddbed359 ve.direct as jason array 2022-08-23 14:42:31 +02:00
helgeerbe
044931c08e Add LOAD to rest api 2022-08-20 17:18:07 +02:00
helgeerbe
48e5b567cb frame handler with string and map 2022-08-20 17:06:56 +02:00
helgeerbe
7140574c37 sk 2022-08-19 15:53:18 +02:00
helgeerbe
b8ffa37e97 end poll latest at 500ms 2022-08-17 20:25:46 +02:00
helgeerbe
17abb57ed6 Stop polling after one successful frame read 2022-08-17 18:39:22 +02:00
helgeerbe
f78561cef7 remove # from serial 2022-08-17 16:25:10 +02:00
helgeerbe
82c5fbcf46 fix order in json 2022-08-17 13:41:19 +02:00
helgeerbe
0d08c6a136 Check poll intervall 2022-08-17 13:40:38 +02:00
helgeerbe
6bdc1c5d3f update weapp after merger from master 2022-08-17 12:27:41 +02:00
helgeerbe
865c9cdac5 Merge branch 'master' into ve.direct 2022-08-17 12:25:07 +02:00
helgeerbe
25094ae5b6 last update is now set after poll full frame 2022-08-17 12:15:12 +02:00
helgeerbe
d9bf0ab2e9 ve.direct:
- add poll rate
- add data_age and age_critical to rest api
2022-08-16 16:26:08 +02:00
helgeerbe
72c0e8579a ve.direct rest api 2022-08-16 14:02:19 +02:00
helgeerbe
2ff8f84387 loop für ve.direct 2022-08-15 10:56:37 +02:00
helgeerbe
70136e20aa yield 100ms 2022-08-11 20:40:52 +02:00
helgeerbe
47dda553d9 vue changes 2022-08-11 17:33:38 +02:00
helgeerbe
0eeafe69d9 Merge branch 've.direct' of
https://github.com/helgeerbe/OpenDTU into ve.direct
2022-08-11 17:31:58 +02:00
helgeerbe
e43a45b979 Timeout for HardwareSerial 500ms 2022-08-11 17:26:00 +02:00
helgeerbe
c889efef7c vue changes 2022-08-11 17:26:00 +02:00
helgeerbe
c7c1506e42 First version 2022-08-11 17:26:00 +02:00
helgeerbe
9142dd9930 vue changes 2022-08-11 10:41:43 +02:00
helgeerbe
6a802d607c Merge branch 've.direct' of https://github.com/helgeerbe/OpenDTU into ve.direct 2022-08-11 10:38:41 +02:00
helgeerbe
5aa27a8dc8 Timeout for HardwareSerial 500ms 2022-08-11 10:35:20 +02:00
helgeerbe
a4752e1379 vue changes 2022-08-11 10:35:20 +02:00
helgeerbe
15c1717290 First version 2022-08-11 10:34:41 +02:00
helgeerbe
0793856259 Timeout for HardwareSerial 500ms 2022-08-11 09:42:36 +02:00
helgeerbe
6ca3fb61ce vue changes 2022-08-08 16:11:36 +02:00
helgeerbe
0e4edc9571 First version 2022-08-08 11:16:06 +02:00
165 changed files with 17785 additions and 1054 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -1,10 +1,15 @@
name: OpenDTU Build name: OpenDTU-onBattery Build
on: on:
push: push:
paths-ignore: paths-ignore:
- docs/** - docs/**
- '**/*.md' - '**/*.md'
branches:
- master
- development
tags-ignore:
- 'v**'
pull_request: pull_request:
paths-ignore: paths-ignore:
- docs/** - docs/**
@ -55,6 +60,15 @@ jobs:
- name: Get tags - name: Get tags
run: git fetch --force --tags origin run: git fetch --force --tags origin
- name: Create and switch to a meaningful branch for pull-requests
if: github.event_name == 'pull_request'
run: |
OWNER=${{ github.repository_owner }}
NAME=${{ github.event.repository.name }}
ID=${{ github.event.pull_request.number }}
DATE=$(date +'%Y%m%d%H%M')
git switch -c ${OWNER}/${NAME}/pr${ID}-${DATE}
- name: Cache pip - name: Cache pip
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@ -96,26 +110,43 @@ jobs:
run: pio run -e ${{ matrix.environment }} run: pio run -e ${{ matrix.environment }}
- name: Rename Firmware - name: Rename Firmware
run: mv .pio/build/${{ matrix.environment }}/firmware.bin .pio/build/${{ matrix.environment }}/opendtu-${{ matrix.environment }}.bin run: mv .pio/build/${{ matrix.environment }}/firmware.bin .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.bin
- name: Rename Factory Firmware - name: Rename Factory Firmware
run: mv .pio/build/${{ matrix.environment }}/firmware.factory.bin .pio/build/${{ matrix.environment }}/opendtu-${{ matrix.environment }}.factory.bin run: mv .pio/build/${{ matrix.environment }}/firmware.factory.bin .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.factory.bin
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: opendtu-${{ matrix.environment }} name: opendtu-onbattery-${{ matrix.environment }}
path: | path: |
.pio/build/${{ matrix.environment }}/opendtu-${{ matrix.environment }}.bin .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.bin
.pio/build/${{ matrix.environment }}/opendtu-${{ matrix.environment }}.factory.bin .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.factory.bin
release: release:
name: Create Release name: Create Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [get_default_envs, build] needs: [get_default_envs, build]
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/2')
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Get tags
run: git fetch --force --tags origin
- name: Get openDTU core release
run: |
echo "OPEN_DTU_CORE_RELEASE=$(git for-each-ref --sort=creatordate --format '%(refname) %(creatordate)' refs/tags | grep 'refs/tags/v' | tail -1 | sed 's#.*/##' | sed 's/ .*//')" >> $GITHUB_ENV
- name: Create openDTU-core-release-Badge
uses: schneegans/dynamic-badges-action@v1.6.0
with:
auth: ${{ secrets.GIST_SECRET }}
gistID: 68b47cc8c8994d04ab3a4fa9d8aee5e6
filename: openDTUcoreRelease.json
label: based on original OpenDTU
message: ${{ env.OPEN_DTU_CORE_RELEASE }}
color: lightblue
- name: Build Changelog - name: Build Changelog
id: github_release id: github_release
@ -135,7 +166,7 @@ jobs:
run: | run: |
ls -R ls -R
cd artifacts cd artifacts
for i in */; do cp ${i}opendtu-*.bin ./; done for i in */; do cp ${i}opendtu-onbattery-*.bin ./; done
- name: Create release - name: Create release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1

View File

@ -30,7 +30,7 @@
} }
], ],
"template": "${{CHANGELOG}}", "template": "${{CHANGELOG}}",
"pr_template": "- [${{TITLE}}](https://github.com/tbnobody/OpenDTU/commit/${{MERGE_SHA}})", "pr_template": "- [${{TITLE}}](https://github.com/helgeerbe/OpenDTU-OnBattery/commit/${{MERGE_SHA}})",
"empty_template": "- no changes", "empty_template": "- no changes",
"label_extractor": [ "label_extractor": [
{ {

103
.github/workflows/test_build.yml vendored Normal file
View File

@ -0,0 +1,103 @@
name: OpenDTU-onBattery Test Build
on: workflow_dispatch
jobs:
get_default_envs:
name: Gather Environments
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Cache pip
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Install PlatformIO
run: |
python -m pip install --upgrade pip
pip install --upgrade platformio
- name: Get default environments
id: envs
run: |
echo "environments=$(pio project config --json-output | jq -cr '.[1][1][0][1]|split(",")')" >> $GITHUB_OUTPUT
outputs:
environments: ${{ steps.envs.outputs.environments }}
build:
name: Build Enviornments
runs-on: ubuntu-latest
needs: get_default_envs
strategy:
matrix:
environment: ${{ fromJSON(needs.get_default_envs.outputs.environments) }}
steps:
- uses: actions/checkout@v3
- name: Get tags
run: git fetch --force --tags origin
- name: Cache pip
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Cache PlatformIO
uses: actions/cache@v3
with:
path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Install PlatformIO
run: |
python -m pip install --upgrade pip
pip install --upgrade platformio
- name: Setup Node.js and yarn
uses: actions/setup-node@v3
with:
node-version: "18"
cache: "yarn"
cache-dependency-path: "webapp/yarn.lock"
- name: Install WebApp dependencies
run: yarn --cwd webapp install --frozen-lockfile
- name: Build WebApp
run: yarn --cwd webapp build
- name: Build firmware
run: pio run -e ${{ matrix.environment }}
- name: Rename Firmware
run: mv .pio/build/${{ matrix.environment }}/firmware.bin .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.bin
- name: Rename Factory Firmware
run: mv .pio/build/${{ matrix.environment }}/firmware.factory.bin .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.factory.bin
- uses: actions/upload-artifact@v3
with:
name: opendtu-onbattery-${{ matrix.environment }}
path: |
.pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.bin
.pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.factory.bin

62
.vscode/settings.json vendored
View File

@ -1,3 +1,63 @@
{ {
"C_Cpp.clang_format_style": "WebKit" "C_Cpp.clang_format_style": "WebKit",
"files.associations": {
"*.tcc": "cpp",
"algorithm": "cpp",
"array": "cpp",
"atomic": "cpp",
"bitset": "cpp",
"cctype": "cpp",
"chrono": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"condition_variable": "cpp",
"cstdarg": "cpp",
"cstddef": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"cwctype": "cpp",
"deque": "cpp",
"list": "cpp",
"unordered_map": "cpp",
"unordered_set": "cpp",
"vector": "cpp",
"exception": "cpp",
"functional": "cpp",
"iterator": "cpp",
"map": "cpp",
"memory": "cpp",
"memory_resource": "cpp",
"numeric": "cpp",
"optional": "cpp",
"random": "cpp",
"ratio": "cpp",
"regex": "cpp",
"string": "cpp",
"string_view": "cpp",
"system_error": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"utility": "cpp",
"fstream": "cpp",
"initializer_list": "cpp",
"iomanip": "cpp",
"iosfwd": "cpp",
"iostream": "cpp",
"istream": "cpp",
"limits": "cpp",
"mutex": "cpp",
"new": "cpp",
"ostream": "cpp",
"sstream": "cpp",
"stdexcept": "cpp",
"streambuf": "cpp",
"thread": "cpp",
"cinttypes": "cpp",
"typeinfo": "cpp",
"variant": "cpp"
}
} }

110
README.md
View File

@ -1,80 +1,56 @@
# OpenDTU - [OpenDTU-onBattery](#opendtu-onbattery)
- [What is OpenDTU-onBattery](#what-is-opendtu-onbattery)
- [History of the project](#history-of-the-project)
- [Highlights of OpenDTU-onBattery](#highlights-of-opendtu-onbattery)
- [Documentation](#documentation)
- [Acknowledgment](#acknowledgment)
[![OpenDTU Build](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml/badge.svg)](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml) # OpenDTU-onBattery
[![cpplint](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml/badge.svg)](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml)
[![Yarn Linting](https://github.com/tbnobody/OpenDTU/actions/workflows/yarnlint.yml/badge.svg)](https://github.com/tbnobody/OpenDTU/actions/workflows/yarnlint.yml)
## !! IMPORTANT UPGRADE NOTES !! This is a fork from the Hoymiles project [OpenDTU](https://github.com/tbnobody/OpenDTU).
If you are upgrading from a version before 15.03.2023 you have to upgrade the partition table of the ESP32. Please follow the [this](docs/UpgradePartition.md) documentation! ![GitHub tag (latest SemVer)](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/helgeerbe/68b47cc8c8994d04ab3a4fa9d8aee5e6/raw/openDTUcoreRelease.json)
## Background [![OpenDTU-onBattery Build](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/build.yml/badge.svg)](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/build.yml)
[![cpplint](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/cpplint.yml/badge.svg)](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/cpplint.yml)
[![Yarn Linting](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/yarnlint.yml/badge.svg)](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/yarnlint.yml)
This project was started from [this](https://www.mikrocontroller.net/topic/525778) discussion (Mikrocontroller.net). ## What is OpenDTU-onBattery
It was the goal to replace the original Hoymiles DTU (Telemetry Gateway) with their cloud access. With a lot of reverse engineering the Hoymiles protocol was decrypted and analyzed.
OpenDTU-onBattery is an extension of the original OpenDTU to support battery chargers, battery management systems (BMS) and power meters on a single esp32. With the help of a dynamic power limiter, the power production can be adjusted to the actual consumption. In this way, it is possible to come as close as possible to the goal of zero feed-in.
## History of the project
The original OpenDTU project was started from [this](https://www.mikrocontroller.net/topic/525778) discussion (Mikrocontroller.net). It was the goal to replace the original Hoymiles DTU (Telemetry Gateway) with their cloud access. With a lot of reverse engineering the Hoymiles protocol was decrypted and analyzed.
Summer 2022 I bought my Victron MPPT battery charger, and didn't like the idea to set up a separate esp32 to recieve the charger data. I decided to fork OpenDTU and extend it with battery charger support and a dynamic power limitter to my own needs. Hoping someone can make use of it.
## Highlights of OpenDTU-onBattery
This project is still under development and adds following features:
* Support Victron's Ve.Direct protocol on the same chip (cable based serial interface!). Additional information about Ve.direct can be downloaded directly from [Victron's website](https://www.victronenergy.com/support-and-downloads/technical-information).
* Dynamically sets the Hoymiles power limited according to the currently used energy in the household. Needs an HTTP JSON based power meter (e.g. Tasmota), an MQTT based power meter like Shelly 3EM or an SDM power meter.
* Battery support: Read the voltage from Victron MPPT charge controller or from the Hoymiles DC inputs and starts/stops the power producing based on configurable voltage thresholds
* Voltage correction that takes the voltage drop because of the current output load into account (not 100% reliable calculation)
* Can read the current solar panel power from the Victron MPPT and adjust the limiter accordingly to not save energy in the battery (for increased system efficiency). Increases the battery lifespan and reduces energy loses.
* Settings can be configured in the UI
* Pylontech Battery support (via CAN bus interface). Use the SOC for starting/stopping the power output and provide the battery data via MQTT (autodiscovery for home assistant is currently not supported). Pin Mapping is supported (default RX PIN 27, TX PIN 26). Actual no live view support for Pylontech Battery.
* Huawei R4850G2 power supply unit that can act as AC charger. Supports status shown on the web interface and options to set voltage and current limits on the web interface and via MQTT. Connection is done using CAN bus (needs to be separate from Pylontech CAN bus) via SN65HVD230 interface.
## Documentation ## Documentation
The documentation can be found [here](https://tbnobody.github.io/OpenDTU-docs/). [Full documentation of OpenDTU-onBattery extensions can be found at the project's wiki](https://github.com/helgeerbe/OpenDTU-OnBattery/wiki).
Please feel free to support and create a PR in [this](https://github.com/tbnobody/OpenDTU-docs) repository to make the documentation even better.
## Breaking changes For documentation of openDTU core functionality I refer to the original [repo](https://github.com/tbnobody/OpenDTU) and its [documentation](https://tbnobody.github.io/OpenDTU-docs/).
Generated using: `git log --date=short --pretty=format:"* %h%x09%ad%x09%s" | grep BREAKING` Please note that openDTU-onBattery may change significantly during its development.
Bug reports, comments, feature requests and fixes are most welcome!
```code To find out what's new or improved have a look at the [changelog](https://github.com/helgeerbe/OpenDTU-OnBattery/releases).
* 1b637f08 2024-01-30 BREAKING CHANGE: Web API Endpoint /api/livedata/status and /api/prometheus/metrics
* e1564780 2024-01-30 BREAKING CHANGE: Web API Endpoint /api/livedata/status and /api/prometheus/metrics
* f0b5542c 2024-01-30 BREAKING CHANGE: Web API Endpoint /api/livedata/status and /api/prometheus/metrics
* c27ecc36 2024-01-29 BREAKING CHANGE: Web API Endpoint /api/livedata/status
* 71d1b3b 2023-11-07 BREAKING CHANGE: Home Assistant Auto Discovery to new naming scheme
* 04f62e0 2023-04-20 BREAKING CHANGE: Web API Endpoint /api/eventlog/status no nested serial object
* 59f43a8 2023-04-17 BREAKING CHANGE: Web API Endpoint /api/devinfo/status requires GET parameter inv=
* 318136d 2023-03-15 BREAKING CHANGE: Updated partition table: Make sure you have a configuration backup and completly reflash the device!
* 3b7aef6 2023-02-13 BREAKING CHANGE: Web API!
* d4c838a 2023-02-06 BREAKING CHANGE: Prometheus API!
* daf847e 2022-11-14 BREAKING CHANGE: Removed deprecated config parsing method
* 69b675b 2022-11-01 BREAKING CHANGE: Structure WebAPI /api/livedata/status changed
* 27ed4e3 2022-10-31 BREAKING: Change power factor from percent value to value between 0 and 1
```
## Currently supported Inverters ## Acknowledgment
| Model | Required RF Module | DC Inputs | MPP-Tracker | AC Phases | A special Thank to Thomas Basler (tbnobody) the author of the original [OpenDTU](https://github.com/tbnobody/OpenDTU) project. You are doing a great job!
| ---------------------| ------------------ | --------- | ----------- | --------- |
| Hoymiles HM-300-1T | NRF24L01+ | 1 | 1 | 1 | Last but not least, I would like to thank all the contributors. With your ideas and enhancements, you have made OpenDTU-onBattery much more than I originally had in mind.
| Hoymiles HM-350-1T | NRF24L01+ | 1 | 1 | 1 |
| Hoymiles HM-400-1T | NRF24L01+ | 1 | 1 | 1 |
| Hoymiles HM-600-2T | NRF24L01+ | 2 | 2 | 1 |
| Hoymiles HM-700-2T | NRF24L01+ | 2 | 2 | 1 |
| Hoymiles HM-800-2T | NRF24L01+ | 2 | 2 | 1 |
| Hoymiles HM-1000-4T | NRF24L01+ | 4 | 2 | 1 |
| Hoymiles HM-1200-4T | NRF24L01+ | 4 | 2 | 1 |
| Hoymiles HM-1500-4T | NRF24L01+ | 4 | 2 | 1 |
| Hoymiles HMS-300-1T | CMT2300A | 1 | 1 | 1 |
| Hoymiles HMS-350-1T | CMT2300A | 1 | 1 | 1 |
| Hoymiles HMS-400-1T | CMT2300A | 1 | 1 | 1 |
| Hoymiles HMS-450-1T | CMT2300A | 1 | 1 | 1 |
| Hoymiles HMS-500-1T | CMT2300A | 1 | 1 | 1 |
| Hoymiles HMS-600-2T | CMT2300A | 2 | 2 | 1 |
| Hoymiles HMS-700-2T | CMT2300A | 2 | 2 | 1 |
| Hoymiles HMS-800-2T | CMT2300A | 2 | 2 | 1 |
| Hoymiles HMS-900-2T | CMT2300A | 2 | 2 | 1 |
| Hoymiles HMS-1000-2T | CMT2300A | 2 | 2 | 1 |
| Hoymiles HMS-1600-4T | CMT2300A | 4 | 4 | 1 |
| Hoymiles HMS-1800-4T | CMT2300A | 4 | 4 | 1 |
| Hoymiles HMS-2000-4T | CMT2300A | 4 | 4 | 1 |
| Hoymiles HMT-1600-4T | CMT2300A | 4 | 2 | 3 |
| Hoymiles HMT-1800-4T | CMT2300A | 4 | 2 | 3 |
| Hoymiles HMT-2000-4T | CMT2300A | 4 | 2 | 3 |
| Hoymiles HMT-1800-6T | CMT2300A | 6 | 3 | 3 |
| Hoymiles HMT-2250-6T | CMT2300A | 6 | 3 | 3 |
| Solenso SOL-H350 | NRF24L01+ | 1 | 1 | 1 |
| Solenso SOL-H400 | NRF24L01+ | 1 | 1 | 1 |
| Solenso SOL-H800 | NRF24L01+ | 2 | 2 | 1 |
| TSUN TSOL-M350 | NRF24L01+ | 1 | 1 | 1 |
| TSUN TSOL-M800 | NRF24L01+ | 2 | 2 | 1 |
| TSUN TSOL-M1600 | NRF24L01+ | 4 | 2 | 1 |
| E-Star HERF-800 | NRF24L01+ | 2 | 2 | 1 |
| E-Star HERF-1600 | NRF24L01+ | 4 | 2 | 1 |
| E-Star HERF-1800 | NRF24L01+ | 4 | 2 | 1 |

186
README_onBattery.md Normal file
View File

@ -0,0 +1,186 @@
# OpenDTU-OnBattery
This is a fork from the Hoymiles project [OpenDTU](https://github.com/tbnobody/OpenDTU). This project is still under development but is being used on a day to day basis as well.
![GitHub tag (latest SemVer)](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/helgeerbe/68b47cc8c8994d04ab3a4fa9d8aee5e6/raw/openDTUcoreRelease.json)
> **Warning**
>
> In contrast to the original openDTU, with release 2023.05.23.post1 openDTU-onBattery supports only 5 inverters. Otherwise, there is not enough memory for the liveData view.
## Features
* Hoymiles inverter support for live data and display of various inverter internal information. (Partial) support for multiple inverters.
* MQTT support (with TLS) with partial Home Assistant MQTT Auto Discovery
* Automatic inverter power control of a selected Hoymiles inverter to compensate the currently used energy in the household.
* Energy meter support with interface options to HTTP JSON based power meters (e.g. Tasmota), MQTT based power meters (e.g. Shelly 3EM) or SDM power meters.
* Support for Victron MPPT charge controller using Ve.Direct. cf. Ve.direct: https://www.victronenergy.com/support-and-downloads/technical-information.
* Generic voltage based battery support using Victron MPPT charge controller or Hoymiles inverter voltage values to start / stop inverter power output. (with load compensation)
* Pylontech battery support via CAN bus interface. State of Charge reported by BMS is used to start / stop inverter power output. Battery data is exported via MQTT (no support for home assistant auto discovery).
* Support for Huawei R4850G2 power supply unit that can act as AC charging source. [Overview](https://www.beyondlogic.org/review-huawei-r4850g2-power-supply-53-5vdc-3kw/)
* Other features from [OpenDTU](https://github.com/tbnobody/OpenDTU) maintained
## Hardware
To get started with this project you will need to assemble a few hardware components that allow interfacing with the desired devices. What is needed depends on the use-case but may consist of:
* ESP32 board that contains the CPU and WIFI connectivity
* NRF24L01+ or CMT2300A radio board to interface with the inverter. Please check the list of the supported inverters below for the board needed.
* 3.3V / 5V logic level shifter to interface with the Victron MPPT charge controller
* SN65HVD230 CAN bus transceiver to interface with a Pylontech battery
* MCP2515 SPI / CAN bus transceiver to interface with the Huawei AC PSU
* Relais board + 3.3V / 5 V logic level shifter to switch the slot detect on the Huawei AC PSU
* Display [Display](docs/Display.md)
More detailed information on the hardware can be found in the [Hardware and flashing](docs/hardware_flash.md) document.
### Currently supported Inverters
| Model | Required RF Module | DC Inputs | MPP-Tracker | AC Phases |
| --------------------| ------------------ | --------- | ----------- | --------- |
| Hoymiles HM-300 | NRF24L01+ | 1 | 1 | 1 |
| Hoymiles HM-350 | NRF24L01+ | 1 | 1 | 1 |
| Hoymiles HM-400 | NRF24L01+ | 1 | 1 | 1 |
| Hoymiles HM-600 | NRF24L01+ | 2 | 2 | 1 |
| Hoymiles HM-700 | NRF24L01+ | 2 | 2 | 1 |
| Hoymiles HM-800 | NRF24L01+ | 2 | 2 | 1 |
| Hoymiles HM-1000 | NRF24L01+ | 4 | 2 | 1 |
| Hoymiles HM-1200 | NRF24L01+ | 4 | 2 | 1 |
| Hoymiles HM-1500 | NRF24L01+ | 4 | 2 | 1 |
| Hoymiles HMS-300 | CMT2300A | 1 | 1 | 1 |
| Hoymiles HMS-350 | CMT2300A | 1 | 1 | 1 |
| Hoymiles HMS-400 | CMT2300A | 1 | 1 | 1 |
| Hoymiles HMS-450 | CMT2300A | 1 | 1 | 1 |
| Hoymiles HMS-500 | CMT2300A | 1 | 1 | 1 |
| Hoymiles HMS-600 | CMT2300A | 2 | 2 | 1 |
| Hoymiles HMS-700 | CMT2300A | 2 | 2 | 1 |
| Hoymiles HMS-800 | CMT2300A | 2 | 2 | 1 |
| Hoymiles HMS-900 | CMT2300A | 2 | 2 | 1 |
| Hoymiles HMS-1000 | CMT2300A | 2 | 2 | 1 |
| Hoymiles HMS-1600 | CMT2300A | 4 | 4 | 1 |
| Hoymiles HMS-1800 | CMT2300A | 4 | 4 | 1 |
| Hoymiles HMS-2000 | CMT2300A | 4 | 4 | 1 |
| Hoymiles HMT-1800 | CMT2300A | 6 | 3 | 3 |
| Hoymiles HMT-2250 | CMT2300A | 6 | 3 | 3 |
| Solenso SOL-H350 | NRF24L01+ | 1 | 1 | 1 |
| Solenso SOL-H400 | NRF24L01+ | 1 | 1 | 1 |
| Solenso SOL-H800 | NRF24L01+ | 2 | 2 | 1 |
| TSUN TSOL-M350 | NRF24L01+ | 1 | 1 | 1 |
| TSUN TSOL-M800 | NRF24L01+ | 2 | 2 | 1 |
| TSUN TSOL-M1600 | NRF24L01+ | 4 | 2 | 1 |
**TSUN compatibility remark:**
Compatibility with OpenDTU is most likely related to the serial number of the inverter. Current findings indicate that TSUN inverters with a serial number starting with "11" are supported, whereby inverters with a serial number starting with "10" are not.
## Screenshots
Several screenshots of the frontend can be found here: [Screenshots](docs/screenshots/README.md)
## Configuration and usage
### First configuration
* After the [initial flashing](docs/hardware_flash.md#flashing-and-starting-up) of the microcontroller, an Access Point called "OpenDTU-*" is opened. The default password is "openDTU42".
* Use a web browser to open the address [http://192.168.4.1](http://192.168.4.1)
* Navigate to Settings --> Network Settings and enter your WiFi credentials. The username to access the config menu is "admin" and the password the same as for accessing the Access Point (default: "openDTU42").
* OpenDTU then simultaneously connects to your WiFi AP with these credentials. Navigate to Info --> Network and look into section "Network Interface (Station)" for the IP address received via DHCP.
* If your WiFi AP uses an allow-list for MAC-addresses, please be aware that the ESP32 has two different MAC addresses for its AP and client modes, they are also listed at Info --> Network.
* When OpenDTU is connected to a configured WiFI AP, the "OpenDTU-*" Access Point is closed after 3 minutes.
* OpenDTU needs access to a working NTP server to get the current date & time. Both are sent to the inverter with each request. Default NTP server is pool.ntp.org. If your network has different requirements please change accordingly (Settings --> NTP Settings).
* Activate Ve.direct, Battery and the AC Charger according to the available hardware
* Configure a Power Meter to provide a data source for the current consumption
* Configure the Dynamic Power Limiter according to the used battery. Documentation about the power limiter interface and states can be found below.
* If desired connect to a home automation system using MQTT or the Webapi.
* A documentation of all available MQTT Topics can be found here: [MQTT Documentation](docs/MQTT_Topics.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)
### 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.
Some points for consideration are:
* Make sure to consider the PSU voltage range when selecting the battery voltage as lower voltages <42V are not supported.
* The PSU runs a noisy fan and it is therefore desireable to switch it off when not being used. Some users have found that switching the slot detect pins with a relay accomplishes this. A GPIO pin is made available from the ESP to turn the PSU on/off
#### Operation modes
openDTU-onBattery supports three operation modes for the Huawei PSU:
1. Fully manual - In this mode the PSU needs to be turned on/off externally using MQTT and voltage and current limits need to be provided. See [MQTT Documentation](docs/MQTT_Topics.md) for details on these commands
2. Manual with auto power on / off - In this mode the PSU is turned on when a current limit > 1A is set. If the current limit is < 1A for some time the PSU is turned off. Current and voltage limits need to be provided externally using MQTT. See [MQTT Documentation](docs/MQTT_Topics.md) for details on these commands.
3. Automatic - In this mode the PSU power is controlled by the Power Meter and information provided in the web-interface. If excess power is present the PSU is turned on. The voltage limit is set as per web-interface and the current limit is set so that the maximum PSU output power equals the Power Meter value. Minium and maximum PSU power levels as configured in the web-interface are respected in this process. The PSU is turned off if the output current is limited and the output power drops below the minium power level. This will disable automatic mode until the battery is discharged below the start voltage level (set in the web-interface). This mode can be enabled using the web-interface and MQTT. See [MQTT Documentation](docs/MQTT_Topics.md)
## Troubleshooting
* First: When there is no light on the solar panels, the inverter completely turns off and does not answer to OpenDTU! So if you assembled your OpenDTU in the evening, wait until tomorrow.
* When there is no data received from the inverter(s) - try to reduce the distance between the openDTU and the inverter (e.g. move it to the window towards the roof)
* Under Settings -> DTU Settings you can increase the transmit power "PA level". Default is "minimum".
* The NRF24L01+ needs relatively much current. With bad power supply (and especially bad cables!) a 10 µF capacitor soldered directly to the NRF24L01+ board connector brings more stability (pin 1+2 are the power supply). Note the polarity of the capacitor…
* You can try to use an USB power supply with 1 A or more instead of connecting the ESP32 to the computer.
* Try a different USB cable. Once again, a stable power source is important. Some USB cables are made of much plastic and very little copper inside.
* Double check that you have a radio module NRF24L01+ with a plus sign at the end. NRF24L01 module without the plus are not compatible with this project.
* There is no possibility of auto-discovering the inverters. Double check you have entered the serial numbers of the inverters correctly.
* OpenDTU needs access to a working NTP server to get the current date & time.
* If your problem persists, check the [Issues on Github](https://github.com/tbnobody/OpenDTU/issues). Please inspect not only the open issues, also the closed issues contain useful information.
* Another source of information are the [Discussions](https://github.com/tbnobody/OpenDTU/discussions/)
* When flashing with VSCode Plattform.IO fails and also with ESPRESSIF tool a demo bin file cannot be flashed to the ESP32 with error message "A fatal error occurred: MD5 of file does not match data in flash!" than un-wire/unconnect ESP32 from the NRF24L01+ board. Try to flash again and rewire afterwards.
## Background
This project was started from [this](https://www.mikrocontroller.net/topic/525778) discussion (Mikrocontroller.net).
It was the goal to replace the original Hoymiles DTU (Telemetry Gateway) with their cloud access. With a lot of reverse engineering the Hoymiles protocol was decrypted and analyzed.
## Features for developers
### Status
[![OpenDTU-onBattery Build](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/build.yml/badge.svg)](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/build.yml)
[![cpplint](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/cpplint.yml/badge.svg)](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/cpplint.yml)
[![Yarn Linting](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/yarnlint.yml/badge.svg)](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/yarnlint.yml)
### Core technologies used
* The microcontroller part
* Build with Arduino PlatformIO Framework for the ESP32
* Uses a fork of [ESPAsyncWebserver](https://github.com/yubox-node-org/ESPAsyncWebServer) and [espMqttClient](https://github.com/bertmelis/espMqttClient)
* The WebApp part
* Build with [Vue.js](https://vuejs.org)
* Source is written in TypeScript
### Breaking changes
Generated using: `git log --date=short --pretty=format:"* %h%x09%ad%x09%s" | grep BREAKING`
```code
* 59f43a8 2023-04-17 BREAKING CHANGE: Web API Endpoint /api/devinfo/status requires GET parameter inv=
* 318136d 2023-03-15 BREAKING CHANGE: Updated partition table: Make sure you have a configuration backup and completly reflash the device!
* 3b7aef6 2023-02-13 BREAKING CHANGE: Web API!
* d4c838a 2023-02-06 BREAKING CHANGE: Prometheus API!
* daf847e 2022-11-14 BREAKING CHANGE: Removed deprecated config parsing method
* 69b675b 2022-11-01 BREAKING CHANGE: Structure WebAPI /api/livedata/status changed
* 27ed4e3 2022-10-31 BREAKING: Change power factor from percent value to value between 0 and 1
```
### Building
* Building the WebApp
* The WebApp can be build using yarn
```bash
cd webapp
yarn install
yarn build
```
* The updated output is placed in the 'webapp_dist' directory
* It is only necessary to build the webapp when you made changes to it
* Building the microcontroller firmware
* Visual Studio Code with the PlatformIO Extension is required for building
## Related Projects
* [Ahoy](https://github.com/grindylow/ahoy)
* [DTU Simulator](https://github.com/Ziyatoe/DTUsimMI1x00-Hoymiles)
* [OpenDTU extended to talk to Victrons MPPT battery chargers (Ve.Direct)](https://github.com/helgeerbe/OpenDTU_VeDirect)

View File

@ -1,3 +1,91 @@
# Device Profiles # Device Profiles
This documentation will has been moved and can be found here: <https://tbnobody.github.io/OpenDTU-docs/firmware/device_profiles/> This documentation will has been moved and can be found here: <https://tbnobody.github.io/OpenDTU-docs/firmware/device_profiles/>
## Structure of the json file for openDTU-onBattery (outdated example)
```json
[
{
"name": "Generic NodeMCU 38 pin",
"nrf24": {
"miso": 19,
"mosi": 23,
"clk": 18,
"irq": 16,
"en": 4,
"cs": 5
},
"victron": {
"rx": 22,
"tx": 21
},
"battery": {
"rx": 27,
"tx": 14
},
"huawei": {
"miso": 12,
"mosi": 13,
"clk": 26,
"irq": 25,
"power": 33,
"cs": 15
},
"eth": {
"enabled": false,
"phy_addr": -1,
"power": -1,
"mdc": -1,
"mdio": -1,
"type": -1,
"clk_mode": -1
}
},
{
"name": "Generic NodeMCU 38 pin with SSD1306",
"nrf24": {
"miso": 19,
"mosi": 23,
"clk": 18,
"irq": 16,
"en": 4,
"cs": 5
},
"eth": {
"enabled": false,
"phy_addr": -1,
"power": -1,
"mdc": -1,
"mdio": -1,
"type": -1,
"clk_mode": -1
},
"display": {
"type": 2,
"data": 21,
"clk": 22
}
},
{
"name": "Olimex ESP32-POE",
"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
}
}
]
```

View File

@ -0,0 +1,275 @@
<mxfile host="65bd71144e">
<diagram name="Page-1" id="b5b7bab2-c9e2-2cf4-8b2a-24fd1a2a6d21">
<mxGraphModel dx="1370" dy="985" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" background="none" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="6e0c8c40b5770093-72" value="" style="shape=folder;fontStyle=1;spacingTop=10;tabWidth=194;tabHeight=22;tabPosition=left;html=1;rounded=0;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fillColor=none;fontFamily=Verdana;fontSize=10;align=center;" parent="1" vertex="1">
<mxGeometry x="150" y="114.5" width="1090" height="1065.5" as="geometry"/>
</mxCell>
<mxCell id="6e0c8c40b5770093-73" value="&lt;div style=&quot;color: rgb(212, 212, 212); background-color: rgb(30, 30, 30); font-family: Menlo, Monaco, &amp;quot;Courier New&amp;quot;, monospace; font-size: 12px; line-height: 18px;&quot;&gt;&lt;span style=&quot;color: #4ec9b0;&quot;&gt;PowerLimiterClass&lt;/span&gt;::&lt;span style=&quot;color: #dcdcaa;&quot;&gt;loop&lt;/span&gt;()&lt;/div&gt;" style="text;html=1;align=left;verticalAlign=top;spacingTop=-4;fontSize=10;fontFamily=Verdana" parent="1" vertex="1">
<mxGeometry x="150" y="114.5" width="130" height="20" as="geometry"/>
</mxCell>
<mxCell id="sqCMRMHiXPc9LqBIY9SA-1" value="" style="ellipse;html=1;shape=startState;fillColor=#000000;strokeColor=#ff0000;" parent="1" vertex="1">
<mxGeometry x="475" y="50" width="30" height="30" as="geometry"/>
</mxCell>
<mxCell id="sqCMRMHiXPc9LqBIY9SA-2" value="discover state on initial call or after enabling powerLimiter" style="html=1;verticalAlign=bottom;endArrow=open;endSize=8;strokeColor=#ff0000;rounded=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="sqCMRMHiXPc9LqBIY9SA-1" target="2" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="205" y="370" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="sqCMRMHiXPc9LqBIY9SA-6" value="&lt;p style=&quot;margin:0px;margin-top:4px;text-align:center;&quot;&gt;STATE_OFF&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;/p&gt;&lt;p style=&quot;margin:0px;margin-left:8px;text-align:left;&quot;&gt;entry / stop inverter, limit lower limit&lt;br&gt;do / nothing&lt;br&gt;exit / nothing&lt;/p&gt;" style="shape=mxgraph.sysml.simpleState;html=1;overflow=fill;whiteSpace=wrap;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="170" y="585" width="200" height="100" as="geometry"/>
</mxCell>
<mxCell id="sqCMRMHiXPc9LqBIY9SA-7" value="&lt;p style=&quot;margin:0px;margin-top:4px;text-align:center;&quot;&gt;STATE_CONSUME_SOLAR_POWER_ONLY&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;/p&gt;&lt;p style=&quot;margin:0px;margin-left:8px;text-align:left;&quot;&gt;entry /&lt;br&gt;do / setNewLimit&lt;br&gt;exit /&lt;/p&gt;" style="shape=mxgraph.sysml.simpleState;html=1;overflow=fill;whiteSpace=wrap;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="525" y="590" width="270" height="90" as="geometry"/>
</mxCell>
<mxCell id="sqCMRMHiXPc9LqBIY9SA-8" value="&lt;p style=&quot;margin:0px;margin-top:4px;text-align:center;&quot;&gt;STATE_NORMAL_OPERATION&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;/p&gt;&lt;p style=&quot;margin:0px;margin-left:8px;text-align:left;&quot;&gt;entry /&lt;br&gt;do / setNewLimit&lt;br&gt;exit /&lt;/p&gt;" style="shape=mxgraph.sysml.simpleState;html=1;overflow=fill;whiteSpace=wrap;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="970" y="585" width="200" height="100" as="geometry"/>
</mxCell>
<mxCell id="2" value="&lt;p style=&quot;margin:0px;margin-top:4px;text-align:center;&quot;&gt;STATE_DISOVER&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;/p&gt;&lt;p style=&quot;margin:0px;margin-left:8px;text-align:left;&quot;&gt;entry /&amp;nbsp;&lt;br&gt;do / nothing&lt;br&gt;exit /&amp;nbsp;&lt;/p&gt;" style="shape=mxgraph.sysml.simpleState;html=1;overflow=fill;whiteSpace=wrap;align=center;fillColor=#f5f5f5;strokeColor=#666666;fontColor=#333333;" parent="1" vertex="1">
<mxGeometry x="390" y="180" width="200" height="100" as="geometry"/>
</mxCell>
<mxCell id="5" value="" style="shape=ellipse;html=1;dashed=0;whitespace=wrap;aspect=fixed;strokeWidth=5;perimeter=ellipsePerimeter;" parent="1" vertex="1">
<mxGeometry x="414" y="780" width="30" height="30" as="geometry"/>
</mxCell>
<mxCell id="7" value="&lt;p&gt;&lt;span style=&quot;font-size: 11px; background-color: rgb(255, 255, 255);&quot;&gt;!Inverter-&amp;gt;isProducing || isStopThresholdReached&lt;/span&gt;&lt;/p&gt;" style="rhombus;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="425" y="300" width="130" height="130" as="geometry"/>
</mxCell>
<mxCell id="10" value="&lt;span style=&quot;font-size: 11px; background-color: rgb(255, 255, 255);&quot;&gt;canUseDirectSolarPower&lt;/span&gt;" style="rhombus;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="595" y="390" width="130" height="120" as="geometry"/>
</mxCell>
<mxCell id="11" value="" style="endArrow=classic;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="10" target="sqCMRMHiXPc9LqBIY9SA-7" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="620" y="540" as="sourcePoint"/>
<mxPoint x="670" y="490" as="targetPoint"/>
<Array as="points"/>
</mxGeometry>
</mxCell>
<mxCell id="20" value="yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="11" vertex="1" connectable="0">
<mxGeometry x="0.075" y="1" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="12" value="" style="endArrow=classic;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="2" target="7" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="484" y="280" as="sourcePoint"/>
<mxPoint x="420" y="220" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="13" value="" style="endArrow=classic;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryPerimeter=0;" parent="1" source="7" target="sqCMRMHiXPc9LqBIY9SA-6" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="570" y="290" as="sourcePoint"/>
<mxPoint x="400" y="330" as="targetPoint"/>
<Array as="points">
<mxPoint x="270" y="365"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="14" value="yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="13" vertex="1" connectable="0">
<mxGeometry x="0.3049" relative="1" as="geometry">
<mxPoint x="80" y="-105" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="15" value="" style="endArrow=classic;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="7" target="10" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="360" y="370" as="sourcePoint"/>
<mxPoint x="280" y="595" as="targetPoint"/>
<Array as="points">
<mxPoint x="660" y="365"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="16" value="no" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="15" vertex="1" connectable="0">
<mxGeometry x="0.3049" relative="1" as="geometry">
<mxPoint x="-45" y="-15" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="18" value="" style="endArrow=classic;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;edgeStyle=orthogonalEdgeStyle;" parent="1" source="10" target="sqCMRMHiXPc9LqBIY9SA-8" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="760" y="340" as="sourcePoint"/>
<mxPoint x="810" y="290" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="19" value="no" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="18" vertex="1" connectable="0">
<mxGeometry x="0.142" y="2" relative="1" as="geometry">
<mxPoint x="-239" y="-8" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="21" value="isStopThresholdReached" style="rhombus;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="220" y="730" width="130" height="130" as="geometry"/>
</mxCell>
<mxCell id="22" value="" style="endArrow=classic;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="21" target="24" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="620" y="540" as="sourcePoint"/>
<mxPoint x="670" y="490" as="targetPoint"/>
<Array as="points">
<mxPoint x="285" y="995"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="31" value="no" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="22" vertex="1" connectable="0">
<mxGeometry x="-0.702" y="1" relative="1" as="geometry">
<mxPoint x="39" y="91" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="23" value="" style="endArrow=classic;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.581;exitY=0.998;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="sqCMRMHiXPc9LqBIY9SA-6" target="21" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="480" y="810" as="sourcePoint"/>
<mxPoint x="530" y="760" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="41" style="edgeStyle=none;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="24" target="25">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="42" value="no" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="41">
<mxGeometry x="0.36" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="24" value="canUseDirectSolarPower" style="rhombus;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="600" y="930" width="130" height="130" as="geometry"/>
</mxCell>
<mxCell id="25" value="isStartThresholdReached" style="rhombus;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="830" y="930" width="130" height="130" as="geometry"/>
</mxCell>
<mxCell id="29" value="" style="endArrow=classic;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="21" target="5" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="350" y="830" as="sourcePoint"/>
<mxPoint x="400" y="780" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="30" value="yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="29" vertex="1" connectable="0">
<mxGeometry x="-0.1" relative="1" as="geometry">
<mxPoint x="3" y="-15" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="JKiNQljIdbqBsyxyxwz1-38" value="" style="endArrow=classic;html=1;rounded=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="sqCMRMHiXPc9LqBIY9SA-7" target="sqCMRMHiXPc9LqBIY9SA-6" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="570" y="820" as="sourcePoint"/>
<mxPoint x="620" y="770" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="JKiNQljIdbqBsyxyxwz1-39" value="isStopThresholdReached ||&lt;br&gt;(!canUseDirectSolarPower &lt;br&gt;&amp;amp;&amp;amp; EMPTY_WHEN_FULL)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="JKiNQljIdbqBsyxyxwz1-38" vertex="1" connectable="0">
<mxGeometry x="-0.2129" y="-2" relative="1" as="geometry">
<mxPoint x="-14" y="-33" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="JKiNQljIdbqBsyxyxwz1-40" value="isStartThresholdReached ||&lt;br style=&quot;border-color: var(--border-color);&quot;&gt;(!canUseDirectSolarPower&lt;br style=&quot;border-color: var(--border-color);&quot;&gt;&amp;amp;&amp;amp; EMPTY_AT_NIGHT)" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="sqCMRMHiXPc9LqBIY9SA-7" target="sqCMRMHiXPc9LqBIY9SA-8" edge="1">
<mxGeometry x="-0.0286" y="25" width="50" height="50" relative="1" as="geometry">
<mxPoint x="570" y="560" as="sourcePoint"/>
<mxPoint x="620" y="510" as="targetPoint"/>
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="JKiNQljIdbqBsyxyxwz1-41" value="" style="endArrow=classic;html=1;rounded=0;entryX=0.22;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;exitX=0.354;exitY=1.025;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="sqCMRMHiXPc9LqBIY9SA-8" target="sqCMRMHiXPc9LqBIY9SA-6" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1040" y="680" as="sourcePoint"/>
<mxPoint x="890" y="750" as="targetPoint"/>
<Array as="points">
<mxPoint x="1040" y="1070"/>
<mxPoint x="214" y="1070"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="JKiNQljIdbqBsyxyxwz1-42" value="isStopThresholdReached ||" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="JKiNQljIdbqBsyxyxwz1-41" vertex="1" connectable="0">
<mxGeometry x="-0.2129" y="-2" relative="1" as="geometry">
<mxPoint x="-151" y="32" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="JKiNQljIdbqBsyxyxwz1-43" value="" style="shape=ellipse;html=1;dashed=0;whitespace=wrap;aspect=fixed;strokeWidth=5;perimeter=ellipsePerimeter;" parent="1" vertex="1">
<mxGeometry x="1055" y="799.7" width="30" height="30" as="geometry"/>
</mxCell>
<mxCell id="JKiNQljIdbqBsyxyxwz1-44" value="" style="endArrow=classic;html=1;exitX=0.82;exitY=1.003;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitPerimeter=0;" parent="1" target="JKiNQljIdbqBsyxyxwz1-43" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1069.0000000000002" y="685" as="sourcePoint"/>
<mxPoint x="1119" y="749.7" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="JKiNQljIdbqBsyxyxwz1-45" value="inTargetRange do nothing" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="JKiNQljIdbqBsyxyxwz1-44" vertex="1" connectable="0">
<mxGeometry x="-0.1" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="JKiNQljIdbqBsyxyxwz1-46" value="" style="shape=ellipse;html=1;dashed=0;whitespace=wrap;aspect=fixed;strokeWidth=5;perimeter=ellipsePerimeter;" parent="1" vertex="1">
<mxGeometry x="530" y="795" width="30" height="30" as="geometry"/>
</mxCell>
<mxCell id="JKiNQljIdbqBsyxyxwz1-47" value="" style="endArrow=classic;html=1;exitX=0.82;exitY=1.003;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitPerimeter=0;" parent="1" target="JKiNQljIdbqBsyxyxwz1-46" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="544" y="680" as="sourcePoint"/>
<mxPoint x="594" y="745" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="JKiNQljIdbqBsyxyxwz1-48" value="after setNewLimit" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="JKiNQljIdbqBsyxyxwz1-47" vertex="1" connectable="0">
<mxGeometry x="-0.1" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="JKiNQljIdbqBsyxyxwz1-49" value="" style="shape=ellipse;html=1;dashed=0;whitespace=wrap;aspect=fixed;strokeWidth=5;perimeter=ellipsePerimeter;" parent="1" vertex="1">
<mxGeometry x="1110" y="800" width="30" height="30" as="geometry"/>
</mxCell>
<mxCell id="JKiNQljIdbqBsyxyxwz1-50" value="" style="endArrow=classic;html=1;exitX=0.82;exitY=1.003;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitPerimeter=0;" parent="1" target="JKiNQljIdbqBsyxyxwz1-49" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1124.0000000000002" y="685.3" as="sourcePoint"/>
<mxPoint x="1174" y="750" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="JKiNQljIdbqBsyxyxwz1-51" value="after setNewLimit" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="JKiNQljIdbqBsyxyxwz1-50" vertex="1" connectable="0">
<mxGeometry x="-0.1" relative="1" as="geometry">
<mxPoint x="16" y="33" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="38" value="" style="endArrow=classic;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="24" target="sqCMRMHiXPc9LqBIY9SA-7">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="580" y="710" as="sourcePoint"/>
<mxPoint x="630" y="660" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="39" value="yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="38">
<mxGeometry x="0.104" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="40" value="" style="endArrow=classic;html=1;entryX=1;entryY=0.75;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" target="sqCMRMHiXPc9LqBIY9SA-7">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="970" y="658" as="sourcePoint"/>
<mxPoint x="800" y="660" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="48" value="canUseDirectSolarPower&lt;br style=&quot;border-color: var(--border-color);&quot;&gt;&amp;amp;&amp;amp; EMPTY_AT_NIGHT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="40">
<mxGeometry x="-0.04" y="1" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="43" value="" style="endArrow=classic;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="25" target="sqCMRMHiXPc9LqBIY9SA-8">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="960" y="980" as="sourcePoint"/>
<mxPoint x="1010" y="930" as="targetPoint"/>
<Array as="points">
<mxPoint x="1020" y="995"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="44" value="yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="43">
<mxGeometry x="0.1135" y="2" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="45" value="" style="shape=ellipse;html=1;dashed=0;whitespace=wrap;aspect=fixed;strokeWidth=5;perimeter=ellipsePerimeter;" vertex="1" parent="1">
<mxGeometry x="880" y="850" width="30" height="30" as="geometry"/>
</mxCell>
<mxCell id="46" value="" style="endArrow=classic;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="25" target="45">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="580" y="710" as="sourcePoint"/>
<mxPoint x="630" y="660" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="47" value="no" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="46">
<mxGeometry x="-0.08" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

View File

@ -1,3 +1,44 @@
# Web API # Web API
This documentation will has been moved and can be found here: <https://tbnobody.github.io/OpenDTU-docs/firmware/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
This list may be incomplete
| GET/POST | Auth required | URL |
| -------- | --- | -- |
| Get | no | /api/vedirectlivedata/status |
| Get | no | /api/vedirect/status |
| Get | no | /api/huawei/status |
| Get | no | /api/huawei/config |
| Get | no | /api/huawei/limit/config |
| Get | no | /api/batterylivedata/status |
| Get | no | /api/battery/status |
| Get | no | /api/powerlimiter/status |
### Victron REST-API (/api/vedirectlivedata/status):
````JSON
{
"data_age":0,
"age_critical":false,
"PID":"SmartSolar MPPT 100|30",
"SER":"XXX",
"FW":"159",
"LOAD":"ON",
"CS":"Bulk",
"ERR":"No error",
"OR":"Not off",
"MPPT":"MPP Tracker active",
"HSDS":{"v":46,"u":"Days"},
"V":{"v":26.36,"u":"V"},
"I":{"v":3.4,"u":"A"},
"VPV":{"v":37.13,"u":"V"},
"PPV":{"v":93,"u":"W"},
"H19":{"v":83.16,"u":"kWh"},
"H20":{"v":1.39,"u":"kWh"},
"H21":{"v":719,"u":"W"},
"H22":{"v":1.43,"u":"kWh"},
"H23":{"v":737,"u":"W"}
}
````

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 261 KiB

194
docs/components.drawio.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 3.9 MiB

189
docs/hardware_flash.md Normal file
View File

@ -0,0 +1,189 @@
# Hardware, building and flashing Firmware
## Hardware you need
### ESP32 board
For ease of use, buy a "ESP32 DEVKIT DOIT" or "ESP32 NodeMCU Development Board" with an ESP32-S3 or ESP-WROOM-32 chipset on it.
Sample Picture:
![NodeMCU-ESP32](nodemcu-esp32.png)
Also supported: Board with Ethernet-Connector and Power-over-Ethernet [Olimex ESP32-POE](https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware)
### NRF24L01+ radio board (See inverter table above for supported inverters)
The PLUS sign is IMPORTANT! There are different variants available, with antenna on the printed circuit board or external antenna.
Sample picture:
![nrf24l01plus](nrf24l01plus.png)
Buy your hardware from a trusted source, at best from a dealer/online shop in your country where you have support and the right to return non-functional hardware.
When you want to buy from Amazon, AliExpress, eBay etc., take note that there is a lot of low-quality or fake hardware offered. Read customer comments and ratings carefully!
A heavily incomplete list of trusted hardware shops in Germany is:
* [AZ-Delivery](https://www.az-delivery.de/)
* [Makershop](https://www.makershop.de/)
* [Berrybase](https://www.berrybase.de/)
This list is for your convenience only, the project is not related to any of these shops.
### CMT2300A radio board (See inverter table above for supported inverters)
It is important to get a module which supports SPI communication. The following modules are currently supported:
* EBYTE E49-900M20S
The CMT2300A uses 3-Wire half duplex SPI communication. Due to this fact it currently requires a separate SPI bus. If you want to run the CMT2300A module on the same ESP32 as a NRF24L01+ module or a PCD8544 display make sure you get a ESP which supports 2 SPI busses. Currently the SPI bus host is hardcoded to number 2. This may change in future.
### 3.3V / 5V logic level converter
The logic level converter is used to interface with the Victron MPPT charge controller and the relay board. It converts the 3.3V logic level used by the ESP32 to 5V logic used by the other devices.
### SN65HVD230 CAN bus transceiver
The SN65HVD230 CAN bus transceiver is used to interface with the Pylontech battery. It leverages the CAN bus controller of the ESP32. This CAN bus operates at 500kbit/s
### MCP2515 CAN bus module
See [Wiki](https://github.com/helgeerbe/OpenDTU-OnBattery/wiki/Huawei-AC-PSU) for details.
### Relay module
The Huawei PSU can be switched on / off using the slot detect port. This is done by this relay.
### Power supply
Use a power supply with 5 V and 1 A. The USB cable connected to your PC/Notebook may be powerful enough or may be not.
## Wiring up the NRF24L01+ module
### Schematic
![Schematic](Wiring_ESP32_Schematic.png)
### Symbolic view
![Symbolic](Wiring_ESP32_Symbol.png)
### Change pin assignment
Its possible to change all the pins of the NRF24L01+ module, the Display, the LED etc.
The recommend way to change the pin assignment is by creating a custom [device profile](DeviceProfiles.md).
It is also possible to create a custom environment and compile the source yourself. This can be achieved by copying one of the [env:....] sections from 'platformio.ini' to 'platformio_override.ini' and editing the 'platformio_override.ini' file and add/change one or more of the following lines to the 'build_flags' parameter:
```makefile
-DHOYMILES_PIN_MISO=19
-DHOYMILES_PIN_MOSI=23
-DHOYMILES_PIN_SCLK=18
-DHOYMILES_PIN_IRQ=16
-DHOYMILES_PIN_CE=4
-DHOYMILES_PIN_CS=5
-DVICTRON_PIN_TX=21
-DVICTRON_PIN_RX=22
-DPYLONTECH_PIN_RX=27
-DPYLONTECH_PIN_TX=14
-DHUAWEI_PIN_MISO=12
-DHUAWEI_PIN_MOSI=13
-DHUAWEI_PIN_SCLK=26
-DHUAWEI_PIN_IRQ=25
-DHUAWEI_PIN_CS=15
-DHUAWEI_PIN_POWER=33
```
It is recommended to make all changes only in the 'platformio_override.ini', this is your personal copy.
## Flashing and starting up
### with Visual Studio Code
* Install [Visual Studio Code](https://code.visualstudio.com/download) (from now named "vscode")
* In Visual Studio Code, install the [PlatformIO Extension](https://marketplace.visualstudio.com/items?itemName=platformio.platformio-ide)
* Install git and enable git in vscode - [git download](https://git-scm.com/downloads/) - [Instructions](https://www.jcchouinard.com/install-git-in-vscode/)
* Clone this repository (you really have to clone it, don't just download the ZIP file. During the build process the git hash gets embedded into the firmware. If you download the ZIP file a build error will occur): Inside vscode open the command palette by pressing `CTRL` + `SHIFT` + `P`. Enter `git clone`, add the repository-URL `https://github.com/tbnobody/OpenDTU`. Next you have to choose (or create) a target directory.
* In vscode, choose File --> Open Folder and select the previously downloaded source code. (You have to select the folder which contains the "platformio.ini" and "platformio_override.ini" file)
* Adjust the COM port in the file "platformio_override.ini" for your USB-to-serial-converter. It occurs twice:
* upload_port
* monitor_port
* Select the arrow button in the blue bottom status bar (PlatformIO: Upload) to compile and upload the firmware. During the compilation, all required libraries are downloaded automatically.
* Under Linux, if the upload fails with error messages "Could not open /dev/ttyUSB0, the port doesn't exist", you can check via ```ls -la /dev/tty*``` to which group your port belongs to, and then add your user this group via ```sudo adduser <yourusername> dialout``` (if you are using ```arch-linux``` use: ```sudo gpasswd -a <yourusername> uucp```, this method requires a logout/login of the affected user).
* There are two videos showing these steps:
* [Git Clone and compilation](https://youtu.be/9cA_esv3zeA)
* [Full installation and compilation](https://youtu.be/xs6TqHn7QWM)
### on the commandline with PlatformIO Core
* Install [PlatformIO Core](https://platformio.org/install/cli)
* Clone this repository (you really have to clone it, don't just download the ZIP file. During the build process the git hash gets embedded into the firmware. If you download the ZIP file a build error will occur)
* Adjust the COM port in the file "platformio_override.ini". It occurs twice:
* upload_port
* monitor_port
* build: `platformio run -e generic`
* upload to esp module: `platformio run -e generic -t upload`
* other options:
* clean the sources: `platformio run -e generic -t clean`
* erase flash: `platformio run -e generic -t erase`
### using the pre-compiled .bin files
The pre-compiled files can be found on the [github page](https://github.com/tbnobody/OpenDTU) in the tab "Actions" and the sub menu "OpenDTU Build". Just choose the latest build from the master branch (search for "master" in the blue font text but click on the white header text!). You need to be logged in with your github account to download the files.
Use a ESP32 flash tool of your choice (see next chapter) and flash the `.bin` files to the right addresses:
| Address | File |
| ---------| ---------------------- |
| 0x1000 | bootloader.bin |
| 0x8000 | partitions.bin |
| 0xe000 | boot_app0.bin |
| 0x10000 | opendtu-*.bin |
For further updates you can just use the web interface and upload the `opendtu-*.bin` file.
#### Flash with esptool.py (Linux)
```bash
esptool.py --port /dev/ttyUSB0 --chip esp32 --before default_reset --after hard_reset \
write_flash --flash_mode dout --flash_freq 40m --flash_size detect \
0x1000 bootloader.bin \
0x8000 partitions.bin \
0xe000 boot_app0.bin \
0x10000 opendtu-generic.bin
```
#### Flash with Espressif Flash Download Tool (Windows)
[Download link](https://www.espressif.com/en/support/download/other-tools)
* On startup, select Chip Type -> "ESP32" / WorkMode -> "Develop"
* Prepare all settings (see picture). Make sure to uncheck the `DoNotChgBin` option. Otherwise you may get errors like "invalid header".
* ![flash tool image](esp32_flash_download_tool.png)
* Press "Erase" button on screen. Look into the terminal window, you should see dots appear. Then press the "Boot" button on the ESP32 board. Wait for "FINISH" to see if flashing/erasing is done.
* To program, press "Start" on screen, then the "Boot" button.
* When flashing is complete (FINISH appears) then press the Reset button on the ESP32 board (or powercycle ) to start the OpenDTU application.
#### Flash with ESP_Flasher (Windows)
Users report that [ESP_Flasher](https://github.com/Jason2866/ESP_Flasher/releases/) is suitable for flashing OpenDTU on Windows.
#### Flash with [ESP_Flasher](https://espressif.github.io/esptool-js/) - web version
It is also possible to flash it via the web tools which might be more convenient and is platform independent.
## Flashing an Update using "Over The Air" OTA Update
Once you have your OpenDTU running and connected to WLAN, you can do further updates through the web interface.
Navigate to Settings --> Firmware upgrade and press the browse button. Select the firmware file from your local computer.
You'll find the firmware file (after a successful build process) under `.pio/build/generic/firmware.bin`.
If you downloaded a precompiled zip archive, unpack it and choose `opendtu-generic.bin`.
After the successful upload, the OpenDTU immediately restarts into the new firmware.
## Builds
Different builds from existing installations can be found here [Builds](builds/README.md)
Like to show your own build? Just send me a Pull Request.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 146 KiB

35
include/Battery.h Normal file
View File

@ -0,0 +1,35 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include <mutex>
#include <TaskSchedulerDeclarations.h>
#include "BatteryStats.h"
class BatteryProvider {
public:
// returns true if the provider is ready for use, false otherwise
virtual bool init(bool verboseLogging) = 0;
virtual void deinit() = 0;
virtual void loop() = 0;
virtual std::shared_ptr<BatteryStats> getStats() const = 0;
virtual bool usesHwPort2() const { return false; }
};
class BatteryClass {
public:
void init(Scheduler&);
void updateSettings();
std::shared_ptr<BatteryStats const> getStats() const;
private:
void loop();
Task _loopTask;
mutable std::mutex _mutex;
std::unique_ptr<BatteryProvider> _upProvider = nullptr;
};
extern BatteryClass Battery;

183
include/BatteryStats.h Normal file
View File

@ -0,0 +1,183 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <stdint.h>
#include "AsyncJson.h"
#include "Arduino.h"
#include "JkBmsDataPoints.h"
#include "VeDirectShuntController.h"
#include <cfloat>
// mandatory interface for all kinds of batteries
class BatteryStats {
public:
String const& getManufacturer() const { return _manufacturer; }
// the last time *any* datum was updated
uint32_t getAgeSeconds() const { return (millis() - _lastUpdate) / 1000; }
bool updateAvailable(uint32_t since) const;
uint8_t getSoC() const { return _soc; }
uint32_t getSoCAgeSeconds() const { return (millis() - _lastUpdateSoC) / 1000; }
float getVoltage() const { return _voltage; }
uint32_t getVoltageAgeSeconds() const { return (millis() - _lastUpdateVoltage) / 1000; }
// convert stats to JSON for web application live view
virtual void getLiveViewData(JsonVariant& root) const;
void mqttLoop();
// the interval at which all battery datums will be re-published, even
// if they did not change. used to calculate Home Assistent expiration.
virtual uint32_t getMqttFullPublishIntervalMs() const;
bool isSoCValid() const { return _lastUpdateSoC > 0; }
bool isVoltageValid() const { return _lastUpdateVoltage > 0; }
// returns true if the battery reached a critically low voltage/SoC,
// such that it is in need of charging to prevent degredation.
virtual bool getImmediateChargingRequest() const { return false; };
virtual float getChargeCurrent() const { return 0; };
virtual float getChargeCurrentLimitation() const { return FLT_MAX; };
protected:
virtual void mqttPublish() const;
void setSoC(float soc, uint8_t precision, uint32_t timestamp) {
_soc = soc;
_socPrecision = precision;
_lastUpdateSoC = _lastUpdate = timestamp;
}
void setVoltage(float voltage, uint32_t timestamp) {
_voltage = voltage;
_lastUpdateVoltage = _lastUpdate = timestamp;
}
String _manufacturer = "unknown";
uint32_t _lastUpdate = 0;
private:
uint32_t _lastMqttPublish = 0;
float _soc = 0;
uint8_t _socPrecision = 0; // decimal places
uint32_t _lastUpdateSoC = 0;
float _voltage = 0; // total battery pack voltage
uint32_t _lastUpdateVoltage = 0;
};
class PylontechBatteryStats : public BatteryStats {
friend class PylontechCanReceiver;
public:
void getLiveViewData(JsonVariant& root) const final;
void mqttPublish() const final;
bool getImmediateChargingRequest() const { return _chargeImmediately; } ;
float getChargeCurrent() const { return _current; } ;
float getChargeCurrentLimitation() const { return _chargeCurrentLimitation; } ;
private:
void setManufacturer(String&& m) { _manufacturer = std::move(m); }
void setLastUpdate(uint32_t ts) { _lastUpdate = ts; }
float _chargeVoltage;
float _chargeCurrentLimitation;
float _dischargeCurrentLimitation;
uint16_t _stateOfHealth;
// total current into (positive) or from (negative)
// the battery, i.e., the charging current
float _current;
float _temperature;
bool _alarmOverCurrentDischarge;
bool _alarmOverCurrentCharge;
bool _alarmUnderTemperature;
bool _alarmOverTemperature;
bool _alarmUnderVoltage;
bool _alarmOverVoltage;
bool _alarmBmsInternal;
bool _warningHighCurrentDischarge;
bool _warningHighCurrentCharge;
bool _warningLowTemperature;
bool _warningHighTemperature;
bool _warningLowVoltage;
bool _warningHighVoltage;
bool _warningBmsInternal;
bool _chargeEnabled;
bool _dischargeEnabled;
bool _chargeImmediately;
};
class JkBmsBatteryStats : public BatteryStats {
public:
void getLiveViewData(JsonVariant& root) const final {
getJsonData(root, false);
}
void getInfoViewData(JsonVariant& root) const {
getJsonData(root, true);
}
void mqttPublish() const final;
uint32_t getMqttFullPublishIntervalMs() const final { return 60 * 1000; }
void updateFrom(JkBms::DataPointContainer const& dp);
private:
void getJsonData(JsonVariant& root, bool verbose) const;
JkBms::DataPointContainer _dataPoints;
mutable uint32_t _lastMqttPublish = 0;
mutable uint32_t _lastFullMqttPublish = 0;
uint16_t _cellMinMilliVolt = 0;
uint16_t _cellAvgMilliVolt = 0;
uint16_t _cellMaxMilliVolt = 0;
uint32_t _cellVoltageTimestamp = 0;
};
class VictronSmartShuntStats : public BatteryStats {
public:
void getLiveViewData(JsonVariant& root) const final;
void mqttPublish() const final;
void updateFrom(VeDirectShuntController::data_t const& shuntData);
private:
float _current;
float _temperature;
bool _tempPresent;
uint8_t _chargeCycles;
uint32_t _timeToGo;
float _chargedEnergy;
float _dischargedEnergy;
String _modelName;
int32_t _instantaneousPower;
float _consumedAmpHours;
int32_t _lastFullCharge;
bool _alarmLowVoltage;
bool _alarmHighVoltage;
bool _alarmLowSOC;
bool _alarmLowTemperature;
bool _alarmHighTemperature;
};
class MqttBatteryStats : public BatteryStats {
friend class MqttBattery;
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 { }
// if the voltage is subscribed to at all, it alone does not warrant a
// card in the live view, since the SoC is already displayed at the top
void getLiveViewData(JsonVariant& root) const final { }
};

View File

@ -18,7 +18,7 @@
#define MQTT_MAX_HOSTNAME_STRLEN 128 #define MQTT_MAX_HOSTNAME_STRLEN 128
#define MQTT_MAX_USERNAME_STRLEN 64 #define MQTT_MAX_USERNAME_STRLEN 64
#define MQTT_MAX_PASSWORD_STRLEN 64 #define MQTT_MAX_PASSWORD_STRLEN 64
#define MQTT_MAX_TOPIC_STRLEN 32 #define MQTT_MAX_TOPIC_STRLEN 256
#define MQTT_MAX_LWTVALUE_STRLEN 20 #define MQTT_MAX_LWTVALUE_STRLEN 20
#define MQTT_MAX_CERT_STRLEN 2560 #define MQTT_MAX_CERT_STRLEN 2560
@ -30,6 +30,15 @@
#define DEV_MAX_MAPPING_NAME_STRLEN 63 #define DEV_MAX_MAPPING_NAME_STRLEN 63
#define POWERMETER_MAX_PHASES 3
#define POWERMETER_MAX_HTTP_URL_STRLEN 1024
#define POWERMETER_MAX_USERNAME_STRLEN 64
#define POWERMETER_MAX_PASSWORD_STRLEN 64
#define POWERMETER_MAX_HTTP_HEADER_KEY_STRLEN 64
#define POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN 256
#define POWERMETER_MAX_HTTP_JSON_PATH_STRLEN 256
#define POWERMETER_HTTP_TIMEOUT 1000
struct CHANNEL_CONFIG_T { struct CHANNEL_CONFIG_T {
uint16_t MaxChannelPower; uint16_t MaxChannelPower;
char Name[CHAN_MAX_NAME_STRLEN]; char Name[CHAN_MAX_NAME_STRLEN];
@ -51,6 +60,23 @@ struct INVERTER_CONFIG_T {
CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT]; CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT];
}; };
struct POWERMETER_HTTP_PHASE_CONFIG_T {
enum Auth { None, Basic, Digest };
enum Unit { Watts = 0, MilliWatts = 1, KiloWatts = 2 };
bool Enabled;
char Url[POWERMETER_MAX_HTTP_URL_STRLEN + 1];
Auth AuthType;
char Username[POWERMETER_MAX_USERNAME_STRLEN +1];
char Password[POWERMETER_MAX_USERNAME_STRLEN +1];
char HeaderKey[POWERMETER_MAX_HTTP_HEADER_KEY_STRLEN + 1];
char HeaderValue[POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN + 1];
uint16_t Timeout;
char JsonPath[POWERMETER_MAX_HTTP_JSON_PATH_STRLEN + 1];
Unit PowerUnit;
bool SignInverted;
};
using PowerMeterHttpConfig = struct POWERMETER_HTTP_PHASE_CONFIG_T;
struct CONFIG_T { struct CONFIG_T {
struct { struct {
uint32_t Version; uint32_t Version;
@ -86,6 +112,7 @@ struct CONFIG_T {
struct { struct {
bool Enabled; bool Enabled;
char Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1]; char Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1];
bool VerboseLogging;
uint32_t Port; uint32_t Port;
char Username[MQTT_MAX_USERNAME_STRLEN + 1]; char Username[MQTT_MAX_USERNAME_STRLEN + 1];
char Password[MQTT_MAX_PASSWORD_STRLEN + 1]; char Password[MQTT_MAX_PASSWORD_STRLEN + 1];
@ -129,6 +156,7 @@ struct CONFIG_T {
uint32_t Frequency; uint32_t Frequency;
uint8_t CountryMode; uint8_t CountryMode;
} Cmt; } Cmt;
bool VerboseLogging;
} Dtu; } Dtu;
struct { struct {
@ -152,6 +180,81 @@ struct CONFIG_T {
uint8_t Brightness; uint8_t Brightness;
} Led_Single[PINMAPPING_LED_COUNT]; } Led_Single[PINMAPPING_LED_COUNT];
struct {
bool Enabled;
bool VerboseLogging;
bool UpdatesOnly;
} Vedirect;
struct {
bool Enabled;
bool VerboseLogging;
uint32_t Interval;
uint32_t Source;
char MqttTopicPowerMeter1[MQTT_MAX_TOPIC_STRLEN + 1];
char MqttTopicPowerMeter2[MQTT_MAX_TOPIC_STRLEN + 1];
char MqttTopicPowerMeter3[MQTT_MAX_TOPIC_STRLEN + 1];
uint32_t SdmBaudrate;
uint32_t SdmAddress;
uint32_t HttpInterval;
bool HttpIndividualRequests;
PowerMeterHttpConfig Http_Phase[POWERMETER_MAX_PHASES];
} PowerMeter;
struct {
bool Enabled;
bool VerboseLogging;
bool SolarPassThroughEnabled;
uint8_t SolarPassThroughLosses;
bool BatteryAlwaysUseAtNight;
uint32_t Interval;
bool IsInverterBehindPowerMeter;
bool IsInverterSolarPowered;
uint64_t InverterId;
uint8_t InverterChannelId;
int32_t TargetPowerConsumption;
int32_t TargetPowerConsumptionHysteresis;
int32_t LowerPowerLimit;
int32_t BaseLoadLimit;
int32_t UpperPowerLimit;
bool IgnoreSoc;
uint32_t BatterySocStartThreshold;
uint32_t BatterySocStopThreshold;
float VoltageStartThreshold;
float VoltageStopThreshold;
float VoltageLoadCorrectionFactor;
int8_t RestartHour;
uint32_t FullSolarPassThroughSoc;
float FullSolarPassThroughStartVoltage;
float FullSolarPassThroughStopVoltage;
} PowerLimiter;
struct {
bool Enabled;
bool VerboseLogging;
uint8_t Provider;
uint8_t JkBmsInterface;
uint8_t JkBmsPollingInterval;
char MqttSocTopic[MQTT_MAX_TOPIC_STRLEN + 1];
char MqttVoltageTopic[MQTT_MAX_TOPIC_STRLEN + 1];
} Battery;
struct {
bool Enabled;
bool VerboseLogging;
uint32_t CAN_Controller_Frequency;
bool Auto_Power_Enabled;
bool Auto_Power_BatterySoC_Limits_Enabled;
bool Emergency_Charge_Enabled;
float Auto_Power_Voltage_Limit;
float Auto_Power_Enable_Voltage_Limit;
float Auto_Power_Lower_Power_Limit;
float Auto_Power_Upper_Power_Limit;
uint8_t Auto_Power_Stop_BatterySoC_Threshold;
float Auto_Power_Target_Power_Consumption;
} Huawei;
INVERTER_CONFIG_T Inverter[INV_MAX_COUNT]; INVERTER_CONFIG_T Inverter[INV_MAX_COUNT];
char Dev_PinMapping[DEV_MAX_MAPPING_NAME_STRLEN + 1]; char Dev_PinMapping[DEV_MAX_MAPPING_NAME_STRLEN + 1];
}; };

34
include/HttpPowerMeter.h Normal file
View File

@ -0,0 +1,34 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <stdint.h>
#include <Arduino.h>
#include <HTTPClient.h>
#include "Configuration.h"
using Auth_t = PowerMeterHttpConfig::Auth;
using Unit_t = PowerMeterHttpConfig::Unit;
class HttpPowerMeterClass {
public:
void init();
bool updateValues();
float getPower(int8_t phase);
char httpPowerMeterError[256];
bool queryPhase(int phase, PowerMeterHttpConfig const& config);
private:
float power[POWERMETER_MAX_PHASES];
HTTPClient httpClient;
String httpResponse;
bool httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config);
bool extractUrlComponents(String url, String& _protocol, String& _hostname, String& _uri, uint16_t& uint16_t, String& _base64Authorization);
String extractParam(String& authReq, const String& param, const char delimit);
String getcNonce(const int len);
String getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter);
bool tryGetFloatValueForPhase(int phase, String jsonPath, Unit_t unit, bool signInverted);
void prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue);
String sha256(const String& data);
};
extern HttpPowerMeterClass HttpPowerMeter;

158
include/Huawei_can.h Normal file
View File

@ -0,0 +1,158 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <cstdint>
#include "SPI.h"
#include <mcp_can.h>
#include <mutex>
#include <TaskSchedulerDeclarations.h>
#ifndef HUAWEI_PIN_MISO
#define HUAWEI_PIN_MISO 12
#endif
#ifndef HUAWEI_PIN_MOSI
#define HUAWEI_PIN_MOSI 13
#endif
#ifndef HUAWEI_PIN_SCLK
#define HUAWEI_PIN_SCLK 26
#endif
#ifndef HUAWEI_PIN_IRQ
#define HUAWEI_PIN_IRQ 25
#endif
#ifndef HUAWEI_PIN_CS
#define HUAWEI_PIN_CS 15
#endif
#ifndef HUAWEI_PIN_POWER
#define HUAWEI_PIN_POWER 33
#endif
#define HUAWEI_MINIMAL_OFFLINE_VOLTAGE 48
#define HUAWEI_MINIMAL_ONLINE_VOLTAGE 42
#define MAX_CURRENT_MULTIPLIER 20
// Index values for rec_values array
#define HUAWEI_INPUT_POWER_IDX 0
#define HUAWEI_INPUT_FREQ_IDX 1
#define HUAWEI_INPUT_CURRENT_IDX 2
#define HUAWEI_OUTPUT_POWER_IDX 3
#define HUAWEI_EFFICIENCY_IDX 4
#define HUAWEI_OUTPUT_VOLTAGE_IDX 5
#define HUAWEI_OUTPUT_CURRENT_MAX_IDX 6
#define HUAWEI_INPUT_VOLTAGE_IDX 7
#define HUAWEI_OUTPUT_TEMPERATURE_IDX 8
#define HUAWEI_INPUT_TEMPERATURE_IDX 9
#define HUAWEI_OUTPUT_CURRENT_IDX 10
#define HUAWEI_OUTPUT_CURRENT1_IDX 11
// Defines and index values for tx_values array
#define HUAWEI_OFFLINE_VOLTAGE 0x01
#define HUAWEI_ONLINE_VOLTAGE 0x00
#define HUAWEI_OFFLINE_CURRENT 0x04
#define HUAWEI_ONLINE_CURRENT 0x03
// Modes of operation
#define HUAWEI_MODE_OFF 0
#define HUAWEI_MODE_ON 1
#define HUAWEI_MODE_AUTO_EXT 2
#define HUAWEI_MODE_AUTO_INT 3
// Error codes
#define HUAWEI_ERROR_CODE_RX 0x01
#define HUAWEI_ERROR_CODE_TX 0x02
// Wait time/current before shuting down the PSU / charger
// This is set to allow the fan to run for some time
#define HUAWEI_AUTO_MODE_SHUTDOWN_DELAY 60000
#define HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT 0.75
// Updateinterval used to request new values from the PSU
#define HUAWEI_DATA_REQUEST_INTERVAL_MS 2500
typedef struct RectifierParameters {
float input_voltage;
float input_frequency;
float input_current;
float input_power;
float input_temp;
float efficiency;
float output_voltage;
float output_current;
float max_output_current;
float output_power;
float output_temp;
float amp_hour;
} RectifierParameters_t;
class HuaweiCanCommClass {
public:
bool init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk,
uint8_t huawei_irq, uint8_t huawei_cs, uint32_t frequency);
void loop();
bool gotNewRxDataFrame(bool clear);
uint8_t getErrorCode(bool clear);
uint32_t getParameterValue(uint8_t parameter);
void setParameterValue(uint16_t in, uint8_t parameterType);
private:
void sendRequest();
SPIClass *SPI;
MCP_CAN *_CAN;
uint8_t _huaweiIrq; // IRQ pin
uint32_t _nextRequestMillis = 0; // When to send next data request to PSU
std::mutex _mutex;
uint32_t _recValues[12];
uint16_t _txValues[5];
bool _hasNewTxValue[5];
uint8_t _errorCode;
bool _completeUpdateReceived;
};
class HuaweiCanClass {
public:
void init(Scheduler& scheduler, uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power);
void updateSettings(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power);
void setValue(float in, uint8_t parameterType);
void setMode(uint8_t mode);
RectifierParameters_t * get();
uint32_t getLastUpdate() const { return _lastUpdateReceivedMillis; };
bool getAutoPowerStatus() const { return _autoPowerEnabled; };
uint8_t getMode() const { return _mode; };
private:
void loop();
void processReceivedParameters();
void _setValue(float in, uint8_t parameterType);
Task _loopTask;
TaskHandle_t _HuaweiCanCommunicationTaskHdl = NULL;
bool _initialized = false;
uint8_t _huaweiPower; // Power pin
uint8_t _mode = HUAWEI_MODE_AUTO_EXT;
RectifierParameters_t _rp;
uint32_t _lastUpdateReceivedMillis; // Timestamp for last data seen from the PSU
uint32_t _outputCurrentOnSinceMillis; // Timestamp since when the PSU was idle at zero amps
uint32_t _nextAutoModePeriodicIntMillis; // When to set the next output voltage in automatic mode
uint32_t _lastPowerMeterUpdateReceivedMillis; // Timestamp of last seen power meter value
uint32_t _autoModeBlockedTillMillis = 0; // Timestamp to block running auto mode for some time
uint8_t _autoPowerEnabledCounter = 0;
bool _autoPowerEnabled = false;
bool _batteryEmergencyCharging = false;
};
extern HuaweiCanClass HuaweiCan;
extern HuaweiCanCommClass HuaweiCanComm;

79
include/JkBmsController.h Normal file
View File

@ -0,0 +1,79 @@
#pragma once
#include <memory>
#include <vector>
#include <frozen/string.h>
#include "Battery.h"
#include "JkBmsSerialMessage.h"
class DataPointContainer;
namespace JkBms {
class Controller : public BatteryProvider {
public:
Controller() = default;
bool init(bool verboseLogging) final;
void deinit() final;
void loop() final;
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
bool usesHwPort2() const final {
return ARDUINO_USB_CDC_ON_BOOT != 1;
}
private:
enum class Status : unsigned {
Initializing,
Timeout,
WaitingForPollInterval,
HwSerialNotAvailableForWrite,
BusyReading,
RequestSent,
FrameCompleted
};
frozen::string const& getStatusText(Status status);
void announceStatus(Status status);
void sendRequest(uint8_t pollInterval);
void rxData(uint8_t inbyte);
void reset();
void frameComplete();
void processDataPoints(DataPointContainer const& dataPoints);
enum class Interface : unsigned {
Invalid,
Uart,
Transceiver
};
Interface getInterface() const;
enum class ReadState : unsigned {
Idle,
WaitingForFrameStart,
FrameStartReceived,
StartMarkerReceived,
FrameLengthMsbReceived,
ReadingFrame
};
ReadState _readState;
void setReadState(ReadState state) {
_readState = state;
}
bool _verboseLogging = true;
int8_t _rxEnablePin = -1;
int8_t _txEnablePin = -1;
Status _lastStatus = Status::Initializing;
uint32_t _lastStatusPrinted = 0;
uint32_t _lastRequest = 0;
uint16_t _frameLength = 0;
uint8_t _protocolVersion = -1;
SerialResponse::tData _buffer = {};
std::shared_ptr<JkBmsBatteryStats> _stats =
std::make_shared<JkBmsBatteryStats>();
};
} /* namespace JkBms */

304
include/JkBmsDataPoints.h Normal file
View File

@ -0,0 +1,304 @@
#pragma once
#include <Arduino.h>
#include <map>
#include <optional>
#include <string>
#include <unordered_map>
#include <variant>
#include <frozen/map.h>
#include <frozen/string.h>
namespace JkBms {
#define ALARM_BITS(fnc) \
fnc(LowCapacity, (1<<0)) \
fnc(BmsOvertemperature, (1<<1)) \
fnc(ChargingOvervoltage, (1<<2)) \
fnc(DischargeUndervoltage, (1<<3)) \
fnc(BatteryOvertemperature, (1<<4)) \
fnc(ChargingOvercurrent, (1<<5)) \
fnc(DischargeOvercurrent, (1<<6)) \
fnc(CellVoltageDifference, (1<<7)) \
fnc(BatteryBoxOvertemperature, (1<<8)) \
fnc(BatteryUndertemperature, (1<<9)) \
fnc(CellOvervoltage, (1<<10)) \
fnc(CellUndervoltage, (1<<11)) \
fnc(AProtect, (1<<12)) \
fnc(BProtect, (1<<13)) \
fnc(Reserved1, (1<<14)) \
fnc(Reserved2, (1<<15))
enum class AlarmBits : uint16_t {
#define ALARM_ENUM(name, value) name = value,
ALARM_BITS(ALARM_ENUM)
#undef ALARM_ENUM
};
static const frozen::map<AlarmBits, frozen::string, 16> AlarmBitTexts = {
#define ALARM_TEXT(name, value) { AlarmBits::name, #name },
ALARM_BITS(ALARM_TEXT)
#undef ALARM_TEXT
};
#define STATUS_BITS(fnc) \
fnc(ChargingActive, (1<<0)) \
fnc(DischargingActive, (1<<1)) \
fnc(BalancingActive, (1<<2)) \
fnc(BatteryOnline, (1<<3))
enum class StatusBits : uint16_t {
#define STATUS_ENUM(name, value) name = value,
STATUS_BITS(STATUS_ENUM)
#undef STATUS_ENUM
};
static const frozen::map<StatusBits, frozen::string, 4> StatusBitTexts = {
#define STATUS_TEXT(name, value) { StatusBits::name, #name },
STATUS_BITS(STATUS_TEXT)
#undef STATUS_TEXT
};
enum class DataPointLabel : uint8_t {
CellsMilliVolt = 0x79,
BmsTempCelsius = 0x80,
BatteryTempOneCelsius = 0x81,
BatteryTempTwoCelsius = 0x82,
BatteryVoltageMilliVolt = 0x83,
BatteryCurrentMilliAmps = 0x84,
BatterySoCPercent = 0x85,
BatteryTemperatureSensorAmount = 0x86,
BatteryCycles = 0x87,
BatteryCycleCapacity = 0x89,
BatteryCellAmount = 0x8a,
AlarmsBitmask = 0x8b,
StatusBitmask = 0x8c,
TotalOvervoltageThresholdMilliVolt = 0x8e,
TotalUndervoltageThresholdMilliVolt = 0x8f,
CellOvervoltageThresholdMilliVolt = 0x90,
CellOvervoltageRecoveryMilliVolt = 0x91,
CellOvervoltageProtectionDelaySeconds = 0x92,
CellUndervoltageThresholdMilliVolt = 0x93,
CellUndervoltageRecoveryMilliVolt = 0x94,
CellUndervoltageProtectionDelaySeconds = 0x95,
CellVoltageDiffThresholdMilliVolt = 0x96,
DischargeOvercurrentThresholdAmperes = 0x97,
DischargeOvercurrentDelaySeconds = 0x98,
ChargeOvercurrentThresholdAmps = 0x99,
ChargeOvercurrentDelaySeconds = 0x9a,
BalanceCellVoltageThresholdMilliVolt = 0x9b,
BalanceVoltageDiffThresholdMilliVolt = 0x9c,
BalancingEnabled = 0x9d,
BmsTempProtectionThresholdCelsius = 0x9e,
BmsTempRecoveryThresholdCelsius = 0x9f,
BatteryTempProtectionThresholdCelsius = 0xa0,
BatteryTempRecoveryThresholdCelsius = 0xa1,
BatteryTempDiffThresholdCelsius = 0xa2,
ChargeHighTempThresholdCelsius = 0xa3,
DischargeHighTempThresholdCelsius = 0xa4,
ChargeLowTempThresholdCelsius = 0xa5,
ChargeLowTempRecoveryCelsius = 0xa6,
DischargeLowTempThresholdCelsius = 0xa7,
DischargeLowTempRecoveryCelsius = 0xa8,
CellAmountSetting = 0xa9,
BatteryCapacitySettingAmpHours = 0xaa,
BatteryChargeEnabled = 0xab,
BatteryDischargeEnabled = 0xac,
CurrentCalibrationMilliAmps = 0xad,
BmsAddress = 0xae,
BatteryType = 0xaf,
SleepWaitTime = 0xb0, // what's this?
LowCapacityAlarmThresholdPercent = 0xb1,
ModificationPassword = 0xb2,
DedicatedChargerSwitch = 0xb3, // what's this?
EquipmentId = 0xb4,
DateOfManufacturing = 0xb5,
BmsHourMeterMinutes = 0xb6,
BmsSoftwareVersion = 0xb7,
CurrentCalibration = 0xb8,
ActualBatteryCapacityAmpHours = 0xb9,
ProductId = 0xba,
ProtocolVersion = 0xc0
};
using tCells = std::map<uint8_t, uint16_t>;
template<DataPointLabel> struct DataPointLabelTraits;
#define LABEL_TRAIT(n, t, u) template<> struct DataPointLabelTraits<DataPointLabel::n> { \
using type = t; \
static constexpr char const name[] = #n; \
static constexpr char const unit[] = u; \
};
/**
* the types associated with the labels are the types for the respective data
* points in the JkBms::DataPoint class. they are *not* always equal to the
* type used in the serial message.
*
* it is unfortunate that we have to repeat all enum values here to define the
* traits. code generation could help here (labels are defined in a single
* source of truth and this code is generated -- no typing errors, etc.).
* however, the compiler will complain if an enum is misspelled or traits are
* defined for a removed enum, so we will notice. it will also complain when a
* trait is missing and if a data point for a label without traits is added to
* the DataPointContainer class, because the traits must be available then.
* even though this is tedious to maintain, human errors will be caught.
*/
LABEL_TRAIT(CellsMilliVolt, tCells, "mV");
LABEL_TRAIT(BmsTempCelsius, int16_t, "°C");
LABEL_TRAIT(BatteryTempOneCelsius, int16_t, "°C");
LABEL_TRAIT(BatteryTempTwoCelsius, int16_t, "°C");
LABEL_TRAIT(BatteryVoltageMilliVolt, uint32_t, "mV");
LABEL_TRAIT(BatteryCurrentMilliAmps, int32_t, "mA");
LABEL_TRAIT(BatterySoCPercent, uint8_t, "%");
LABEL_TRAIT(BatteryTemperatureSensorAmount, uint8_t, "");
LABEL_TRAIT(BatteryCycles, uint16_t, "");
LABEL_TRAIT(BatteryCycleCapacity, uint32_t, "Ah");
LABEL_TRAIT(BatteryCellAmount, uint16_t, "");
LABEL_TRAIT(AlarmsBitmask, uint16_t, "");
LABEL_TRAIT(StatusBitmask, uint16_t, "");
LABEL_TRAIT(TotalOvervoltageThresholdMilliVolt, uint32_t, "mV");
LABEL_TRAIT(TotalUndervoltageThresholdMilliVolt, uint32_t, "mV");
LABEL_TRAIT(CellOvervoltageThresholdMilliVolt, uint16_t, "mV");
LABEL_TRAIT(CellOvervoltageRecoveryMilliVolt, uint16_t, "mV");
LABEL_TRAIT(CellOvervoltageProtectionDelaySeconds, uint16_t, "s");
LABEL_TRAIT(CellUndervoltageThresholdMilliVolt, uint16_t, "mV");
LABEL_TRAIT(CellUndervoltageRecoveryMilliVolt, uint16_t, "mV");
LABEL_TRAIT(CellUndervoltageProtectionDelaySeconds, uint16_t, "s");
LABEL_TRAIT(CellVoltageDiffThresholdMilliVolt, uint16_t, "mV");
LABEL_TRAIT(DischargeOvercurrentThresholdAmperes, uint16_t, "A");
LABEL_TRAIT(DischargeOvercurrentDelaySeconds, uint16_t, "s");
LABEL_TRAIT(ChargeOvercurrentThresholdAmps, uint16_t, "A");
LABEL_TRAIT(ChargeOvercurrentDelaySeconds, uint16_t, "s");
LABEL_TRAIT(BalanceCellVoltageThresholdMilliVolt, uint16_t, "mV");
LABEL_TRAIT(BalanceVoltageDiffThresholdMilliVolt, uint16_t, "mV");
LABEL_TRAIT(BalancingEnabled, bool, "");
LABEL_TRAIT(BmsTempProtectionThresholdCelsius, uint16_t, "°C");
LABEL_TRAIT(BmsTempRecoveryThresholdCelsius, uint16_t, "°C");
LABEL_TRAIT(BatteryTempProtectionThresholdCelsius, uint16_t, "°C");
LABEL_TRAIT(BatteryTempRecoveryThresholdCelsius, uint16_t, "°C");
LABEL_TRAIT(BatteryTempDiffThresholdCelsius, uint16_t, "°C");
LABEL_TRAIT(ChargeHighTempThresholdCelsius, uint16_t, "°C");
LABEL_TRAIT(DischargeHighTempThresholdCelsius, uint16_t, "°C");
LABEL_TRAIT(ChargeLowTempThresholdCelsius, int16_t, "°C");
LABEL_TRAIT(ChargeLowTempRecoveryCelsius, int16_t, "°C");
LABEL_TRAIT(DischargeLowTempThresholdCelsius, int16_t, "°C");
LABEL_TRAIT(DischargeLowTempRecoveryCelsius, int16_t, "°C");
LABEL_TRAIT(CellAmountSetting, uint8_t, "");
LABEL_TRAIT(BatteryCapacitySettingAmpHours, uint32_t, "Ah");
LABEL_TRAIT(BatteryChargeEnabled, bool, "");
LABEL_TRAIT(BatteryDischargeEnabled, bool, "");
LABEL_TRAIT(CurrentCalibrationMilliAmps, uint16_t, "mA");
LABEL_TRAIT(BmsAddress, uint8_t, "");
LABEL_TRAIT(BatteryType, uint8_t, "");
LABEL_TRAIT(SleepWaitTime, uint16_t, "s");
LABEL_TRAIT(LowCapacityAlarmThresholdPercent, uint8_t, "%");
LABEL_TRAIT(ModificationPassword, std::string, "");
LABEL_TRAIT(DedicatedChargerSwitch, bool, "");
LABEL_TRAIT(EquipmentId, std::string, "");
LABEL_TRAIT(DateOfManufacturing, std::string, "");
LABEL_TRAIT(BmsHourMeterMinutes, uint32_t, "min");
LABEL_TRAIT(BmsSoftwareVersion, std::string, "");
LABEL_TRAIT(CurrentCalibration, bool, "");
LABEL_TRAIT(ActualBatteryCapacityAmpHours, uint32_t, "Ah");
LABEL_TRAIT(ProductId, std::string, "");
LABEL_TRAIT(ProtocolVersion, uint8_t, "");
#undef LABEL_TRAIT
class DataPoint {
friend class DataPointContainer;
public:
using tValue = std::variant<bool, uint8_t, uint16_t, uint32_t,
int16_t, int32_t, std::string, tCells>;
DataPoint() = delete;
DataPoint(DataPoint const& other)
: _strLabel(other._strLabel)
, _strValue(other._strValue)
, _strUnit(other._strUnit)
, _value(other._value)
, _timestamp(other._timestamp) { }
DataPoint(std::string const& strLabel, std::string const& strValue,
std::string const& strUnit, tValue value, uint32_t timestamp)
: _strLabel(strLabel)
, _strValue(strValue)
, _strUnit(strUnit)
, _value(std::move(value))
, _timestamp(timestamp) { }
std::string const& getLabelText() const { return _strLabel; }
std::string const& getValueText() const { return _strValue; }
std::string const& getUnitText() const { return _strUnit; }
uint32_t getTimestamp() const { return _timestamp; }
bool operator==(DataPoint const& other) const {
return _value == other._value;
}
private:
std::string _strLabel;
std::string _strValue;
std::string _strUnit;
tValue _value;
uint32_t _timestamp;
};
template<typename T> std::string dataPointValueToStr(T const& v);
class DataPointContainer {
public:
DataPointContainer() = default;
using Label = DataPointLabel;
template<Label L> using Traits = JkBms::DataPointLabelTraits<L>;
template<Label L>
void add(typename Traits<L>::type val) {
_dataPoints.emplace(
L,
DataPoint(
Traits<L>::name,
dataPointValueToStr(val),
Traits<L>::unit,
DataPoint::tValue(std::move(val)),
millis()
)
);
}
// make sure add() is only called with the type expected for the
// respective label, no implicit conversions allowed.
template<Label L, typename T>
void add(T) = delete;
template<Label L>
std::optional<DataPoint const> getDataPointFor() const {
auto it = _dataPoints.find(L);
if (it == _dataPoints.end()) { return std::nullopt; }
return it->second;
}
template<Label L>
std::optional<typename Traits<L>::type> get() const {
auto optionalDataPoint = getDataPointFor<L>();
if (!optionalDataPoint.has_value()) { return std::nullopt; }
return std::get<typename Traits<L>::type>(optionalDataPoint->_value);
}
using tMap = std::unordered_map<Label, DataPoint const>;
tMap::const_iterator cbegin() const { return _dataPoints.cbegin(); }
tMap::const_iterator cend() const { return _dataPoints.cend(); }
// copy all data points from source into this instance, overwriting
// existing data points in this instance.
void updateFrom(DataPointContainer const& source);
private:
tMap _dataPoints;
};
} /* namespace JkBms */

View File

@ -0,0 +1,93 @@
#pragma once
#include <utility>
#include <vector>
#include <Arduino.h>
#include "JkBmsDataPoints.h"
namespace JkBms {
class SerialMessage {
public:
using tData = std::vector<uint8_t>;
SerialMessage() = delete;
enum class Command : uint8_t {
Activate = 0x01,
Write = 0x02,
Read = 0x03,
Password = 0x05,
ReadAll = 0x06
};
Command getCommand() const { return static_cast<Command>(_raw[8]); }
enum class Source : uint8_t {
BMS = 0x00,
Bluetooth = 0x01,
GPS = 0x02,
Host = 0x03
};
Source getSource() const { return static_cast<Source>(_raw[9]); }
enum class Type : uint8_t {
Command = 0x00,
Response = 0x01,
Unsolicited = 0x02
};
Type getType() const { return static_cast<Type>(_raw[10]); }
// this does *not* include the two byte start marker
uint16_t getFrameLength() const { return get<uint16_t>(_raw.cbegin()+2); }
uint32_t getTerminalId() const { return get<uint32_t>(_raw.cbegin()+4); }
// there are 20 bytes of overhead. two of those are the start marker
// bytes, which are *not* counted by the frame length.
uint16_t getVariableFieldLength() const { return getFrameLength() - 18; }
// the upper byte of the 4-byte "record number" is reserved (for encryption)
uint32_t getSequence() const { return get<uint32_t>(_raw.cend()-9) >> 8; }
bool isValid() const;
uint8_t const* data() { return _raw.data(); }
size_t size() { return _raw.size(); }
protected:
template <typename... Args>
explicit SerialMessage(Args&&... args) : _raw(std::forward<Args>(args)...) { }
template<typename T, typename It> T get(It&& pos) const;
template<typename It> bool getBool(It&& pos) const;
template<typename It> int16_t getTemperature(It&& pos) const;
template<typename It> std::string getString(It&& pos, size_t len, bool replaceZeroes = false) const;
void processBatteryCurrent(tData::const_iterator& pos, uint8_t protocolVersion);
template<typename T> void set(tData::iterator const& pos, T val);
uint16_t calcChecksum() const;
void updateChecksum();
tData _raw;
JkBms::DataPointContainer _dp;
static constexpr uint16_t startMarker = 0x4e57;
static constexpr uint8_t endMarker = 0x68;
};
class SerialResponse : public SerialMessage {
public:
using tData = SerialMessage::tData;
explicit SerialResponse(tData&& raw, uint8_t protocolVersion = -1);
DataPointContainer const& getDataPoints() const { return _dp; }
};
class SerialCommand : public SerialMessage {
public:
using Command = SerialMessage::Command;
explicit SerialCommand(Command cmd);
};
} /* namespace JkBms */

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:
@ -22,13 +23,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;

27
include/MqttBattery.h Normal file
View File

@ -0,0 +1,27 @@
#pragma once
#include <optional>
#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;
String _voltageTopic;
std::shared_ptr<MqttBatteryStats> _stats = std::make_shared<MqttBatteryStats>();
std::optional<float> getFloat(std::string const& src, char const* topic);
void onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties,
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total);
void onMqttMessageVoltage(espMqttClientTypes::MessageProperties const& properties,
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total);
};

View File

@ -0,0 +1,25 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ArduinoJson.h>
#include <TaskSchedulerDeclarations.h>
class MqttHandleBatteryHassClass {
public:
void init(Scheduler& scheduler);
void forceUpdate() { _doPublish = true; }
private:
void loop();
void publish(const String& subtopic, const String& payload);
void publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off);
void publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass = NULL, const char* stateClass = NULL, const char* unitOfMeasurement = NULL);
void createDeviceInfo(JsonObject& object);
Task _loopTask;
bool _doPublish = true;
String serial = "0001"; // pseudo-serial, can be replaced in future with real serialnumber
};
extern MqttHandleBatteryHassClass MqttHandleBatteryHass;

View File

@ -0,0 +1,44 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include <Huawei_can.h>
#include <espMqttClient.h>
#include <TaskSchedulerDeclarations.h>
#include <mutex>
#include <deque>
#include <functional>
class MqttHandleHuaweiClass {
public:
void init(Scheduler& scheduler);
private:
void loop();
enum class Topic : unsigned {
LimitOnlineVoltage,
LimitOnlineCurrent,
LimitOfflineVoltage,
LimitOfflineCurrent,
Mode
};
void onMqttMessage(Topic t,
const espMqttClientTypes::MessageProperties& properties,
const char* topic, const uint8_t* payload, size_t len,
size_t index, size_t total);
Task _loopTask;
uint32_t _lastPublishStats;
uint32_t _lastPublish;
// MQTT callbacks to process updates on subscribed topics are executed in
// the MQTT thread's context. we use this queue to switch processing the
// user requests into the main loop's context (TaskScheduler context).
mutable std::mutex _mqttMutex;
std::deque<std::function<void()>> _mqttCallbacks;
};
extern MqttHandleHuaweiClass MqttHandleHuawei;

View File

@ -0,0 +1,43 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include <espMqttClient.h>
#include <TaskSchedulerDeclarations.h>
#include <mutex>
#include <deque>
#include <functional>
class MqttHandlePowerLimiterClass {
public:
void init(Scheduler& scheduler);
private:
void loop();
enum class MqttPowerLimiterCommand : unsigned {
Mode,
BatterySoCStartThreshold,
BatterySoCStopThreshold,
FullSolarPassthroughSoC,
VoltageStartThreshold,
VoltageStopThreshold,
FullSolarPassThroughStartVoltage,
FullSolarPassThroughStopVoltage
};
void onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
Task _loopTask;
uint32_t _lastPublishStats;
uint32_t _lastPublish;
// MQTT callbacks to process updates on subscribed topics are executed in
// the MQTT thread's context. we use this queue to switch processing the
// user requests into the main loop's context (TaskScheduler context).
mutable std::mutex _mqttMutex;
std::deque<std::function<void()>> _mqttCallbacks;
};
extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter;

View File

@ -0,0 +1,26 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ArduinoJson.h>
#include <TaskSchedulerDeclarations.h>
class MqttHandlePowerLimiterHassClass {
public:
void init(Scheduler& scheduler);
void publishConfig();
void forceUpdate();
private:
void loop();
void publish(const String& subtopic, const String& payload);
void publishNumber(const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min, const int16_t max);
void publishSelect(const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic);
void createDeviceInfo(JsonObject& object);
Task _loopTask;
bool _wasConnected = false;
bool _updateForced = false;
};
extern MqttHandlePowerLimiterHassClass MqttHandlePowerLimiterHass;

View File

@ -0,0 +1,40 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "VeDirectMpptController.h"
#include "Configuration.h"
#include <Arduino.h>
#include <map>
#include <TaskSchedulerDeclarations.h>
#ifndef VICTRON_PIN_RX
#define VICTRON_PIN_RX 22
#endif
#ifndef VICTRON_PIN_TX
#define VICTRON_PIN_TX 21
#endif
class MqttHandleVedirectClass {
public:
void init(Scheduler& scheduler);
void forceUpdate();
private:
void loop();
std::map<std::string, VeDirectMpptController::data_t> _kvFrames;
Task _loopTask;
// point of time in millis() when updated values will be published
uint32_t _nextPublishUpdatesOnly = 0;
// point of time in millis() when all values will be published
uint32_t _nextPublishFull = 1;
bool _PublishFull;
void publish_mppt_data(const VeDirectMpptController::data_t &mpptData,
const VeDirectMpptController::data_t &frame) const;
};
extern MqttHandleVedirectClass MqttHandleVedirect;

View File

@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ArduinoJson.h>
#include "VeDirectMpptController.h"
#include <TaskSchedulerDeclarations.h>
class MqttHandleVedirectHassClass {
public:
void init(Scheduler& scheduler);
void publishConfig();
void forceUpdate();
private:
void loop();
void publish(const String& subtopic, const String& payload);
void publishBinarySensor(const char *caption, const char *icon, const char *subTopic,
const char *payload_on, const char *payload_off,
const VeDirectMpptController::data_t &mpptData);
void publishSensor(const char *caption, const char *icon, const char *subTopic,
const char *deviceClass, const char *stateClass,
const char *unitOfMeasurement,
const VeDirectMpptController::data_t &mpptData);
void createDeviceInfo(JsonObject &object,
const VeDirectMpptController::data_t &mpptData);
Task _loopTask;
bool _wasConnected = false;
bool _updateForced = false;
};
extern MqttHandleVedirectHassClass MqttHandleVedirectHass;

View File

@ -11,6 +11,7 @@ class MqttSettingsClass {
public: public:
MqttSettingsClass(); MqttSettingsClass();
void init(); void init();
void loop();
void performReconnect(); void performReconnect();
bool getConnected(); bool getConnected();
void publish(const String& subtopic, const String& payload); void publish(const String& subtopic, const String& payload);
@ -37,6 +38,7 @@ private:
Ticker _mqttReconnectTimer; Ticker _mqttReconnectTimer;
MqttSubscribeParser _mqttSubscribeParser; MqttSubscribeParser _mqttSubscribeParser;
std::mutex _clientLock; std::mutex _clientLock;
bool _verboseLogging = true;
}; };
extern MqttSettingsClass MqttSettings; extern MqttSettingsClass MqttSettings;

View File

@ -39,6 +39,25 @@ struct PinMapping_t {
uint8_t display_cs; uint8_t display_cs;
uint8_t display_reset; uint8_t display_reset;
int8_t led[PINMAPPING_LED_COUNT]; int8_t led[PINMAPPING_LED_COUNT];
// OpenDTU-OnBattery-specific pins below
int8_t victron_tx;
int8_t victron_rx;
int8_t victron_tx2;
int8_t victron_rx2;
int8_t battery_rx;
int8_t battery_rxen;
int8_t battery_tx;
int8_t battery_txen;
int8_t huawei_miso;
int8_t huawei_mosi;
int8_t huawei_clk;
int8_t huawei_irq;
int8_t huawei_cs;
int8_t huawei_power;
int8_t powermeter_rx;
int8_t powermeter_tx;
int8_t powermeter_dere;
}; };
class PinMappingClass { class PinMappingClass {
@ -50,6 +69,7 @@ public:
bool isValidNrf24Config() const; bool isValidNrf24Config() const;
bool isValidCmt2300Config() const; bool isValidCmt2300Config() const;
bool isValidEthConfig() const; bool isValidEthConfig() const;
bool isValidHuaweiConfig() const;
private: private:
PinMapping_t _pinMapping; PinMapping_t _pinMapping;

104
include/PowerLimiter.h Normal file
View File

@ -0,0 +1,104 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include <espMqttClient.h>
#include <Arduino.h>
#include <Hoymiles.h>
#include <memory>
#include <functional>
#include <optional>
#include <TaskSchedulerDeclarations.h>
#include <frozen/string.h>
#define PL_UI_STATE_INACTIVE 0
#define PL_UI_STATE_CHARGING 1
#define PL_UI_STATE_USE_SOLAR_ONLY 2
#define PL_UI_STATE_USE_SOLAR_AND_BATTERY 3
class PowerLimiterClass {
public:
enum class Status : unsigned {
Initializing,
DisabledByConfig,
DisabledByMqtt,
WaitingForValidTimestamp,
PowerMeterPending,
InverterInvalid,
InverterChanged,
InverterOffline,
InverterCommandsDisabled,
InverterLimitPending,
InverterPowerCmdPending,
InverterDevInfoPending,
InverterStatsPending,
CalculatedLimitBelowMinLimit,
UnconditionalSolarPassthrough,
NoVeDirect,
NoEnergy,
HuaweiPsu,
Stable,
};
void init(Scheduler& scheduler);
uint8_t getInverterUpdateTimeouts() const { return _inverterUpdateTimeouts; }
uint8_t getPowerLimiterState();
int32_t getLastRequestedPowerLimit() { return _lastRequestedPowerLimit; }
enum class Mode : unsigned {
Normal = 0,
Disabled = 1,
UnconditionalFullSolarPassthrough = 2
};
void setMode(Mode m) { _mode = m; }
Mode getMode() const { return _mode; }
void calcNextInverterRestart();
private:
void loop();
Task _loopTask;
int32_t _lastRequestedPowerLimit = 0;
bool _shutdownPending = false;
std::optional<uint32_t> _oInverterStatsMillis = std::nullopt;
std::optional<uint32_t> _oUpdateStartMillis = std::nullopt;
std::optional<int32_t> _oTargetPowerLimitWatts = std::nullopt;
std::optional<bool> _oTargetPowerState = std::nullopt;
Status _lastStatus = Status::Initializing;
uint32_t _lastStatusPrinted = 0;
uint32_t _lastCalculation = 0;
static constexpr uint32_t _calculationBackoffMsDefault = 128;
uint32_t _calculationBackoffMs = _calculationBackoffMsDefault;
Mode _mode = Mode::Normal;
std::shared_ptr<InverterAbstract> _inverter = nullptr;
bool _batteryDischargeEnabled = false;
uint32_t _nextInverterRestart = 0; // Values: 0->not calculated / 1->no restart configured / >1->time of next inverter restart in millis()
uint32_t _nextCalculateCheck = 5000; // time in millis for next NTP check to calulate restart
bool _fullSolarPassThroughEnabled = false;
bool _verboseLogging = true;
uint8_t _inverterUpdateTimeouts = 0;
frozen::string const& getStatusText(Status status);
void announceStatus(Status status);
bool shutdown(Status status);
bool shutdown() { return shutdown(_lastStatus); }
float getBatteryVoltage(bool log = false);
int32_t inverterPowerDcToAc(std::shared_ptr<InverterAbstract> inverter, int32_t dcPower);
void unconditionalSolarPassthrough(std::shared_ptr<InverterAbstract> inverter);
bool canUseDirectSolarPower();
bool calcPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t solarPower, bool batteryPower);
bool updateInverter();
bool setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t newPowerLimit);
int32_t getSolarPower();
float getLoadCorrectedVoltage();
bool testThreshold(float socThreshold, float voltThreshold,
std::function<bool(float, float)> compare);
bool isStartThresholdReached();
bool isStopThresholdReached();
bool isBelowStopThreshold();
bool useFullSolarPassthrough();
};
extern PowerLimiterClass PowerLimiter;

76
include/PowerMeter.h Normal file
View File

@ -0,0 +1,76 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include <espMqttClient.h>
#include <Arduino.h>
#include <map>
#include <list>
#include <mutex>
#include "SDM.h"
#include "sml.h"
#include <TaskSchedulerDeclarations.h>
#include <SoftwareSerial.h>
typedef struct {
const unsigned char OBIS[6];
void (*Fn)(double&);
float* Arg;
} OBISHandler;
class PowerMeterClass {
public:
enum class Source : unsigned {
MQTT = 0,
SDM1PH = 1,
SDM3PH = 2,
HTTP = 3,
SML = 4,
SMAHM2 = 5
};
void init(Scheduler& scheduler);
float getPowerTotal(bool forceUpdate = true);
uint32_t getLastPowerMeterUpdate();
bool isDataValid();
private:
void loop();
void mqtt();
void onMqttMessage(const espMqttClientTypes::MessageProperties& properties,
const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
Task _loopTask;
bool _verboseLogging = true;
uint32_t _lastPowerMeterCheck;
// Used in Power limiter for safety check
uint32_t _lastPowerMeterUpdate;
float _powerMeter1Power = 0.0;
float _powerMeter2Power = 0.0;
float _powerMeter3Power = 0.0;
float _powerMeter1Voltage = 0.0;
float _powerMeter2Voltage = 0.0;
float _powerMeter3Voltage = 0.0;
float _powerMeterImport = 0.0;
float _powerMeterExport = 0.0;
std::map<String, float*> _mqttSubscriptions;
mutable std::mutex _mutex;
std::unique_ptr<SDM> _upSdm = nullptr;
std::unique_ptr<SoftwareSerial> _upSmlSerial = nullptr;
void readPowerMeter();
bool smlReadLoop();
const std::list<OBISHandler> smlHandlerList{
{{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_powerMeter1Power},
{{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterImport},
{{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterExport}
};
};
extern PowerMeterClass PowerMeter;

View File

@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include "Battery.h"
#include <espMqttClient.h>
#include <driver/twai.h>
#include <Arduino.h>
#include <memory>
class PylontechCanReceiver : public BatteryProvider {
public:
bool init(bool verboseLogging) final;
void deinit() final;
void loop() final;
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
private:
uint16_t readUnsignedInt16(uint8_t *data);
int16_t readSignedInt16(uint8_t *data);
float scaleValue(int16_t value, float factor);
bool getBit(uint8_t value, uint8_t bit);
void dummyData();
bool _verboseLogging = true;
std::shared_ptr<PylontechBatteryStats> _stats =
std::make_shared<PylontechBatteryStats>();
};

36
include/SMA_HM.h Normal file
View File

@ -0,0 +1,36 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2024 Holger-Steffen Stapf
*/
#pragma once
#include <cstdint>
#include <TaskSchedulerDeclarations.h>
class SMA_HMClass {
public:
void init(Scheduler& scheduler, bool verboseLogging);
void loop();
void event1();
float getPowerTotal() const { return _powerMeterPower; }
float getPowerL1() const { return _powerMeterL1; }
float getPowerL2() const { return _powerMeterL2; }
float getPowerL3() const { return _powerMeterL3; }
private:
void Soutput(int kanal, int index, int art, int tarif,
char const* name, float value, uint32_t timestamp);
uint8_t* decodeGroup(uint8_t* offset, uint16_t grouplen);
bool _verboseLogging = false;
float _powerMeterPower = 0.0;
float _powerMeterL1 = 0.0;
float _powerMeterL2 = 0.0;
float _powerMeterL3 = 0.0;
uint32_t _previousMillis = 0;
uint32_t _serial = 0;
Task _loopTask;
};
extern SMA_HMClass SMA_HM;

View File

@ -0,0 +1,27 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <map>
class SerialPortManagerClass {
public:
bool allocateMpptPort(int port);
bool allocateBatteryPort(int port);
void invalidateBatteryPort();
void invalidateMpptPorts();
private:
enum Owner {
BATTERY,
MPPT
};
std::map<uint8_t, Owner> allocatedPorts;
bool allocatePort(uint8_t port, Owner owner);
void invalidate(Owner owner);
static const char* print(Owner owner);
};
extern SerialPortManagerClass SerialPortManager;

61
include/VictronMppt.h Normal file
View File

@ -0,0 +1,61 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <mutex>
#include <memory>
#include "VeDirectMpptController.h"
#include "Configuration.h"
#include <TaskSchedulerDeclarations.h>
class VictronMpptClass {
public:
VictronMpptClass() = default;
~VictronMpptClass() = default;
void init(Scheduler& scheduler);
void updateSettings();
bool isDataValid() const;
bool isDataValid(size_t idx) const;
// returns the data age of all controllers,
// i.e, the youngest data's age is returned.
uint32_t getDataAgeMillis() const;
uint32_t getDataAgeMillis(size_t idx) const;
size_t controllerAmount() const { return _controllers.size(); }
std::optional<VeDirectMpptController::data_t> getData(size_t idx = 0) const;
// total output of all MPPT charge controllers in Watts
int32_t getPowerOutputWatts() const;
// total panel input power of all MPPT charge controllers in Watts
int32_t getPanelPowerWatts() const;
// sum of total yield of all MPPT charge controllers in kWh
float getYieldTotal() const;
// sum of today's yield of all MPPT charge controllers in kWh
float getYieldDay() const;
// minimum of all MPPT charge controllers' output voltages in V
float getOutputVoltage() const;
private:
void loop();
VictronMpptClass(VictronMpptClass const& other) = delete;
VictronMpptClass(VictronMpptClass&& other) = delete;
VictronMpptClass& operator=(VictronMpptClass const& other) = delete;
VictronMpptClass& operator=(VictronMpptClass&& other) = delete;
Task _loopTask;
mutable std::mutex _mutex;
using controller_t = std::unique_ptr<VeDirectMpptController>;
std::vector<controller_t> _controllers;
bool initController(int8_t rx, int8_t tx, bool logging, int hwSerialPort);
};
extern VictronMpptClass VictronMppt;

View File

@ -0,0 +1,20 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Battery.h"
class VictronSmartShunt : public BatteryProvider {
public:
bool init(bool verboseLogging) final;
void deinit() final { }
void loop() final;
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
bool usesHwPort2() const final {
return ARDUINO_USB_CDC_ON_BOOT != 1;
}
private:
uint32_t _lastUpdate = 0;
std::shared_ptr<VictronSmartShuntStats> _stats =
std::make_shared<VictronSmartShuntStats>();
};

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include "WebApi_battery.h"
#include "WebApi_config.h" #include "WebApi_config.h"
#include "WebApi_device.h" #include "WebApi_device.h"
#include "WebApi_devinfo.h" #include "WebApi_devinfo.h"
@ -16,6 +17,8 @@
#include "WebApi_network.h" #include "WebApi_network.h"
#include "WebApi_ntp.h" #include "WebApi_ntp.h"
#include "WebApi_power.h" #include "WebApi_power.h"
#include "WebApi_powermeter.h"
#include "WebApi_powerlimiter.h"
#include "WebApi_prometheus.h" #include "WebApi_prometheus.h"
#include "WebApi_security.h" #include "WebApi_security.h"
#include "WebApi_sysstatus.h" #include "WebApi_sysstatus.h"
@ -23,6 +26,11 @@
#include "WebApi_ws_console.h" #include "WebApi_ws_console.h"
#include "WebApi_ws_live.h" #include "WebApi_ws_live.h"
#include <AsyncJson.h> #include <AsyncJson.h>
#include "WebApi_ws_vedirect_live.h"
#include "WebApi_vedirect.h"
#include "WebApi_ws_Huawei.h"
#include "WebApi_Huawei.h"
#include "WebApi_ws_battery.h"
#include <ESPAsyncWebServer.h> #include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h> #include <TaskSchedulerDeclarations.h>
@ -45,6 +53,7 @@ public:
private: private:
AsyncWebServer _server; AsyncWebServer _server;
WebApiBatteryClass _webApiBattery;
WebApiConfigClass _webApiConfig; WebApiConfigClass _webApiConfig;
WebApiDeviceClass _webApiDevice; WebApiDeviceClass _webApiDevice;
WebApiDevInfoClass _webApiDevInfo; WebApiDevInfoClass _webApiDevInfo;
@ -59,12 +68,19 @@ private:
WebApiNetworkClass _webApiNetwork; WebApiNetworkClass _webApiNetwork;
WebApiNtpClass _webApiNtp; WebApiNtpClass _webApiNtp;
WebApiPowerClass _webApiPower; WebApiPowerClass _webApiPower;
WebApiPowerMeterClass _webApiPowerMeter;
WebApiPowerLimiterClass _webApiPowerLimiter;
WebApiPrometheusClass _webApiPrometheus; WebApiPrometheusClass _webApiPrometheus;
WebApiSecurityClass _webApiSecurity; WebApiSecurityClass _webApiSecurity;
WebApiSysstatusClass _webApiSysstatus; WebApiSysstatusClass _webApiSysstatus;
WebApiWebappClass _webApiWebapp; WebApiWebappClass _webApiWebapp;
WebApiWsConsoleClass _webApiWsConsole; WebApiWsConsoleClass _webApiWsConsole;
WebApiWsLiveClass _webApiWsLive; WebApiWsLiveClass _webApiWsLive;
WebApiWsVedirectLiveClass _webApiWsVedirectLive;
WebApiVedirectClass _webApiVedirect;
WebApiHuaweiClass _webApiHuaweiClass;
WebApiWsHuaweiLiveClass _webApiWsHuaweiLive;
WebApiWsBatteryLiveClass _webApiWsBatteryLive;
}; };
extern WebApiClass WebApi; extern WebApiClass WebApi;

19
include/WebApi_Huawei.h Normal file
View File

@ -0,0 +1,19 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
#include <AsyncJson.h>
class WebApiHuaweiClass {
public:
void init(AsyncWebServer& server, Scheduler& scheduler);
void getJsonData(JsonVariant& root);
private:
void onStatus(AsyncWebServerRequest* request);
void onAdminGet(AsyncWebServerRequest* request);
void onAdminPost(AsyncWebServerRequest* request);
void onPost(AsyncWebServerRequest* request);
AsyncWebServer* _server;
};

17
include/WebApi_battery.h Normal file
View File

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

View File

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

View File

@ -0,0 +1,21 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
#include <ArduinoJson.h>
#include "Configuration.h"
class WebApiPowerMeterClass {
public:
void init(AsyncWebServer& server, Scheduler& scheduler);
private:
void onStatus(AsyncWebServerRequest* request);
void onAdminGet(AsyncWebServerRequest* request);
void onAdminPost(AsyncWebServerRequest* request);
void decodeJsonPhaseConfig(JsonObject const& json, PowerMeterHttpConfig& config) const;
void onTestHttpRequest(AsyncWebServerRequest* request);
AsyncWebServer* _server;
};

18
include/WebApi_vedirect.h Normal file
View File

@ -0,0 +1,18 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
class WebApiVedirectClass {
public:
void init(AsyncWebServer& server, Scheduler& scheduler);
private:
void onVedirectStatus(AsyncWebServerRequest* request);
void onVedirectAdminGet(AsyncWebServerRequest* request);
void onVedirectAdminPost(AsyncWebServerRequest* request);
AsyncWebServer* _server;
};

View File

@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "ArduinoJson.h"
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
#include <mutex>
class WebApiWsHuaweiLiveClass {
public:
WebApiWsHuaweiLiveClass();
void init(AsyncWebServer& server, Scheduler& scheduler);
private:
void generateCommonJsonResponse(JsonVariant& root);
void onLivedataStatus(AsyncWebServerRequest* request);
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
AsyncWebServer* _server;
AsyncWebSocket _ws;
std::mutex _mutex;
Task _wsCleanupTask;
void wsCleanupTaskCb();
Task _sendDataTask;
void sendDataTaskCb();
};

View File

@ -0,0 +1,32 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "ArduinoJson.h"
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
#include <mutex>
class WebApiWsBatteryLiveClass {
public:
WebApiWsBatteryLiveClass();
void init(AsyncWebServer& server, Scheduler& scheduler);
private:
void generateCommonJsonResponse(JsonVariant& root);
void onLivedataStatus(AsyncWebServerRequest* request);
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
AsyncWebServer* _server;
AsyncWebSocket _ws;
uint32_t _lastUpdateCheck = 0;
static constexpr uint16_t _responseSize = 1024 + 512;
std::mutex _mutex;
Task _wsCleanupTask;
void wsCleanupTaskCb();
Task _sendDataTask;
void sendDataTaskCb();
};

View File

@ -17,6 +17,9 @@ private:
static void generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv); static void generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv);
static void generateCommonJsonResponse(JsonVariant& root); static void generateCommonJsonResponse(JsonVariant& root);
void generateOnBatteryJsonResponse(JsonVariant& root, bool all);
void sendOnBatteryStats();
static void addField(JsonObject& root, std::shared_ptr<InverterAbstract> inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, String topic = ""); static void addField(JsonObject& root, std::shared_ptr<InverterAbstract> inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, String topic = "");
static void addTotalField(JsonObject& root, const String& name, const float value, const String& unit, const uint8_t digits); static void addTotalField(JsonObject& root, const String& name, const float value, const String& unit, const uint8_t digits);
@ -25,6 +28,12 @@ private:
AsyncWebSocket _ws; AsyncWebSocket _ws;
uint32_t _lastPublishOnBatteryFull = 0;
uint32_t _lastPublishVictron = 0;
uint32_t _lastPublishHuawei = 0;
uint32_t _lastPublishBattery = 0;
uint32_t _lastPublishPowerMeter = 0;
uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 }; uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 };
std::mutex _mutex; std::mutex _mutex;

View File

@ -0,0 +1,37 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "ArduinoJson.h"
#include "Configuration.h"
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
#include <VeDirectMpptController.h>
#include <mutex>
class WebApiWsVedirectLiveClass {
public:
WebApiWsVedirectLiveClass();
void init(AsyncWebServer& server, Scheduler& scheduler);
private:
void generateCommonJsonResponse(JsonVariant& root, bool fullUpdate);
static void populateJson(const JsonObject &root, const VeDirectMpptController::data_t &mpptData);
void onLivedataStatus(AsyncWebServerRequest* request);
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
bool hasUpdate(size_t idx);
AsyncWebServer* _server;
AsyncWebSocket _ws;
uint32_t _lastFullPublish = 0;
uint32_t _lastPublish = 0;
uint16_t responseSize() const;
std::mutex _mutex;
Task _wsCleanupTask;
void wsCleanupTaskCb();
Task _sendDataTask;
void sendDataTaskCb();
};

View File

@ -108,3 +108,55 @@
#define LED_BRIGHTNESS 100U #define LED_BRIGHTNESS 100U
#define MAX_INVERTER_LIMIT 2250 #define MAX_INVERTER_LIMIT 2250
// values specific to downstream project OpenDTU-OnBattery start here:
#define VEDIRECT_ENABLED false
#define VEDIRECT_VERBOSE_LOGGING false
#define VEDIRECT_UPDATESONLY true
#define POWERMETER_ENABLED false
#define POWERMETER_INTERVAL 10
#define POWERMETER_SOURCE 2
#define POWERMETER_SDMBAUDRATE 9600
#define POWERMETER_SDMADDRESS 1
#define POWERLIMITER_ENABLED false
#define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true
#define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3
#define POWERLIMITER_BATTERY_ALWAYS_USE_AT_NIGHT false
#define POWERLIMITER_INTERVAL 10
#define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true
#define POWERLIMITER_IS_INVERTER_SOLAR_POWERED false
#define POWERLIMITER_INVERTER_ID 0ULL
#define POWERLIMITER_INVERTER_CHANNEL_ID 0
#define POWERLIMITER_TARGET_POWER_CONSUMPTION 0
#define POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS 0
#define POWERLIMITER_LOWER_POWER_LIMIT 10
#define POWERLIMITER_BASE_LOAD_LIMIT 100
#define POWERLIMITER_UPPER_POWER_LIMIT 800
#define POWERLIMITER_IGNORE_SOC false
#define POWERLIMITER_BATTERY_SOC_START_THRESHOLD 80
#define POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD 20
#define POWERLIMITER_VOLTAGE_START_THRESHOLD 50.0
#define POWERLIMITER_VOLTAGE_STOP_THRESHOLD 49.0
#define POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR 0.001
#define POWERLIMITER_RESTART_HOUR -1
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_SOC 100
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_START_VOLTAGE 100.0
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE 100.0
#define BATTERY_ENABLED false
#define BATTERY_PROVIDER 0 // Pylontech CAN receiver
#define BATTERY_JKBMS_INTERFACE 0
#define BATTERY_JKBMS_POLLING_INTERVAL 5
#define HUAWEI_ENABLED false
#define HUAWEI_CAN_CONTROLLER_FREQUENCY 8000000UL
#define HUAWEI_AUTO_POWER_VOLTAGE_LIMIT 42.0
#define HUAWEI_AUTO_POWER_ENABLE_VOLTAGE_LIMIT 42.0
#define HUAWEI_AUTO_POWER_LOWER_POWER_LIMIT 150
#define HUAWEI_AUTO_POWER_UPPER_POWER_LIMIT 2000
#define HUAWEI_AUTO_POWER_STOP_BATTERYSOC_THRESHOLD 95
#define HUAWEI_AUTO_POWER_TARGET_POWER_CONSUMPTION 0
#define VERBOSE_LOGGING true

BIN
lib/.DS_Store vendored Normal file

Binary file not shown.

BIN
lib/Hoymiles/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -269,6 +269,11 @@ void HoymilesClass::setPollInterval(const uint32_t interval)
_pollInterval = interval; _pollInterval = interval;
} }
void HoymilesClass::setVerboseLogging(bool verboseLogging)
{
_verboseLogging = verboseLogging;
}
void HoymilesClass::setMessageOutput(Print* output) void HoymilesClass::setMessageOutput(Print* output)
{ {
_messageOutput = output; _messageOutput = output;
@ -278,3 +283,18 @@ Print* HoymilesClass::getMessageOutput()
{ {
return _messageOutput; return _messageOutput;
} }
class Silent : public Print {
public:
size_t write(uint8_t c) final { return 0; }
};
Silent Dummy;
Print* HoymilesClass::getVerboseMessageOutput()
{
if (_verboseLogging) {
return _messageOutput;
}
return &Dummy;
}

View File

@ -22,6 +22,7 @@ public:
void setMessageOutput(Print* output); void setMessageOutput(Print* output);
Print* getMessageOutput(); Print* getMessageOutput();
Print* getVerboseMessageOutput();
std::shared_ptr<InverterAbstract> addInverter(const char* name, const uint64_t serial); std::shared_ptr<InverterAbstract> addInverter(const char* name, const uint64_t serial);
std::shared_ptr<InverterAbstract> getInverterByPos(const uint8_t pos); std::shared_ptr<InverterAbstract> getInverterByPos(const uint8_t pos);
@ -35,6 +36,7 @@ public:
uint32_t PollInterval() const; uint32_t PollInterval() const;
void setPollInterval(const uint32_t interval); void setPollInterval(const uint32_t interval);
void setVerboseLogging(bool verboseLogging);
bool isAllRadioIdle() const; bool isAllRadioIdle() const;
@ -46,6 +48,7 @@ private:
std::mutex _mutex; std::mutex _mutex;
uint32_t _pollInterval = 0; uint32_t _pollInterval = 0;
bool _verboseLogging = true;
uint32_t _lastPoll = 0; uint32_t _lastPoll = 0;
Print* _messageOutput = &Serial; Print* _messageOutput = &Serial;

View File

@ -54,7 +54,7 @@ void HoymilesRadio::sendLastPacketAgain()
void HoymilesRadio::handleReceivedPackage() void HoymilesRadio::handleReceivedPackage()
{ {
if (_busyFlag && _rxTimeout.occured()) { if (_busyFlag && _rxTimeout.occured()) {
Hoymiles.getMessageOutput()->println("RX Period End"); Hoymiles.getVerboseMessageOutput()->println("RX Period End");
std::shared_ptr<InverterAbstract> inv = Hoymiles.getInverterBySerial(_commandQueue.front().get()->getTargetAddress()); std::shared_ptr<InverterAbstract> inv = Hoymiles.getInverterBySerial(_commandQueue.front().get()->getTargetAddress());
if (nullptr != inv) { if (nullptr != inv) {
@ -117,10 +117,10 @@ void HoymilesRadio::handleReceivedPackage()
void HoymilesRadio::dumpBuf(const uint8_t buf[], const uint8_t len, const bool appendNewline) void HoymilesRadio::dumpBuf(const uint8_t buf[], const uint8_t len, const bool appendNewline)
{ {
for (uint8_t i = 0; i < len; i++) { for (uint8_t i = 0; i < len; i++) {
Hoymiles.getMessageOutput()->printf("%02X ", buf[i]); Hoymiles.getVerboseMessageOutput()->printf("%02X ", buf[i]);
} }
if (appendNewline) { if (appendNewline) {
Hoymiles.getMessageOutput()->println(""); Hoymiles.getVerboseMessageOutput()->println("");
} }
} }

View File

@ -126,7 +126,7 @@ void HoymilesRadio_CMT::loop()
} }
if (_packetReceived) { if (_packetReceived) {
Hoymiles.getMessageOutput()->println("Interrupt received"); Hoymiles.getVerboseMessageOutput()->println("Interrupt received");
while (_radio->available()) { while (_radio->available()) {
if (!(_rxBuffer.size() > FRAGMENT_BUFFER_SIZE)) { if (!(_rxBuffer.size() > FRAGMENT_BUFFER_SIZE)) {
fragment_t f; fragment_t f;
@ -165,9 +165,9 @@ void HoymilesRadio_CMT::loop()
if (nullptr != inv) { if (nullptr != inv) {
// Save packet in inverter rx buffer // Save packet in inverter rx buffer
Hoymiles.getMessageOutput()->printf("RX %.2f MHz --> ", getFrequencyFromChannel(f.channel) / 1000000.0); Hoymiles.getVerboseMessageOutput()->printf("RX %.2f MHz --> ", getFrequencyFromChannel(f.channel) / 1000000.0);
dumpBuf(f.fragment, f.len, false); dumpBuf(f.fragment, f.len, false);
Hoymiles.getMessageOutput()->printf("| %d dBm\r\n", f.rssi); Hoymiles.getVerboseMessageOutput()->printf("| %d dBm\r\n", f.rssi);
inv->addRxFragment(f.fragment, f.len); inv->addRxFragment(f.fragment, f.len);
} else { } else {
@ -274,9 +274,9 @@ void HoymilesRadio_CMT::sendEsbPacket(CommandAbstract& cmd)
cmtSwitchDtuFreq(getInvBootFrequency()); cmtSwitchDtuFreq(getInvBootFrequency());
} }
Hoymiles.getMessageOutput()->printf("TX %s %.2f MHz --> ", Hoymiles.getVerboseMessageOutput()->printf("TX %s %.2f MHz --> ",
cmd.getCommandName().c_str(), getFrequencyFromChannel(_radio->getChannel()) / 1000000.0); cmd.getCommandName().c_str(), getFrequencyFromChannel(_radio->getChannel()) / 1000000.0);
cmd.dumpDataPayload(Hoymiles.getMessageOutput()); cmd.dumpDataPayload(Hoymiles.getVerboseMessageOutput());
if (!_radio->write(cmd.getDataPayload(), cmd.getDataSize())) { if (!_radio->write(cmd.getDataPayload(), cmd.getDataSize())) {
Hoymiles.getMessageOutput()->println("TX SPI Timeout"); Hoymiles.getMessageOutput()->println("TX SPI Timeout");

View File

@ -48,7 +48,7 @@ void HoymilesRadio_NRF::loop()
} }
if (_packetReceived) { if (_packetReceived) {
Hoymiles.getMessageOutput()->println("Interrupt received"); Hoymiles.getVerboseMessageOutput()->println("Interrupt received");
while (_radio->available()) { while (_radio->available()) {
if (!(_rxBuffer.size() > FRAGMENT_BUFFER_SIZE)) { if (!(_rxBuffer.size() > FRAGMENT_BUFFER_SIZE)) {
fragment_t f; fragment_t f;
@ -76,9 +76,9 @@ void HoymilesRadio_NRF::loop()
if (nullptr != inv) { if (nullptr != inv) {
// Save packet in inverter rx buffer // Save packet in inverter rx buffer
Hoymiles.getMessageOutput()->printf("RX Channel: %d --> ", f.channel); Hoymiles.getVerboseMessageOutput()->printf("RX Channel: %d --> ", f.channel);
dumpBuf(f.fragment, f.len, false); dumpBuf(f.fragment, f.len, false);
Hoymiles.getMessageOutput()->printf("| %d dBm\r\n", f.rssi); Hoymiles.getVerboseMessageOutput()->printf("| %d dBm\r\n", f.rssi);
inv->addRxFragment(f.fragment, f.len); inv->addRxFragment(f.fragment, f.len);
} else { } else {
@ -183,9 +183,9 @@ void HoymilesRadio_NRF::sendEsbPacket(CommandAbstract& cmd)
openWritingPipe(s); openWritingPipe(s);
_radio->setRetries(3, 15); _radio->setRetries(3, 15);
Hoymiles.getMessageOutput()->printf("TX %s Channel: %d --> ", Hoymiles.getVerboseMessageOutput()->printf("TX %s Channel: %d --> ",
cmd.getCommandName().c_str(), _radio->getChannel()); cmd.getCommandName().c_str(), _radio->getChannel());
cmd.dumpDataPayload(Hoymiles.getMessageOutput()); cmd.dumpDataPayload(Hoymiles.getVerboseMessageOutput());
_radio->write(cmd.getDataPayload(), cmd.getDataSize()); _radio->write(cmd.getDataPayload(), cmd.getDataSize());
_radio->setRetries(0, 0); _radio->setRetries(0, 0);

View File

@ -127,6 +127,10 @@ bool HM_Abstract::sendActivePowerControlRequest(float limit, const PowerLimitCon
return false; return false;
} }
if (CMD_PENDING == SystemConfigPara()->getLastLimitCommandSuccess()) {
return false;
}
if (type == PowerLimitControlType::RelativNonPersistent || type == PowerLimitControlType::RelativPersistent) { if (type == PowerLimitControlType::RelativNonPersistent || type == PowerLimitControlType::RelativPersistent) {
limit = min<float>(100, limit); limit = min<float>(100, limit);
} }
@ -154,6 +158,10 @@ bool HM_Abstract::sendPowerControlRequest(const bool turnOn)
return false; return false;
} }
if (CMD_PENDING == PowerCommand()->getLastPowerCommandSuccess()) {
return false;
}
if (turnOn) { if (turnOn) {
_powerState = 1; _powerState = 1;
} else { } else {

405
lib/SMLParser/sml.cpp Normal file
View File

@ -0,0 +1,405 @@
#include <stdio.h>
#include <string.h>
#include "sml.h"
#include "smlCrcTable.h"
#ifdef SML_DEBUG
char logBuff[200];
#ifdef SML_NATIVE
#define SML_LOG(...) \
do { \
printf(__VA_ARGS__); \
} while (0)
#define SML_TREELOG(level, ...) \
do { \
printf("%.*s", level, " "); \
printf(__VA_ARGS__); \
} while (0)
#elif ARDUINO
#include <Arduino.h>
#define SML_LOG(...) \
do { \
sprintf(logBuff, __VA_ARGS__); \
Serial.print(logBuff); \
} while (0)
#define SML_TREELOG(level, ...) \
do { \
sprintf(logBuff, __VA_ARGS__); \
Serial.print(logBuff); \
} while (0)
#endif
#else
#define SML_LOG(...) \
do { \
} while (0)
#define SML_TREELOG(level, ...) \
do { \
} while (0)
#endif
#define MAX_LIST_SIZE 80
#define MAX_TREE_SIZE 10
static sml_states_t currentState = SML_START;
static char nodes[MAX_TREE_SIZE];
static unsigned char currentLevel = 0;
static unsigned short crc = 0xFFFF;
static signed char sc;
static unsigned short crcMine = 0xFFFF;
static unsigned short crcReceived = 0x0000;
static unsigned char len = 4;
static unsigned char listBuffer[MAX_LIST_SIZE]; /* keeps a list
as length + state + data */
static unsigned char listPos = 0;
void crc16(unsigned char &byte)
{
#ifdef ARDUINO
crc =
pgm_read_word_near(&smlCrcTable[(byte ^ crc) & 0xff]) ^ (crc >> 8 & 0xff);
#else
crc = smlCrcTable[(byte ^ crc) & 0xff] ^ (crc >> 8 & 0xff);
#endif
}
void setState(sml_states_t state, int byteLen)
{
currentState = state;
len = byteLen;
}
void pushListBuffer(unsigned char byte)
{
if (listPos < MAX_LIST_SIZE) {
listBuffer[listPos++] = byte;
}
}
void reduceList()
{
if (currentLevel <= MAX_TREE_SIZE && nodes[currentLevel] > 0)
nodes[currentLevel]--;
}
void smlNewList(unsigned char size)
{
reduceList();
if (currentLevel < MAX_TREE_SIZE)
currentLevel++;
nodes[currentLevel] = size;
SML_TREELOG(currentLevel, "LISTSTART on level %i with %i nodes\n",
currentLevel, size);
setState(SML_LISTSTART, size);
// @todo workaround for lists inside obis lists
if (size > 5) {
listPos = 0;
memset(listBuffer, '\0', MAX_LIST_SIZE);
}
else {
pushListBuffer(size);
pushListBuffer(currentState);
}
}
void checkMagicByte(unsigned char &byte)
{
unsigned int size = 0;
while (currentLevel > 0 && nodes[currentLevel] == 0) {
/* go back in tree if no nodes remaining */
SML_TREELOG(currentLevel, "back to previous list\n");
currentLevel--;
}
if (byte > 0x70 && byte <= 0x7F) {
/* new list */
size = byte & 0x0F;
smlNewList(size);
}
else if (byte >= 0x01 && byte <= 0x6F && nodes[currentLevel] > 0) {
if (byte == 0x01) {
/* no data, get next */
SML_TREELOG(currentLevel, " Data %i (empty)\n", nodes[currentLevel]);
pushListBuffer(0);
pushListBuffer(currentState);
if (nodes[currentLevel] == 1) {
setState(SML_LISTEND, 1);
SML_TREELOG(currentLevel, "LISTEND\n");
}
else {
setState(SML_NEXT, 1);
}
}
else {
size = (byte & 0x0F) - 1;
setState(SML_DATA, size);
if ((byte & 0xF0) == 0x50) {
setState(SML_DATA_SIGNED_INT, size);
}
else if ((byte & 0xF0) == 0x60) {
setState(SML_DATA_UNSIGNED_INT, size);
}
else if ((byte & 0xF0) == 0x00) {
setState(SML_DATA_OCTET_STRING, size);
}
SML_TREELOG(currentLevel,
" Data %i (length = %i%s): ", nodes[currentLevel], size,
(currentState == SML_DATA_SIGNED_INT) ? ", signed int"
: (currentState == SML_DATA_UNSIGNED_INT) ? ", unsigned int"
: (currentState == SML_DATA_OCTET_STRING) ? ", octet string"
: "");
pushListBuffer(size);
pushListBuffer(currentState);
}
reduceList();
}
else if (byte == 0x00) {
/* end of block */
reduceList();
SML_TREELOG(currentLevel, "End of block at level %i\n", currentLevel);
if (currentLevel == 0) {
setState(SML_NEXT, 1);
}
else {
setState(SML_BLOCKEND, 1);
}
}
else if (byte & 0x80) {
// MSB bit is set, another TL byte will follow
if (byte >= 0x80 && byte <= 0x8F) {
// Datatype Octet String
setState(SML_HDATA, (byte & 0x0F) << 4);
}
else if (byte >= 0xF0 /*&& byte <= 0xFF*/) {
/* Datatype List of ...*/
setState(SML_LISTEXTENDED, (byte & 0x0F) << 4);
}
}
else if (byte == 0x1B && currentLevel == 0) {
/* end sequence */
setState(SML_END, 3);
}
else {
/* Unexpected Byte */
SML_TREELOG(currentLevel,
"UNEXPECTED magicbyte >%02X< at currentLevel %i\n", byte,
currentLevel);
setState(SML_UNEXPECTED, 4);
}
}
sml_states_t smlState(unsigned char &currentByte)
{
unsigned char size;
if (len > 0)
len--;
crc16(currentByte);
switch (currentState) {
case SML_UNEXPECTED:
case SML_CHECKSUM_ERROR:
case SML_FINAL:
case SML_START:
currentState = SML_START;
currentLevel = 0; // Reset current level at the begin of a new transmission
// to prevent problems
if (currentByte != 0x1b)
setState(SML_UNEXPECTED, 4);
if (len == 0) {
SML_TREELOG(0, "START\n");
/* completely clean any garbage from crc checksum */
crc = 0xFFFF;
currentByte = 0x1b;
crc16(currentByte);
crc16(currentByte);
crc16(currentByte);
crc16(currentByte);
setState(SML_VERSION, 4);
}
break;
case SML_VERSION:
if (currentByte != 0x01)
setState(SML_UNEXPECTED, 4);
if (len == 0) {
setState(SML_BLOCKSTART, 1);
}
break;
case SML_END:
if (currentByte != 0x1b) {
SML_LOG("UNEXPECTED char >%02X< at SML_END\n", currentByte);
setState(SML_UNEXPECTED, 4);
}
if (len == 0) {
setState(SML_CHECKSUM, 4);
}
break;
case SML_CHECKSUM:
// SML_LOG("CHECK: %02X\n", currentByte);
if (len == 2) {
crcMine = crc ^ 0xFFFF;
}
if (len == 1) {
crcReceived += currentByte;
}
if (len == 0) {
crcReceived = crcReceived | (currentByte << 8);
SML_LOG("Received checksum: %02X\n", crcReceived);
SML_LOG("Calculated checksum: %02X\n", crcMine);
if (crcMine == crcReceived) {
setState(SML_FINAL, 4);
}
else {
setState(SML_CHECKSUM_ERROR, 4);
}
crc = 0xFFFF;
crcReceived = 0x000; /* reset CRC */
}
break;
case SML_HDATA:
size = len + currentByte - 1;
setState(SML_DATA, size);
pushListBuffer(size);
pushListBuffer(currentState);
SML_TREELOG(currentLevel, " Data (length = %i): ", size);
break;
case SML_LISTEXTENDED:
size = len + (currentByte & 0x0F);
SML_TREELOG(currentLevel, "Extended List with Size=%i\n", size);
smlNewList(size);
break;
case SML_DATA:
case SML_DATA_SIGNED_INT:
case SML_DATA_UNSIGNED_INT:
case SML_DATA_OCTET_STRING:
SML_LOG("%02X ", currentByte);
pushListBuffer(currentByte);
if (nodes[currentLevel] == 0 && len == 0) {
SML_LOG("\n");
SML_TREELOG(currentLevel, "LISTEND on level %i\n", currentLevel);
currentState = SML_LISTEND;
}
else if (len == 0) {
currentState = SML_DATAEND;
SML_LOG("\n");
}
break;
case SML_DATAEND:
case SML_NEXT:
case SML_LISTSTART:
case SML_LISTEND:
case SML_BLOCKSTART:
case SML_BLOCKEND:
checkMagicByte(currentByte);
break;
}
return currentState;
}
bool smlOBISCheck(const unsigned char *obis)
{
return (memcmp(obis, &listBuffer[2], 6) == 0);
}
void smlOBISManufacturer(unsigned char *str, int maxSize)
{
int i = 0, pos = 0, size = 0;
while (i < listPos) {
size = (int)listBuffer[i];
i++;
pos++;
if (pos == 6) {
/* get manufacturer at position 6 in list */
size = (size > maxSize - 1) ? maxSize : size;
memcpy(str, &listBuffer[i + 1], size);
str[size + 1] = 0;
}
i += size + 1;
}
}
void smlPow(double &val, signed char &scaler)
{
if (scaler < 0) {
while (scaler++) {
val /= 10;
}
}
else {
while (scaler--) {
val *= 10;
}
}
}
void smlOBISByUnit(long long int &val, signed char &scaler, sml_units_t unit)
{
unsigned char i = 0, pos = 0, size = 0, y = 0, skip = 0;
sml_states_t type;
val = -1; /* unknown or error */
while (i < listPos) {
pos++;
size = (int)listBuffer[i++];
type = (sml_states_t)listBuffer[i++];
if (type == SML_LISTSTART && size > 0) {
// skip a list inside an obis list
skip = size;
while (skip > 0) {
size = (int)listBuffer[i++];
type = (sml_states_t)listBuffer[i++];
i += size;
skip--;
}
size = 0;
}
if (pos == 4 && listBuffer[i] != unit) {
/* return unknown (-1) if unit does not match */
return;
}
if (pos == 5) {
scaler = listBuffer[i];
}
if (pos == 6) {
y = size;
// initialize 64bit signed integer based on MSB from received value
val =
(type == SML_DATA_SIGNED_INT && (listBuffer[i] & (1 << 7))) ? ~0 : 0;
for (y = 0; y < size; y++) {
// left shift received bytes to 64 bit signed integer
val = (val << 8) | listBuffer[i + y];
}
}
i += size;
}
}
void smlOBISWh(double &wh)
{
long long int val;
smlOBISByUnit(val, sc, SML_WATT_HOUR);
wh = val;
smlPow(wh, sc);
}
void smlOBISW(double &w)
{
long long int val;
smlOBISByUnit(val, sc, SML_WATT);
w = val;
smlPow(w, sc);
}
void smlOBISVolt(double &v)
{
long long int val;
smlOBISByUnit(val, sc, SML_VOLT);
v = val;
smlPow(v, sc);
}
void smlOBISAmpere(double &a)
{
long long int val;
smlOBISByUnit(val, sc, SML_AMPERE);
a = val;
smlPow(a, sc);
}

106
lib/SMLParser/sml.h Normal file
View File

@ -0,0 +1,106 @@
#ifndef SML_H
#define SML_H
#include <stdbool.h>
typedef enum {
SML_START,
SML_END,
SML_VERSION,
SML_NEXT,
SML_LISTSTART,
SML_LISTEND,
SML_LISTEXTENDED,
SML_DATA,
SML_HDATA,
SML_DATAEND,
SML_BLOCKSTART,
SML_BLOCKEND,
SML_CHECKSUM,
SML_CHECKSUM_ERROR, /* calculated checksum does not match */
SML_UNEXPECTED, /* unexpected byte received */
SML_FINAL, /* final state, checksum OK */
SML_DATA_SIGNED_INT,
SML_DATA_UNSIGNED_INT,
SML_DATA_OCTET_STRING,
} sml_states_t;
typedef enum {
SML_YEAR = 1,
SML_MONTH = 2,
SML_WEEK = 3,
SML_DAY = 4,
SML_HOUR = 5,
SML_MIN = 6,
SML_SECOND = 7,
SML_DEGREE = 8,
SML_DEGREE_CELSIUS = 9,
SML_CURRENCY = 10,
SML_METRE = 11,
SML_METRE_PER_SECOND = 12,
SML_CUBIC_METRE = 13,
SML_CUBIC_METRE_CORRECTED = 14,
SML_CUBIC_METRE_PER_HOUR = 15,
SML_CUBIC_METRE_PER_HOUR_CORRECTED = 16,
SML_CUBIC_METRE_PER_DAY = 17,
SML_CUBIC_METRE_PER_DAY_CORRECTED = 18,
SML_LITRE = 19,
SML_KILOGRAM = 20,
SML_NEWTON = 21,
SML_NEWTONMETER = 22,
SML_PASCAL = 23,
SML_BAR = 24,
SML_JOULE = 25,
SML_JOULE_PER_HOUR = 26,
SML_WATT = 27,
SML_VOLT_AMPERE = 28,
SML_VAR = 29,
SML_WATT_HOUR = 30,
SML_VOLT_AMPERE_HOUR = 31,
SML_VAR_HOUR = 32,
SML_AMPERE = 33,
SML_COULOMB = 34,
SML_VOLT = 35,
SML_VOLT_PER_METRE = 36,
SML_FARAD = 37,
SML_OHM = 38,
SML_OHM_METRE = 39,
SML_WEBER = 40,
SML_TESLA = 41,
SML_AMPERE_PER_METRE = 42,
SML_HENRY = 43,
SML_HERTZ = 44,
SML_ACTIVE_ENERGY_METER_CONSTANT_OR_PULSE_VALUE = 45,
SML_REACTIVE_ENERGY_METER_CONSTANT_OR_PULSE_VALUE = 46,
SML_APPARENT_ENERGY_METER_CONSTANT_OR_PULSE_VALUE = 47,
SML_VOLT_SQUARED_HOURS = 48,
SML_AMPERE_SQUARED_HOURS = 49,
SML_KILOGRAM_PER_SECOND = 50,
SML_KELVIN = 52,
SML_VOLT_SQUARED_HOUR_METER_CONSTANT_OR_PULSE_VALUE = 53,
SML_AMPERE_SQUARED_HOUR_METER_CONSTANT_OR_PULSE_VALUE = 54,
SML_METER_CONSTANT_OR_PULSE_VALUE = 55,
SML_PERCENTAGE = 56,
SML_AMPERE_HOUR = 57,
SML_ENERGY_PER_VOLUME = 60,
SML_CALORIFIC_VALUE = 61,
SML_MOLE_PERCENT = 62,
SML_MASS_DENSITY = 63,
SML_PASCAL_SECOND = 64,
SML_RESERVED = 253,
SML_OTHER_UNIT = 254,
SML_COUNT = 255
} sml_units_t;
sml_states_t smlState(unsigned char &byte);
bool smlOBISCheck(const unsigned char *obis);
void smlOBISManufacturer(unsigned char *str, int maxSize);
void smlOBISByUnit(long long int &wh, signed char &scaler, sml_units_t unit);
// Be aware that double on Arduino UNO is just 32 bit
void smlOBISWh(double &wh);
void smlOBISW(double &w);
void smlOBISVolt(double &v);
void smlOBISAmpere(double &a);
#endif

View File

@ -0,0 +1,42 @@
#ifndef SML_CRC_TABLE_H
#define SML_CRC_TABLE_H
#include <stdint.h>
#ifdef ARDUINO
#include <Arduino.h>
static const uint16_t smlCrcTable[256] PROGMEM =
#else
static const uint16_t smlCrcTable[256] =
#endif
{0x0000, 0x1189, 0x2312, 0x329B, 0x4624, 0x57AD, 0x6536, 0x74BF, 0x8C48,
0x9DC1, 0xAF5A, 0xBED3, 0xCA6C, 0xDBE5, 0xE97E, 0xF8F7, 0x1081, 0x0108,
0x3393, 0x221A, 0x56A5, 0x472C, 0x75B7, 0x643E, 0x9CC9, 0x8D40, 0xBFDB,
0xAE52, 0xDAED, 0xCB64, 0xF9FF, 0xE876, 0x2102, 0x308B, 0x0210, 0x1399,
0x6726, 0x76AF, 0x4434, 0x55BD, 0xAD4A, 0xBCC3, 0x8E58, 0x9FD1, 0xEB6E,
0xFAE7, 0xC87C, 0xD9F5, 0x3183, 0x200A, 0x1291, 0x0318, 0x77A7, 0x662E,
0x54B5, 0x453C, 0xBDCB, 0xAC42, 0x9ED9, 0x8F50, 0xFBEF, 0xEA66, 0xD8FD,
0xC974, 0x4204, 0x538D, 0x6116, 0x709F, 0x0420, 0x15A9, 0x2732, 0x36BB,
0xCE4C, 0xDFC5, 0xED5E, 0xFCD7, 0x8868, 0x99E1, 0xAB7A, 0xBAF3, 0x5285,
0x430C, 0x7197, 0x601E, 0x14A1, 0x0528, 0x37B3, 0x263A, 0xDECD, 0xCF44,
0xFDDF, 0xEC56, 0x98E9, 0x8960, 0xBBFB, 0xAA72, 0x6306, 0x728F, 0x4014,
0x519D, 0x2522, 0x34AB, 0x0630, 0x17B9, 0xEF4E, 0xFEC7, 0xCC5C, 0xDDD5,
0xA96A, 0xB8E3, 0x8A78, 0x9BF1, 0x7387, 0x620E, 0x5095, 0x411C, 0x35A3,
0x242A, 0x16B1, 0x0738, 0xFFCF, 0xEE46, 0xDCDD, 0xCD54, 0xB9EB, 0xA862,
0x9AF9, 0x8B70, 0x8408, 0x9581, 0xA71A, 0xB693, 0xC22C, 0xD3A5, 0xE13E,
0xF0B7, 0x0840, 0x19C9, 0x2B52, 0x3ADB, 0x4E64, 0x5FED, 0x6D76, 0x7CFF,
0x9489, 0x8500, 0xB79B, 0xA612, 0xD2AD, 0xC324, 0xF1BF, 0xE036, 0x18C1,
0x0948, 0x3BD3, 0x2A5A, 0x5EE5, 0x4F6C, 0x7DF7, 0x6C7E, 0xA50A, 0xB483,
0x8618, 0x9791, 0xE32E, 0xF2A7, 0xC03C, 0xD1B5, 0x2942, 0x38CB, 0x0A50,
0x1BD9, 0x6F66, 0x7EEF, 0x4C74, 0x5DFD, 0xB58B, 0xA402, 0x9699, 0x8710,
0xF3AF, 0xE226, 0xD0BD, 0xC134, 0x39C3, 0x284A, 0x1AD1, 0x0B58, 0x7FE7,
0x6E6E, 0x5CF5, 0x4D7C, 0xC60C, 0xD785, 0xE51E, 0xF497, 0x8028, 0x91A1,
0xA33A, 0xB2B3, 0x4A44, 0x5BCD, 0x6956, 0x78DF, 0x0C60, 0x1DE9, 0x2F72,
0x3EFB, 0xD68D, 0xC704, 0xF59F, 0xE416, 0x90A9, 0x8120, 0xB3BB, 0xA232,
0x5AC5, 0x4B4C, 0x79D7, 0x685E, 0x1CE1, 0x0D68, 0x3FF3, 0x2E7A, 0xE70E,
0xF687, 0xC41C, 0xD595, 0xA12A, 0xB0A3, 0x8238, 0x93B1, 0x6B46, 0x7ACF,
0x4854, 0x59DD, 0x2D62, 0x3CEB, 0x0E70, 0x1FF9, 0xF78F, 0xE606, 0xD49D,
0xC514, 0xB1AB, 0xA022, 0x92B9, 0x8330, 0x7BC7, 0x6A4E, 0x58D5, 0x495C,
0x3DE3, 0x2C6A, 0x1EF1, 0x0F78};
#endif

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

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

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

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

View File

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

View File

@ -0,0 +1,260 @@
#include "VeDirectData.h"
template<typename T, size_t L>
static frozen::string const& getAsString(frozen::map<T, frozen::string, L> const& values, T val)
{
auto pos = values.find(val);
if (pos == values.end()) {
static constexpr frozen::string dummy("???");
return dummy;
}
return pos->second;
}
/*
* This function returns the product id (PID) as readable text.
*/
frozen::string const& veStruct::getPidAsString() const
{
/**
* this map is rendered from [1], which is more recent than [2]. Phoenix
* inverters are not included in the map. unfortunately, the documents do
* not fully align. PID 0xA07F is only present in [1]. PIDs 0xA048, 0xA110,
* and 0xA111 are only present in [2]. PIDs 0xA06D and 0xA078 are rev3 in
* [1] but rev2 in [2].
*
* [1] https://www.victronenergy.com/upload/documents/VE.Direct-Protocol-3.33.pdf
* [2] https://www.victronenergy.com/upload/documents/BlueSolar-HEX-protocol.pdf
*/
static constexpr frozen::map<uint16_t, frozen::string, 105> values = {
{ 0x0203, "BMV-700" },
{ 0x0204, "BMV-702" },
{ 0x0205, "BMV-700H" },
{ 0x0300, "BlueSolar MPPT 70|15" },
{ 0xA040, "BlueSolar MPPT 75|50" },
{ 0xA041, "BlueSolar MPPT 150|35" },
{ 0xA042, "BlueSolar MPPT 75|15" },
{ 0xA043, "BlueSolar MPPT 100|15" },
{ 0xA044, "BlueSolar MPPT 100|30" },
{ 0xA045, "BlueSolar MPPT 100|50" },
{ 0xA046, "BlueSolar MPPT 150|70" },
{ 0xA047, "BlueSolar MPPT 150|100" },
{ 0xA048, "BlueSolar MPPT 75|50 rev2" },
{ 0xA049, "BlueSolar MPPT 100|50 rev2" },
{ 0xA04A, "BlueSolar MPPT 100|30 rev2" },
{ 0xA04B, "BlueSolar MPPT 150|35 rev2" },
{ 0xA04C, "BlueSolar MPPT 75|10" },
{ 0xA04D, "BlueSolar MPPT 150|45" },
{ 0xA04E, "BlueSolar MPPT 150|60" },
{ 0xA04F, "BlueSolar MPPT 150|85" },
{ 0xA050, "SmartSolar MPPT 250|100" },
{ 0xA051, "SmartSolar MPPT 150|100" },
{ 0xA052, "SmartSolar MPPT 150|85" },
{ 0xA053, "SmartSolar MPPT 75|15" },
{ 0xA054, "SmartSolar MPPT 75|10" },
{ 0xA055, "SmartSolar MPPT 100|15" },
{ 0xA056, "SmartSolar MPPT 100|30" },
{ 0xA057, "SmartSolar MPPT 100|50" },
{ 0xA058, "SmartSolar MPPT 150|35" },
{ 0xA059, "SmartSolar MPPT 150|100 rev2" },
{ 0xA05A, "SmartSolar MPPT 150|85 rev2" },
{ 0xA05B, "SmartSolar MPPT 250|70" },
{ 0xA05C, "SmartSolar MPPT 250|85" },
{ 0xA05D, "SmartSolar MPPT 250|60" },
{ 0xA05E, "SmartSolar MPPT 250|45" },
{ 0xA05F, "SmartSolar MPPT 100|20" },
{ 0xA060, "SmartSolar MPPT 100|20 48V" },
{ 0xA061, "SmartSolar MPPT 150|45" },
{ 0xA062, "SmartSolar MPPT 150|60" },
{ 0xA063, "SmartSolar MPPT 150|70" },
{ 0xA064, "SmartSolar MPPT 250|85 rev2" },
{ 0xA065, "SmartSolar MPPT 250|100 rev2" },
{ 0xA066, "BlueSolar MPPT 100|20" },
{ 0xA067, "BlueSolar MPPT 100|20 48V" },
{ 0xA068, "SmartSolar MPPT 250|60 rev2" },
{ 0xA069, "SmartSolar MPPT 250|70 rev2" },
{ 0xA06A, "SmartSolar MPPT 150|45 rev2" },
{ 0xA06B, "SmartSolar MPPT 150|60 rev2" },
{ 0xA06C, "SmartSolar MPPT 150|70 rev2" },
{ 0xA06D, "SmartSolar MPPT 150|85 rev3" },
{ 0xA06E, "SmartSolar MPPT 150|100 rev3" },
{ 0xA06F, "BlueSolar MPPT 150|45 rev2" },
{ 0xA070, "BlueSolar MPPT 150|60 rev2" },
{ 0xA071, "BlueSolar MPPT 150|70 rev2" },
{ 0xA072, "BlueSolar MPPT 150|45 rev3" },
{ 0xA073, "SmartSolar MPPT 150|45 rev3" },
{ 0xA074, "SmartSolar MPPT 75|10 rev2" },
{ 0xA075, "SmartSolar MPPT 75|15 rev2" },
{ 0xA076, "BlueSolar MPPT 100|30 rev3" },
{ 0xA077, "BlueSolar MPPT 100|50 rev3" },
{ 0xA078, "BlueSolar MPPT 150|35 rev3" },
{ 0xA079, "BlueSolar MPPT 75|10 rev2" },
{ 0xA07A, "BlueSolar MPPT 75|15 rev2" },
{ 0xA07B, "BlueSolar MPPT 100|15 rev2" },
{ 0xA07C, "BlueSolar MPPT 75|10 rev3" },
{ 0xA07D, "BlueSolar MPPT 75|15 rev3" },
{ 0xA07E, "SmartSolar MPPT 100|30 12V" },
{ 0xA07F, "All-In-1 SmartSolar MPPT 75|15 12V" },
{ 0xA102, "SmartSolar MPPT VE.Can 150|70" },
{ 0xA103, "SmartSolar MPPT VE.Can 150|45" },
{ 0xA104, "SmartSolar MPPT VE.Can 150|60" },
{ 0xA105, "SmartSolar MPPT VE.Can 150|85" },
{ 0xA106, "SmartSolar MPPT VE.Can 150|100" },
{ 0xA107, "SmartSolar MPPT VE.Can 250|45" },
{ 0xA108, "SmartSolar MPPT VE.Can 250|60" },
{ 0xA109, "SmartSolar MPPT VE.Can 250|70" },
{ 0xA10A, "SmartSolar MPPT VE.Can 250|85" },
{ 0xA10B, "SmartSolar MPPT VE.Can 250|100" },
{ 0xA10C, "SmartSolar MPPT VE.Can 150|70 rev2" },
{ 0xA10D, "SmartSolar MPPT VE.Can 150|85 rev2" },
{ 0xA10E, "SmartSolar MPPT VE.Can 150|100 rev2" },
{ 0xA10F, "BlueSolar MPPT VE.Can 150|100" },
{ 0xA110, "SmartSolar MPPT RS 450|100" },
{ 0xA111, "SmartSolar MPPT RS 450|200" },
{ 0xA112, "BlueSolar MPPT VE.Can 250|70" },
{ 0xA113, "BlueSolar MPPT VE.Can 250|100" },
{ 0xA114, "SmartSolar MPPT VE.Can 250|70 rev2" },
{ 0xA115, "SmartSolar MPPT VE.Can 250|100 rev2" },
{ 0xA116, "SmartSolar MPPT VE.Can 250|85 rev2" },
{ 0xA117, "BlueSolar MPPT VE.Can 150|100 rev2" },
{ 0xA340, "Phoenix Smart IP43 Charger 12|50 (1+1)" },
{ 0xA341, "Phoenix Smart IP43 Charger 12|50 (3)" },
{ 0xA342, "Phoenix Smart IP43 Charger 24|25 (1+1)" },
{ 0xA343, "Phoenix Smart IP43 Charger 24|25 (3)" },
{ 0xA344, "Phoenix Smart IP43 Charger 12|30 (1+1)" },
{ 0xA345, "Phoenix Smart IP43 Charger 12|30 (3)" },
{ 0xA346, "Phoenix Smart IP43 Charger 24|16 (1+1)" },
{ 0xA347, "Phoenix Smart IP43 Charger 24|16 (3)" },
{ 0xA381, "BMV-712 Smart" },
{ 0xA382, "BMV-710H Smart" },
{ 0xA383, "BMV-712 Smart Rev2" },
{ 0xA389, "SmartShunt 500A/50mV" },
{ 0xA38A, "SmartShunt 1000A/50mV" },
{ 0xA38B, "SmartShunt 2000A/50mV" },
{ 0xA3F0, "Smart BuckBoost 12V/12V-50A" },
};
return getAsString(values, productID_PID);
}
/*
* This function returns the state of operations (CS) as readable text.
*/
frozen::string const& veMpptStruct::getCsAsString() const
{
static constexpr frozen::map<uint8_t, frozen::string, 9> values = {
{ 0, "OFF" },
{ 2, "Fault" },
{ 3, "Bulk" },
{ 4, "Absorbtion" },
{ 5, "Float" },
{ 7, "Equalize (manual)" },
{ 245, "Starting-up" },
{ 247, "Auto equalize / Recondition" },
{ 252, "External Control" }
};
return getAsString(values, currentState_CS);
}
/*
* This function returns the state of MPPT (MPPT) as readable text.
*/
frozen::string const& veMpptStruct::getMpptAsString() const
{
static constexpr frozen::map<uint8_t, frozen::string, 3> values = {
{ 0, "OFF" },
{ 1, "Voltage or current limited" },
{ 2, "MPP Tracker active" }
};
return getAsString(values, stateOfTracker_MPPT);
}
/*
* This function returns error state (ERR) as readable text.
*/
frozen::string const& veMpptStruct::getErrAsString() const
{
static constexpr frozen::map<uint8_t, frozen::string, 20> values = {
{ 0, "No error" },
{ 2, "Battery voltage too high" },
{ 17, "Charger temperature too high" },
{ 18, "Charger over current" },
{ 19, "Charger current reversed" },
{ 20, "Bulk time limit exceeded" },
{ 21, "Current sensor issue(sensor bias/sensor broken)" },
{ 26, "Terminals overheated" },
{ 28, "Converter issue (dual converter models only)" },
{ 33, "Input voltage too high (solar panel)" },
{ 34, "Input current too high (solar panel)" },
{ 38, "Input shutdown (due to excessive battery voltage)" },
{ 39, "Input shutdown (due to current flow during off mode)" },
{ 40, "Input" },
{ 65, "Lost communication with one of devices" },
{ 67, "Synchronisedcharging device configuration issue" },
{ 68, "BMS connection lost" },
{ 116, "Factory calibration data lost" },
{ 117, "Invalid/incompatible firmware" },
{ 118, "User settings invalid" }
};
return getAsString(values, errorCode_ERR);
}
/*
* This function returns the off reason (OR) as readable text.
*/
frozen::string const& veMpptStruct::getOrAsString() const
{
static constexpr frozen::map<uint32_t, frozen::string, 10> values = {
{ 0x00000000, "Not off" },
{ 0x00000001, "No input power" },
{ 0x00000002, "Switched off (power switch)" },
{ 0x00000004, "Switched off (device moderegister)" },
{ 0x00000008, "Remote input" },
{ 0x00000010, "Protection active" },
{ 0x00000020, "Paygo" },
{ 0x00000040, "BMS" },
{ 0x00000080, "Engine shutdown detection" },
{ 0x00000100, "Analysing input voltage" }
};
return getAsString(values, offReason_OR);
}
frozen::string const& VeDirectHexData::getResponseAsString() const
{
using Response = VeDirectHexResponse;
static constexpr frozen::map<Response, frozen::string, 7> values = {
{ Response::DONE, "Done" },
{ Response::UNKNOWN, "Unknown" },
{ Response::ERROR, "Error" },
{ Response::PING, "Ping" },
{ Response::GET, "Get" },
{ Response::SET, "Set" },
{ Response::ASYNC, "Async" }
};
return getAsString(values, rsp);
}
frozen::string const& VeDirectHexData::getRegisterAsString() const
{
using Register = VeDirectHexRegister;
static constexpr frozen::map<Register, frozen::string, 11> values = {
{ Register::DeviceMode, "Device Mode" },
{ Register::DeviceState, "Device State" },
{ Register::RemoteControlUsed, "Remote Control Used" },
{ Register::PanelVoltage, "Panel Voltage" },
{ Register::ChargerVoltage, "Charger Voltage" },
{ Register::NetworkTotalDcInputPower, "Network Total DC Input Power" },
{ Register::ChargeControllerTemperature, "Charger Controller Temperature" },
{ Register::SmartBatterySenseTemperature, "Smart Battery Sense Temperature" },
{ Register::NetworkInfo, "Network Info" },
{ Register::NetworkMode, "Network Mode" },
{ Register::NetworkStatus, "Network Status" }
};
return getAsString(values, addr);
}

View File

@ -0,0 +1,139 @@
#pragma once
#include <frozen/string.h>
#include <frozen/map.h>
#define VE_MAX_VALUE_LEN 33 // VE.Direct Protocol: max value size is 33 including /0
#define VE_MAX_HEX_LEN 100 // Maximum size of hex frame - max payload 34 byte (=68 char) + safe buffer
typedef struct {
uint16_t productID_PID = 0; // product id
char serialNr_SER[VE_MAX_VALUE_LEN]; // serial number
char firmwareNr_FW[VE_MAX_VALUE_LEN]; // firmware release number
uint32_t batteryVoltage_V_mV = 0; // battery voltage in mV
int32_t batteryCurrent_I_mA = 0; // battery current in mA (can be negative)
float mpptEfficiency_Percent = 0; // efficiency in percent (calculated, moving average)
frozen::string const& getPidAsString() const; // product ID as string
} veStruct;
struct veMpptStruct : veStruct {
uint8_t stateOfTracker_MPPT; // state of MPP tracker
uint16_t panelPower_PPV_W; // panel power in W
uint32_t panelVoltage_VPV_mV; // panel voltage in mV
uint32_t panelCurrent_mA; // panel current in mA (calculated)
int16_t batteryOutputPower_W; // battery output power in W (calculated, can be negative if load output is used)
uint32_t loadCurrent_IL_mA; // Load current in mA (Available only for models with a load output)
bool loadOutputState_LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit)
uint8_t currentState_CS; // current state of operation e.g. OFF or Bulk
uint8_t errorCode_ERR; // error code
uint32_t offReason_OR; // off reason
uint16_t daySequenceNr_HSDS; // day sequence number 1...365
uint32_t yieldTotal_H19_Wh; // yield total resetable Wh
uint32_t yieldToday_H20_Wh; // yield today Wh
uint16_t maxPowerToday_H21_W; // maximum power today W
uint32_t yieldYesterday_H22_Wh; // yield yesterday Wh
uint16_t maxPowerYesterday_H23_W; // maximum power yesterday W
// these are values communicated through the HEX protocol. the pair's first
// value is the timestamp the respective info was last received. if it is
// zero, the value is deemed invalid. the timestamp is reset if no current
// value could be retrieved.
std::pair<uint32_t, int32_t> MpptTemperatureMilliCelsius;
std::pair<uint32_t, int32_t> SmartBatterySenseTemperatureMilliCelsius;
std::pair<uint32_t, uint32_t> NetworkTotalDcInputPowerMilliWatts;
std::pair<uint32_t, uint8_t> NetworkInfo;
std::pair<uint32_t, uint8_t> NetworkMode;
std::pair<uint32_t, uint8_t> NetworkStatus;
frozen::string const& getMpptAsString() const; // state of mppt as string
frozen::string const& getCsAsString() const; // current state as string
frozen::string const& getErrAsString() const; // error state as string
frozen::string const& getOrAsString() const; // off reason as string
};
struct veShuntStruct : veStruct {
int32_t T; // Battery temperature
bool tempPresent; // Battery temperature sensor is attached to the shunt
int32_t P; // Instantaneous power
int32_t CE; // Consumed Amp Hours
int32_t SOC; // State-of-charge
uint32_t TTG; // Time-to-go
bool ALARM; // Alarm condition active
uint16_t alarmReason_AR; // Alarm Reason
int32_t H1; // Depth of the deepest discharge
int32_t H2; // Depth of the last discharge
int32_t H3; // Depth of the average discharge
int32_t H4; // Number of charge cycles
int32_t H5; // Number of full discharges
int32_t H6; // Cumulative Amp Hours drawn
int32_t H7; // Minimum main (battery) voltage
int32_t H8; // Maximum main (battery) voltage
int32_t H9; // Number of seconds since last full charge
int32_t H10; // Number of automatic synchronizations
int32_t H11; // Number of low main voltage alarms
int32_t H12; // Number of high main voltage alarms
int32_t H13; // Number of low auxiliary voltage alarms
int32_t H14; // Number of high auxiliary voltage alarms
int32_t H15; // Minimum auxiliary (battery) voltage
int32_t H16; // Maximum auxiliary (battery) voltage
int32_t H17; // Amount of discharged energy
int32_t H18; // Amount of charged energy
int8_t dcMonitorMode_MON; // DC monitor mode
};
enum class VeDirectHexCommand : uint8_t {
ENTER_BOOT = 0x0,
PING = 0x1,
RSV1 = 0x2,
APP_VERSION = 0x3,
PRODUCT_ID = 0x4,
RSV2 = 0x5,
RESTART = 0x6,
GET = 0x7,
SET = 0x8,
RSV3 = 0x9,
ASYNC = 0xA,
RSV4 = 0xB,
RSV5 = 0xC,
RSV6 = 0xD,
RSV7 = 0xE,
RSV8 = 0xF
};
enum class VeDirectHexResponse : uint8_t {
DONE = 0x1,
UNKNOWN = 0x3,
ERROR = 0x4,
PING = 0x5,
GET = 0x7,
SET = 0x8,
ASYNC = 0xA
};
enum class VeDirectHexRegister : uint16_t {
DeviceMode = 0x0200,
DeviceState = 0x0201,
RemoteControlUsed = 0x0202,
PanelVoltage = 0xEDBB,
ChargerVoltage = 0xEDD5,
NetworkTotalDcInputPower = 0x2027,
ChargeControllerTemperature = 0xEDDB,
SmartBatterySenseTemperature = 0xEDEC,
NetworkInfo = 0x200D,
NetworkMode = 0x200E,
NetworkStatus = 0x200F,
HistoryTotal = 0x104F,
HistoryMPPTD30 = 0x10BE
};
struct VeDirectHexData {
VeDirectHexResponse rsp; // hex response code
VeDirectHexRegister addr; // register address
uint8_t flags; // flags
uint32_t value; // integer value of register
char text[VE_MAX_HEX_LEN]; // text/string response
frozen::string const& getResponseAsString() const;
frozen::string const& getRegisterAsString() const;
};

View File

@ -0,0 +1,321 @@
/* framehandler.cpp
*
* Arduino library to read from Victron devices using VE.Direct protocol.
* Derived from Victron framehandler reference implementation.
*
* The MIT License
*
* Copyright (c) 2019 Victron Energy BV
* Portions Copyright (C) 2020 Chris Terwilliger
* https://github.com/cterwilliger/VeDirectFrameHandler
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* 2020.05.05 - 0.2 - initial release
* 2020.06.21 - 0.2 - add MIT license, no code changes
* 2020.08.20 - 0.3 - corrected #include reference
* 2024.03.08 - 0.4 - adds the ability to send hex commands and disassemble hex messages
*/
#include <Arduino.h>
#include "VeDirectFrameHandler.h"
// The name of the record that contains the checksum.
static constexpr char checksumTagName[] = "CHECKSUM";
class Silent : public Print {
public:
size_t write(uint8_t c) final { return 0; }
};
static Silent MessageOutputDummy;
template<typename T>
VeDirectFrameHandler<T>::VeDirectFrameHandler() :
_msgOut(&MessageOutputDummy),
_lastUpdate(0),
_state(State::IDLE),
_checksum(0),
_textPointer(0),
_hexSize(0),
_name(""),
_value(""),
_debugIn(0),
_lastByteMillis(0)
{
}
template<typename T>
void VeDirectFrameHandler<T>::init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort)
{
_vedirectSerial = std::make_unique<HardwareSerial>(hwSerialPort);
_vedirectSerial->end(); // make sure the UART will be re-initialized
_vedirectSerial->begin(19200, SERIAL_8N1, rx, tx);
_vedirectSerial->flush();
_canSend = (tx != -1);
_msgOut = msgOut;
_verboseLogging = verboseLogging;
_debugIn = 0;
snprintf(_logId, sizeof(_logId), "[VE.Direct %s %d/%d]", who, rx, tx);
if (_verboseLogging) { _msgOut->printf("%s init complete\r\n", _logId); }
}
template<typename T>
void VeDirectFrameHandler<T>::dumpDebugBuffer() {
_msgOut->printf("%s serial input (%d Bytes):", _logId, _debugIn);
for (int i = 0; i < _debugIn; ++i) {
if (i % 16 == 0) {
_msgOut->printf("\r\n%s", _logId);
}
_msgOut->printf(" %02x", _debugBuffer[i]);
}
_msgOut->println("");
_debugIn = 0;
}
template<typename T>
void VeDirectFrameHandler<T>::reset()
{
_checksum = 0;
_state = State::IDLE;
_textData.clear();
}
template<typename T>
void VeDirectFrameHandler<T>::loop()
{
while ( _vedirectSerial->available()) {
rxData(_vedirectSerial->read());
_lastByteMillis = millis();
}
// there will never be a large gap between two bytes.
// if such a large gap is observed, reset the state machine so it tries
// to decode a new frame / hex messages once more data arrives.
if ((State::IDLE != _state) && ((millis() - _lastByteMillis) > 500)) {
_msgOut->printf("%s Resetting state machine (was %d) after timeout\r\n",
_logId, static_cast<unsigned>(_state));
if (_verboseLogging) { dumpDebugBuffer(); }
reset();
}
}
/*
* rxData
* This function is called by loop() which passes a byte of serial data
* Based on Victron's example code. But using String and Map instead of pointer and arrays
*/
template<typename T>
void VeDirectFrameHandler<T>::rxData(uint8_t inbyte)
{
if (_verboseLogging) {
_debugBuffer[_debugIn] = inbyte;
_debugIn = (_debugIn + 1) % _debugBuffer.size();
if (0 == _debugIn) {
_msgOut->printf("%s ERROR: debug buffer overrun!\r\n", _logId);
}
}
if ( (inbyte == ':') && (_state != State::CHECKSUM) ) {
_prevState = _state; //hex frame can interrupt TEXT
_state = State::RECORD_HEX;
_hexSize = 0;
}
if (_state != State::RECORD_HEX) {
_checksum += inbyte;
}
inbyte = toupper(inbyte);
switch(_state) {
case State::IDLE:
/* wait for \n of the start of an record */
switch(inbyte) {
case '\n':
_state = State::RECORD_BEGIN;
break;
case '\r': /* Skip */
default:
break;
}
break;
case State::RECORD_BEGIN:
_textPointer = _name;
*_textPointer++ = inbyte;
_state = State::RECORD_NAME;
break;
case State::RECORD_NAME:
// The record name is being received, terminated by a \t
switch(inbyte) {
case '\t':
// the Checksum record indicates a EOR
if ( _textPointer < (_name + sizeof(_name)) ) {
*_textPointer = 0; /* Zero terminate */
if (strcmp(_name, checksumTagName) == 0) {
_state = State::CHECKSUM;
break;
}
}
_textPointer = _value; /* Reset value pointer */
_state = State::RECORD_VALUE;
break;
case '#': /* Ignore # from serial number*/
break;
default:
// add byte to name, but do no overflow
if ( _textPointer < (_name + sizeof(_name)) )
*_textPointer++ = inbyte;
break;
}
break;
case State::RECORD_VALUE:
// The record value is being received. The \r indicates a new record.
switch(inbyte) {
case '\n':
if ( _textPointer < (_value + sizeof(_value)) ) {
*_textPointer = 0; // make zero ended
_textData.push_back({_name, _value});
}
_state = State::RECORD_BEGIN;
break;
case '\r': /* Skip */
break;
default:
// add byte to value, but do no overflow
if ( _textPointer < (_value + sizeof(_value)) )
*_textPointer++ = inbyte;
break;
}
break;
case State::CHECKSUM:
{
if (_verboseLogging) { dumpDebugBuffer(); }
if (_checksum == 0) {
for (auto const& event : _textData) {
processTextData(event.first, event.second);
}
_lastUpdate = millis();
frameValidEvent();
}
else {
_msgOut->printf("%s checksum 0x%02x != 0x00, invalid frame\r\n", _logId, _checksum);
}
reset();
break;
}
case State::RECORD_HEX:
_state = hexRxEvent(inbyte);
break;
}
}
/*
* This function is called every time a new name/value is successfully parsed. It writes the values to the temporary buffer.
*/
template<typename T>
void VeDirectFrameHandler<T>::processTextData(std::string const& name, std::string const& value) {
if (_verboseLogging) {
_msgOut->printf("%s Text Data '%s' = '%s'\r\n",
_logId, name.c_str(), value.c_str());
}
if (processTextDataDerived(name, value)) { return; }
if (name == "PID") {
_tmpFrame.productID_PID = strtol(value.c_str(), nullptr, 0);
return;
}
if (name == "SER") {
strcpy(_tmpFrame.serialNr_SER, value.c_str());
return;
}
if (name == "FW") {
strcpy(_tmpFrame.firmwareNr_FW, value.c_str());
return;
}
if (name == "V") {
_tmpFrame.batteryVoltage_V_mV = atol(value.c_str());
return;
}
if (name == "I") {
_tmpFrame.batteryCurrent_I_mA = atol(value.c_str());
return;
}
_msgOut->printf("%s Unknown text data '%s' (value '%s')\r\n",
_logId, name.c_str(), value.c_str());
}
/*
* hexRxEvent
* This function records hex answers or async messages
*/
template<typename T>
typename VeDirectFrameHandler<T>::State VeDirectFrameHandler<T>::hexRxEvent(uint8_t inbyte)
{
State ret = State::RECORD_HEX; // default - continue recording until end of frame
switch (inbyte) {
case '\n':
// now we can analyse the hex message
_hexBuffer[_hexSize] = '\0';
VeDirectHexData data;
if (disassembleHexData(data) && !hexDataHandler(data) && _verboseLogging) {
_msgOut->printf("%s Unhandled Hex %s Response, addr: 0x%04X (%s), "
"value: 0x%08X, flags: 0x%02X\r\n", _logId,
data.getResponseAsString().data(),
static_cast<unsigned>(data.addr),
data.getRegisterAsString().data(),
data.value, data.flags);
}
// restore previous state
ret=_prevState;
break;
default:
_hexBuffer[_hexSize++]=inbyte;
if (_hexSize>=VE_MAX_HEX_LEN) { // oops -buffer overflow - something went wrong, we abort
_msgOut->printf("%s hexRx buffer overflow - aborting read\r\n", _logId);
_hexSize=0;
ret = State::IDLE;
}
}
return ret;
}
template<typename T>
bool VeDirectFrameHandler<T>::isDataValid() const
{
// VE.Direct text frame data is valid if we receive a device serialnumber and
// the data is not older as 10 seconds
// we accept a glitch where the data is valid for ten seconds when serialNr_SER != "" and (millis() - _lastUpdate) overflows
return strlen(_tmpFrame.serialNr_SER) > 0 && (millis() - _lastUpdate) < (10 * 1000);
}
template<typename T>
uint32_t VeDirectFrameHandler<T>::getLastUpdate() const
{
return _lastUpdate;
}

View File

@ -0,0 +1,92 @@
/* frameHandler.h
*
* Arduino library to read from Victron devices using VE.Direct protocol.
* Derived from Victron framehandler reference implementation.
*
* 2020.05.05 - 0.2 - initial release
* 2021.02.23 - 0.3 - change frameLen to 22 per VE.Direct Protocol version 3.30
* 2022.08.20 - 0.4 - changes for OpenDTU
* 2024.03.08 - 0.4 - adds the ability to send hex commands and disassemble hex messages
*
*/
#pragma once
#include <Arduino.h>
#include <array>
#include <memory>
#include <utility>
#include <deque>
#include "VeDirectData.h"
template<typename T>
class VeDirectFrameHandler {
public:
virtual void loop(); // main loop to read ve.direct data
uint32_t getLastUpdate() const; // timestamp of last successful frame read
bool isDataValid() const; // return true if data valid and not outdated
T const& getData() const { return _tmpFrame; }
bool sendHexCommand(VeDirectHexCommand cmd, VeDirectHexRegister addr, uint32_t value = 0, uint8_t valsize = 0);
protected:
VeDirectFrameHandler();
void init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort);
virtual bool hexDataHandler(VeDirectHexData const &data) { return false; } // handles the disassembeled hex response
bool _verboseLogging;
Print* _msgOut;
uint32_t _lastUpdate;
T _tmpFrame;
bool _canSend;
char _logId[32];
private:
void reset();
void dumpDebugBuffer();
void rxData(uint8_t inbyte); // byte of serial data
void processTextData(std::string const& name, std::string const& value);
virtual bool processTextDataDerived(std::string const& name, std::string const& value) = 0;
virtual void frameValidEvent() { }
bool disassembleHexData(VeDirectHexData &data); //return true if disassembling was possible
std::unique_ptr<HardwareSerial> _vedirectSerial;
enum class State {
IDLE = 1,
RECORD_BEGIN = 2,
RECORD_NAME = 3,
RECORD_VALUE = 4,
CHECKSUM = 5,
RECORD_HEX = 6
};
State _state;
State _prevState;
State hexRxEvent(uint8_t inbyte);
uint8_t _checksum; // checksum value
char * _textPointer; // pointer to the private buffer we're writing to, name or value
int _hexSize; // length of hex buffer
char _hexBuffer[VE_MAX_HEX_LEN]; // buffer for received hex frames
char _name[VE_MAX_VALUE_LEN]; // buffer for the field name
char _value[VE_MAX_VALUE_LEN]; // buffer for the field value
std::array<uint8_t, 512> _debugBuffer;
unsigned _debugIn;
uint32_t _lastByteMillis; // time of last parsed byte
/**
* not every frame contains every value the device is communicating, i.e.,
* a set of values can be fragmented across multiple frames. frames can be
* invalid. in order to only process data from valid frames, we add data
* to this queue and only process it once the frame was found to be valid.
* this also handles fragmentation nicely, since there is no need to reset
* our data buffer. we simply update the interpreted data from this event
* queue, which is fine as we know the source frame was valid.
*/
std::deque<std::pair<std::string, std::string>> _textData;
};
template class VeDirectFrameHandler<veMpptStruct>;
template class VeDirectFrameHandler<veShuntStruct>;

View File

@ -0,0 +1,226 @@
/* VeDirectFrame
HexHandler.cpp
*
* Library to read/write from Victron devices using VE.Direct Hex protocol.
* Add on to Victron framehandler reference implementation.
*
* How to use:
* 1. Use sendHexCommand() to send hex messages. Use the Victron documentation to find the parameter.
* 2. The from class "VeDirectFrameHandler" derived class X must overwrite the function
* void VeDirectFrameHandler::hexDataHandler(VeDirectHexData const &data)
* to handle the received hex messages. All hex messages will be forwarted to function hexDataHandler()
* 3. Analyse the content of data (struct VeDirectHexData) to check if a message fits.
*
* 2024.03.08 - 0.4 - adds the ability to send hex commands and to parse hex messages
*
*/
#include <Arduino.h>
#include "VeDirectFrameHandler.h"
/*
* calcHexFrameCheckSum()
* help function to calculate the hex checksum
*/
#define ascii2hex(v) (v-48-(v>='A'?7:0))
#define hex2byte(b) (ascii2hex(*(b)))*16+((ascii2hex(*(b+1))))
static uint8_t calcHexFrameCheckSum(const char* buffer, int size) {
uint8_t checksum=0x55-ascii2hex(buffer[1]);
for (int i=2; i<size; i+=2)
checksum -= hex2byte(buffer+i);
return (checksum);
}
/*
* AsciiHexLE2Int()
* help function to convert AsciiHex Little Endian to uint32_t
* ascii: pointer to Ascii Hex Little Endian data
* anz: 1,2,4 or 8 nibble
*/
static uint32_t AsciiHexLE2Int(const char *ascii, const uint8_t anz) {
char help[9] = {};
// sort from little endian format to normal format
switch (anz) {
case 1:
help[0] = ascii[0];
break;
case 2:
case 4:
case 8:
for (uint8_t i = 0; i < anz; i += 2) {
help[i] = ascii[anz-i-2];
help[i+1] = ascii[anz-i-1];
}
default:
break;
}
return (static_cast<uint32_t>(strtoul(help, nullptr, 16)));
}
/*
* disassembleHexData()
* analysis the hex message and extract: response, address, flags and value/text
* buffer: pointer to message (ascii hex little endian format)
* data: disassembeled message
* return: true = successful disassembeld, false = hex sum fault or message
* do not aligin with VE.Diekt syntax
*/
template<typename T>
bool VeDirectFrameHandler<T>::disassembleHexData(VeDirectHexData &data) {
bool state = false;
char * buffer = _hexBuffer;
auto len = strlen(buffer);
// reset hex data first
data = {};
if ((len > 3) && (calcHexFrameCheckSum(buffer, len) == 0x00)) {
data.rsp = static_cast<VeDirectHexResponse>(AsciiHexLE2Int(buffer+1, 1));
using Response = VeDirectHexResponse;
switch (data.rsp) {
case Response::DONE:
case Response::ERROR:
case Response::PING:
case Response::UNKNOWN:
strncpy(data.text, buffer+2, len-4);
state = true;
break;
case Response::GET:
case Response::SET:
case Response::ASYNC:
data.addr = static_cast<VeDirectHexRegister>(AsciiHexLE2Int(buffer+2, 4));
// future option: Up to now we do not use historical data
if ((data.addr >= VeDirectHexRegister::HistoryTotal) && (data.addr <= VeDirectHexRegister::HistoryMPPTD30)) {
state = true;
break;
}
// future option: to analyse the flags here?
data.flags = AsciiHexLE2Int(buffer+6, 2);
if (len == 12) { // 8bit value
data.value = AsciiHexLE2Int(buffer+8, 2);
state = true;
}
if (len == 14) { // 16bit value
data.value = AsciiHexLE2Int(buffer+8, 4);
state = true;
}
if (len == 18) { // 32bit value
data.value = AsciiHexLE2Int(buffer+8, 8);
state = true;
}
break;
default:
break; // something went wrong
}
}
if (!state)
_msgOut->printf("%s failed to disassemble the hex message: %s\r\n", _logId, buffer);
return (state);
}
/*
* uint2toHexLEString()
* help function to convert up to 32 bits into little endian hex String
* ascii: pointer to Ascii Hex Little Endian data
* anz: 1,2,4 or 8 nibble
*/
static String Int2HexLEString(uint32_t value, uint8_t anz) {
char hexchar[] = "0123456789ABCDEF";
char help[9] = {};
switch (anz) {
case 1:
help[0] = hexchar[(value & 0x0000000F)];
break;
case 2:
case 4:
case 8:
for (uint8_t i = 0; i < anz; i += 2) {
help[i] = hexchar[(value>>((1+1*i)*4)) & 0x0000000F];
help[i+1] = hexchar[(value>>((1*i)*4)) & 0x0000000F];
}
default:
;
}
return String(help);
}
/*
* sendHexCommand()
* send the hex commend after assembling the command string
* cmd: command
* addr: register address, default 0
* value: value to write into a register, default 0
* valsize: size of the value, 8, 16 or 32 bit, default 0
* return: true = message assembeld and send, false = it was not possible to put the message together
* SAMPLE: ping command: sendHexCommand(PING),
* read total DC input power sendHexCommand(GET, 0xEDEC)
* set Charge current limit 10A sendHexCommand(SET, 0x2015, 64, 16)
*
* WARNING: some values are stored in non-volatile memory. Continuous writing, for example from a control loop, will
* lead to early failure.
* On MPPT for example 0xEDE0 - 0xEDFF. Check the Vivtron doc "BlueSolar-HEX-protocol.pdf"
*/
template<typename T>
bool VeDirectFrameHandler<T>::sendHexCommand(VeDirectHexCommand cmd, VeDirectHexRegister addr, uint32_t value, uint8_t valsize) {
bool ret = false;
uint8_t flags = 0x00; // always 0x00
String txData = ":" + Int2HexLEString(static_cast<uint32_t>(cmd), 1); // add the command nibble
using Command = VeDirectHexCommand;
switch (cmd) {
case Command::PING:
case Command::APP_VERSION:
case Command::PRODUCT_ID:
ret = true;
break;
case Command::GET:
case Command::ASYNC:
txData += Int2HexLEString(static_cast<uint16_t>(addr), 4);
txData += Int2HexLEString(flags, 2); // add the flags (2 nibble)
ret = true;
break;
case Command::SET:
txData += Int2HexLEString(static_cast<uint16_t>(addr), 4);
txData += Int2HexLEString(flags, 2); // add the flags (2 nibble)
if ((valsize == 8) || (valsize == 16) || (valsize == 32)) {
txData += Int2HexLEString(value, valsize/4); // add value (2-8 nibble)
ret = true;
}
break;
default:
ret = false;
break;
}
if (ret) {
// add the checksum (2 nibble)
txData += Int2HexLEString(calcHexFrameCheckSum(txData.c_str(), txData.length()), 2);
String send = txData + "\n"; // hex command end byte
_vedirectSerial->write(send.c_str(), send.length());
if (_verboseLogging) {
auto blen = _vedirectSerial->availableForWrite();
_msgOut->printf("%s Sending Hex Command: %s, Free FIFO-Buffer: %u\r\n",
_logId, txData.c_str(), blen);
}
}
if (!ret)
_msgOut->printf("%s send hex command fault: %s\r\n", _logId, txData.c_str());
return (ret);
}

View File

@ -0,0 +1,257 @@
/* VeDirectMpptController.cpp
*
*
* 2020.08.20 - 0.0 - ???
* 2024.03.18 - 0.1 - add of: - temperature from "Smart Battery Sense" connected over VE.Smart network
* - temperature from internal MPPT sensor
* - "total DC input power" from MPPT's connected over VE.Smart network
*/
#include <Arduino.h>
#include "VeDirectMpptController.h"
//#define PROCESS_NETWORK_STATE
void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort)
{
VeDirectFrameHandler::init("MPPT", rx, tx, msgOut, verboseLogging, hwSerialPort);
}
bool VeDirectMpptController::processTextDataDerived(std::string const& name, std::string const& value)
{
if (name == "IL") {
_tmpFrame.loadCurrent_IL_mA = atol(value.c_str());
return true;
}
if (name == "LOAD") {
_tmpFrame.loadOutputState_LOAD = (value == "ON");
return true;
}
if (name == "CS") {
_tmpFrame.currentState_CS = atoi(value.c_str());
return true;
}
if (name == "ERR") {
_tmpFrame.errorCode_ERR = atoi(value.c_str());
return true;
}
if (name == "OR") {
_tmpFrame.offReason_OR = strtol(value.c_str(), nullptr, 0);
return true;
}
if (name == "MPPT") {
_tmpFrame.stateOfTracker_MPPT = atoi(value.c_str());
return true;
}
if (name == "HSDS") {
_tmpFrame.daySequenceNr_HSDS = atoi(value.c_str());
return true;
}
if (name == "VPV") {
_tmpFrame.panelVoltage_VPV_mV = atol(value.c_str());
return true;
}
if (name == "PPV") {
_tmpFrame.panelPower_PPV_W = atoi(value.c_str());
return true;
}
if (name == "H19") {
_tmpFrame.yieldTotal_H19_Wh = atol(value.c_str()) * 10;
return true;
}
if (name == "H20") {
_tmpFrame.yieldToday_H20_Wh = atol(value.c_str()) * 10;
return true;
}
if (name == "H21") {
_tmpFrame.maxPowerToday_H21_W = atoi(value.c_str());
return true;
}
if (name == "H22") {
_tmpFrame.yieldYesterday_H22_Wh = atol(value.c_str()) * 10;
return true;
}
if (name == "H23") {
_tmpFrame.maxPowerYesterday_H23_W = atoi(value.c_str());
return true;
}
return false;
}
/*
* frameValidEvent
* This function is called at the end of the received frame.
*/
void VeDirectMpptController::frameValidEvent() {
// power into the battery, (+) means charging, (-) means discharging
_tmpFrame.batteryOutputPower_W = static_cast<int16_t>((_tmpFrame.batteryVoltage_V_mV / 1000.0f) * (_tmpFrame.batteryCurrent_I_mA / 1000.0f));
// calculation of the panel current
if ((_tmpFrame.panelVoltage_VPV_mV > 0) && (_tmpFrame.panelPower_PPV_W >= 1)) {
_tmpFrame.panelCurrent_mA = static_cast<uint32_t>(_tmpFrame.panelPower_PPV_W * 1000000.0f / _tmpFrame.panelVoltage_VPV_mV);
} else {
_tmpFrame.panelCurrent_mA = 0;
}
// calculation of the MPPT efficiency
float totalPower_W = (_tmpFrame.loadCurrent_IL_mA / 1000.0f + _tmpFrame.batteryCurrent_I_mA / 1000.0f) * _tmpFrame.batteryVoltage_V_mV /1000.0f;
if (_tmpFrame.panelPower_PPV_W > 0) {
_efficiency.addNumber(totalPower_W * 100.0f / _tmpFrame.panelPower_PPV_W);
_tmpFrame.mpptEfficiency_Percent = _efficiency.getAverage();
} else {
_tmpFrame.mpptEfficiency_Percent = 0.0f;
}
if (!_canSend) { return; }
// Copy from the "VE.Direct Protocol" documentation
// For firmware version v1.52 and below, when no VE.Direct queries are sent to the device, the
// charger periodically sends human readable (TEXT) data to the serial port. For firmware
// versions v1.53 and above, the charger always periodically sends TEXT data to the serial port.
// --> We just use hex commandes for firmware >= 1.53 to keep text messages alive
if (atoi(_tmpFrame.firmwareNr_FW) < 153) { return; }
using Command = VeDirectHexCommand;
using Register = VeDirectHexRegister;
sendHexCommand(Command::GET, Register::ChargeControllerTemperature);
sendHexCommand(Command::GET, Register::SmartBatterySenseTemperature);
sendHexCommand(Command::GET, Register::NetworkTotalDcInputPower);
#ifdef PROCESS_NETWORK_STATE
sendHexCommand(Command::GET, Register::NetworkInfo);
sendHexCommand(Command::GET, Register::NetworkMode);
sendHexCommand(Command::GET, Register::NetworkStatus);
#endif // PROCESS_NETWORK_STATE
}
void VeDirectMpptController::loop()
{
VeDirectFrameHandler::loop();
auto resetTimestamp = [this](auto& pair) {
if (pair.first > 0 && (millis() - pair.first) > (10 * 1000)) {
pair.first = 0;
}
};
resetTimestamp(_tmpFrame.MpptTemperatureMilliCelsius);
resetTimestamp(_tmpFrame.SmartBatterySenseTemperatureMilliCelsius);
resetTimestamp(_tmpFrame.NetworkTotalDcInputPowerMilliWatts);
#ifdef PROCESS_NETWORK_STATE
resetTimestamp(_tmpFrame.NetworkInfo);
resetTimestamp(_tmpFrame.NetworkMode);
resetTimestamp(_tmpFrame.NetworkStatus);
#endif // PROCESS_NETWORK_STATE
}
/*
* hexDataHandler()
* analyse the content of VE.Direct hex messages
* Handels the received hex data from the MPPT
*/
bool VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) {
if (data.rsp != VeDirectHexResponse::GET &&
data.rsp != VeDirectHexResponse::ASYNC) { return false; }
auto regLog = static_cast<uint16_t>(data.addr);
switch (data.addr) {
case VeDirectHexRegister::ChargeControllerTemperature:
_tmpFrame.MpptTemperatureMilliCelsius =
{ millis(), static_cast<int32_t>(data.value) * 10 };
if (_verboseLogging) {
_msgOut->printf("%s Hex Data: MPPT Temperature (0x%04X): %.2f°C\r\n",
_logId, regLog,
_tmpFrame.MpptTemperatureMilliCelsius.second / 1000.0);
}
return true;
break;
case VeDirectHexRegister::SmartBatterySenseTemperature:
if (data.value == 0xFFFF) {
if (_verboseLogging) {
_msgOut->printf("%s Hex Data: Smart Battery Sense Temperature is not available\r\n", _logId);
}
return true; // we know what to do with it, and we decided to ignore the value
}
_tmpFrame.SmartBatterySenseTemperatureMilliCelsius =
{ millis(), static_cast<int32_t>(data.value) * 10 - 272150 };
if (_verboseLogging) {
_msgOut->printf("%s Hex Data: Smart Battery Sense Temperature (0x%04X): %.2f°C\r\n",
_logId, regLog,
_tmpFrame.SmartBatterySenseTemperatureMilliCelsius.second / 1000.0);
}
return true;
break;
case VeDirectHexRegister::NetworkTotalDcInputPower:
if (data.value == 0xFFFFFFFF) {
if (_verboseLogging) {
_msgOut->printf("%s Hex Data: Network total DC power value "
"indicates non-networked controller\r\n", _logId);
}
_tmpFrame.NetworkTotalDcInputPowerMilliWatts = { 0, 0 };
return true; // we know what to do with it, and we decided to ignore the value
}
_tmpFrame.NetworkTotalDcInputPowerMilliWatts =
{ millis(), data.value * 10 };
if (_verboseLogging) {
_msgOut->printf("%s Hex Data: Network Total DC Power (0x%04X): %.2fW\r\n",
_logId, regLog,
_tmpFrame.NetworkTotalDcInputPowerMilliWatts.second / 1000.0);
}
return true;
break;
#ifdef PROCESS_NETWORK_STATE
case VeDirectHexRegister::NetworkInfo:
_tmpFrame.NetworkInfo =
{ millis(), static_cast<uint8_t>(data.value) };
if (_verboseLogging) {
_msgOut->printf("%s Hex Data: Network Info (0x%04X): 0x%X\r\n",
_logId, regLog, data.value);
}
return true;
break;
case VeDirectHexRegister::NetworkMode:
_tmpFrame.NetworkMode =
{ millis(), static_cast<uint8_t>(data.value) };
if (_verboseLogging) {
_msgOut->printf("%s Hex Data: Network Mode (0x%04X): 0x%X\r\n",
_logId, regLog, data.value);
}
return true;
break;
case VeDirectHexRegister::NetworkStatus:
_tmpFrame.NetworkStatus =
{ millis(), static_cast<uint8_t>(data.value) };
if (_verboseLogging) {
_msgOut->printf("%s Hex Data: Network Status (0x%04X): 0x%X\r\n",
_logId, regLog, data.value);
}
return true;
break;
#endif // PROCESS_NETWORK_STATE
default:
return false;
break;
}
return false;
}

View File

@ -0,0 +1,54 @@
#pragma once
#include <Arduino.h>
#include "VeDirectData.h"
#include "VeDirectFrameHandler.h"
template<typename T, size_t WINDOW_SIZE>
class MovingAverage {
public:
MovingAverage()
: _sum(0)
, _index(0)
, _count(0) { }
void addNumber(T num) {
if (_count < WINDOW_SIZE) {
_count++;
} else {
_sum -= _window[_index];
}
_window[_index] = num;
_sum += num;
_index = (_index + 1) % WINDOW_SIZE;
}
float getAverage() const {
if (_count == 0) { return 0.0; }
return static_cast<float>(_sum) / _count;
}
private:
std::array<T, WINDOW_SIZE> _window;
T _sum;
size_t _index;
size_t _count;
};
class VeDirectMpptController : public VeDirectFrameHandler<veMpptStruct> {
public:
VeDirectMpptController() = default;
void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort);
using data_t = veMpptStruct;
void loop() final;
private:
bool hexDataHandler(VeDirectHexData const &data) final;
bool processTextDataDerived(std::string const& name, std::string const& value) final;
void frameValidEvent() final;
MovingAverage<float, 5> _efficiency;
};

View File

@ -0,0 +1,125 @@
#include <Arduino.h>
#include "VeDirectShuntController.h"
VeDirectShuntController VeDirectShunt;
void VeDirectShuntController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging)
{
VeDirectFrameHandler::init("SmartShunt", rx, tx, msgOut, verboseLogging,
((ARDUINO_USB_CDC_ON_BOOT != 1)?2:0));
}
bool VeDirectShuntController::processTextDataDerived(std::string const& name, std::string const& value)
{
if (name == "T") {
_tmpFrame.T = atoi(value.c_str());
_tmpFrame.tempPresent = true;
return true;
}
if (name == "P") {
_tmpFrame.P = atoi(value.c_str());
return true;
}
if (name == "CE") {
_tmpFrame.CE = atoi(value.c_str());
return true;
}
if (name == "SOC") {
_tmpFrame.SOC = atoi(value.c_str());
return true;
}
if (name == "TTG") {
_tmpFrame.TTG = atoi(value.c_str());
return true;
}
if (name == "ALARM") {
_tmpFrame.ALARM = (value == "ON");
return true;
}
if (name == "AR") {
_tmpFrame.alarmReason_AR = atoi(value.c_str());
return true;
}
if (name == "H1") {
_tmpFrame.H1 = atoi(value.c_str());
return true;
}
if (name == "H2") {
_tmpFrame.H2 = atoi(value.c_str());
return true;
}
if (name == "H3") {
_tmpFrame.H3 = atoi(value.c_str());
return true;
}
if (name == "H4") {
_tmpFrame.H4 = atoi(value.c_str());
return true;
}
if (name == "H5") {
_tmpFrame.H5 = atoi(value.c_str());
return true;
}
if (name == "H6") {
_tmpFrame.H6 = atoi(value.c_str());
return true;
}
if (name == "H7") {
_tmpFrame.H7 = atoi(value.c_str());
return true;
}
if (name == "H8") {
_tmpFrame.H8 = atoi(value.c_str());
return true;
}
if (name == "H9") {
_tmpFrame.H9 = atoi(value.c_str());
return true;
}
if (name == "H10") {
_tmpFrame.H10 = atoi(value.c_str());
return true;
}
if (name == "H11") {
_tmpFrame.H11 = atoi(value.c_str());
return true;
}
if (name == "H12") {
_tmpFrame.H12 = atoi(value.c_str());
return true;
}
if (name == "H13") {
_tmpFrame.H13 = atoi(value.c_str());
return true;
}
if (name == "H14") {
_tmpFrame.H14 = atoi(value.c_str());
return true;
}
if (name == "H15") {
_tmpFrame.H15 = atoi(value.c_str());
return true;
}
if (name == "H16") {
_tmpFrame.H16 = atoi(value.c_str());
return true;
}
if (name == "H17") {
_tmpFrame.H17 = atoi(value.c_str());
return true;
}
if (name == "H18") {
_tmpFrame.H18 = atoi(value.c_str());
return true;
}
if (name == "BMV") {
// This field contains a textual description of the BMV model,
// for example 602S or 702. It is deprecated, refer to the field PID instead.
return true;
}
if (name == "MON") {
_tmpFrame.dcMonitorMode_MON = static_cast<int8_t>(atoi(value.c_str()));
return true;
}
return false;
}

View File

@ -0,0 +1,19 @@
#pragma once
#include <Arduino.h>
#include "VeDirectData.h"
#include "VeDirectFrameHandler.h"
class VeDirectShuntController : public VeDirectFrameHandler<veShuntStruct> {
public:
VeDirectShuntController() = default;
void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging);
using data_t = veShuntStruct;
private:
bool processTextDataDerived(std::string const& name, std::string const& value) final;
};
extern VeDirectShuntController VeDirectShunt;

View File

@ -15,13 +15,22 @@ if missing_pkgs:
from dulwich import porcelain from dulwich import porcelain
def get_firmware_specifier_build_flag(): def get_firmware_specifier_build_flag():
try: try:
build_version = porcelain.describe('.') # '.' refers to the repository root dir build_version = porcelain.describe('.') # '.' refers to the repository root dir
except: except Exception as err:
print(f"Unexpected {err=}, {type(err)=}")
build_version = "g0000000" build_version = "g0000000"
build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version + "\\\"" try:
print ("Firmware Revision: " + build_version) branch_name = porcelain.active_branch('.').decode('utf-8') # '.' refers to the repository root dir
except Exception as err:
print(f"Unexpected {err=}, {type(err)=}")
branch_name = "master"
build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version + "\\\" "
build_flag += "-D AUTO_GIT_BRANCH=\\\"" + branch_name + "\\\""
print("Firmware Revision: " + build_version)
print("Firmware build on branch: " + branch_name)
return (build_flag) return (build_flag)
env.Append( env.Append(

View File

@ -44,6 +44,9 @@ lib_deps =
olikraus/U8g2 @ ^2.35.17 olikraus/U8g2 @ ^2.35.17
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/coryjfowler/MCP_CAN_lib
plerup/EspSoftwareSerial @ ^8.0.1
https://github.com/dok-net/ghostl @ ^1.0.1
extra_scripts = extra_scripts =
pre:pio-scripts/auto_firmware_version.py pre:pio-scripts/auto_firmware_version.py
@ -153,14 +156,23 @@ build_flags = ${env.build_flags}
[env:d1_mini_esp32] [env:d1_mini_esp32]
board = wemos_d1_mini32 board = wemos_d1_mini32
build_flags = build_flags =
${env.build_flags} ${env.build_flags}
-DHOYMILES_PIN_MISO=19 -DHOYMILES_PIN_MISO=19
-DHOYMILES_PIN_MOSI=23 -DHOYMILES_PIN_MOSI=23
-DHOYMILES_PIN_SCLK=18 -DHOYMILES_PIN_SCLK=18
-DHOYMILES_PIN_IRQ=16 -DHOYMILES_PIN_IRQ=16
-DHOYMILES_PIN_CE=17 -DHOYMILES_PIN_CE=17
-DHOYMILES_PIN_CS=5 -DHOYMILES_PIN_CS=5
-DVICTRON_PIN_TX=21
-DVICTRON_PIN_RX=22
-DPYLONTECH_PIN_RX=27
-DPYLONTECH_PIN_TX=14
-DHUAWEI_PIN_MISO=12
-DHUAWEI_PIN_MOSI=13
-DHUAWEI_PIN_SCLK=26
-DHUAWEI_PIN_IRQ=25
-DHUAWEI_PIN_CS=15
-DHUAWEI_PIN_POWER=33
[env:wt32_eth01] [env:wt32_eth01]
; http://www.wireless-tag.com/portfolio/wt32-eth01/ ; http://www.wireless-tag.com/portfolio/wt32-eth01/

View File

@ -30,5 +30,12 @@
; -DHOYMILES_PIN_IRQ=4 ; -DHOYMILES_PIN_IRQ=4
; -DHOYMILES_PIN_CE=5 ; -DHOYMILES_PIN_CE=5
; -DHOYMILES_PIN_CS=6 ; -DHOYMILES_PIN_CS=6
; -DVICTRON_PIN_TX=21
; -DVICTRON_PIN_RX=22
; -DHUAWEI_PIN_MISO=12
; -DHUAWEI_PIN_MOSI=13
; -DHUAWEI_PIN_SCLK=26
; -DHUAWEI_PIN_IRQ=25
; -DHUAWEI_PIN_CS=15
;monitor_port = /dev/ttyACM0 ;monitor_port = /dev/ttyACM0
;upload_port = /dev/ttyACM0 ;upload_port = /dev/ttyACM0

90
src/Battery.cpp Normal file
View File

@ -0,0 +1,90 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Battery.h"
#include "MessageOutput.h"
#include "PylontechCanReceiver.h"
#include "JkBmsController.h"
#include "VictronSmartShunt.h"
#include "MqttBattery.h"
#include "SerialPortManager.h"
BatteryClass Battery;
std::shared_ptr<BatteryStats const> BatteryClass::getStats() const
{
std::lock_guard<std::mutex> lock(_mutex);
if (!_upProvider) {
static auto sspDummyStats = std::make_shared<BatteryStats>();
return sspDummyStats;
}
return _upProvider->getStats();
}
void BatteryClass::init(Scheduler& scheduler)
{
scheduler.addTask(_loopTask);
_loopTask.setCallback(std::bind(&BatteryClass::loop, this));
_loopTask.setIterations(TASK_FOREVER);
_loopTask.enable();
this->updateSettings();
}
void BatteryClass::updateSettings()
{
std::lock_guard<std::mutex> lock(_mutex);
if (_upProvider) {
_upProvider->deinit();
_upProvider = nullptr;
}
SerialPortManager.invalidateBatteryPort();
CONFIG_T& config = Configuration.get();
if (!config.Battery.Enabled) { return; }
bool verboseLogging = config.Battery.VerboseLogging;
switch (config.Battery.Provider) {
case 0:
_upProvider = std::make_unique<PylontechCanReceiver>();
break;
case 1:
_upProvider = std::make_unique<JkBms::Controller>();
break;
case 2:
_upProvider = std::make_unique<MqttBattery>();
break;
case 3:
_upProvider = std::make_unique<VictronSmartShunt>();
break;
default:
MessageOutput.printf("Unknown battery provider: %d\r\n", config.Battery.Provider);
return;
}
if(_upProvider->usesHwPort2()) {
if (!SerialPortManager.allocateBatteryPort(2)) {
MessageOutput.printf("[Battery] Serial port %d already in use. Initialization aborted!\r\n", 2);
_upProvider = nullptr;
return;
}
}
if (!_upProvider->init(verboseLogging)) {
SerialPortManager.invalidateBatteryPort();
_upProvider = nullptr;
}
}
void BatteryClass::loop()
{
std::lock_guard<std::mutex> lock(_mutex);
if (!_upProvider) { return; }
_upProvider->loop();
_upProvider->getStats()->mqttLoop();
}

439
src/BatteryStats.cpp Normal file
View File

@ -0,0 +1,439 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <vector>
#include <algorithm>
#include "BatteryStats.h"
#include "Configuration.h"
#include "MqttSettings.h"
#include "JkBmsDataPoints.h"
#include "MqttSettings.h"
template<typename T>
static void addLiveViewInSection(JsonVariant& root,
std::string const& section, std::string const& name,
T&& value, std::string const& unit, uint8_t precision)
{
auto jsonValue = root["values"][section][name];
jsonValue["v"] = value;
jsonValue["u"] = unit;
jsonValue["d"] = precision;
}
template<typename T>
static void addLiveViewValue(JsonVariant& root, std::string const& name,
T&& value, std::string const& unit, uint8_t precision)
{
addLiveViewInSection(root, "status", name, value, unit, precision);
}
static void addLiveViewTextInSection(JsonVariant& root,
std::string const& section, std::string const& name, std::string const& text)
{
root["values"][section][name] = text;
}
static void addLiveViewTextValue(JsonVariant& root, std::string const& name,
std::string const& text)
{
addLiveViewTextInSection(root, "status", name, text);
}
static void addLiveViewWarning(JsonVariant& root, std::string const& name,
bool warning)
{
if (!warning) { return; }
root["issues"][name] = 1;
}
static void addLiveViewAlarm(JsonVariant& root, std::string const& name,
bool alarm)
{
if (!alarm) { return; }
root["issues"][name] = 2;
}
bool BatteryStats::updateAvailable(uint32_t since) const
{
if (_lastUpdate == 0) { return false; } // no data at all processed yet
auto constexpr halfOfAllMillis = std::numeric_limits<uint32_t>::max() / 2;
return (_lastUpdate - since) < halfOfAllMillis;
}
void BatteryStats::getLiveViewData(JsonVariant& root) const
{
root["manufacturer"] = _manufacturer;
root["data_age"] = getAgeSeconds();
addLiveViewValue(root, "SoC", _soc, "%", _socPrecision);
addLiveViewValue(root, "voltage", _voltage, "V", 2);
}
void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const
{
BatteryStats::getLiveViewData(root);
// values go into the "Status" card of the web application
addLiveViewValue(root, "chargeVoltage", _chargeVoltage, "V", 1);
addLiveViewValue(root, "chargeCurrentLimitation", _chargeCurrentLimitation, "A", 1);
addLiveViewValue(root, "dischargeCurrentLimitation", _dischargeCurrentLimitation, "A", 1);
addLiveViewValue(root, "stateOfHealth", _stateOfHealth, "%", 0);
addLiveViewValue(root, "current", _current, "A", 1);
addLiveViewValue(root, "temperature", _temperature, "°C", 1);
addLiveViewTextValue(root, "chargeEnabled", (_chargeEnabled?"yes":"no"));
addLiveViewTextValue(root, "dischargeEnabled", (_dischargeEnabled?"yes":"no"));
addLiveViewTextValue(root, "chargeImmediately", (_chargeImmediately?"yes":"no"));
// alarms and warnings go into the "Issues" card of the web application
addLiveViewWarning(root, "highCurrentDischarge", _warningHighCurrentDischarge);
addLiveViewAlarm(root, "overCurrentDischarge", _alarmOverCurrentDischarge);
addLiveViewWarning(root, "highCurrentCharge", _warningHighCurrentCharge);
addLiveViewAlarm(root, "overCurrentCharge", _alarmOverCurrentCharge);
addLiveViewWarning(root, "lowTemperature", _warningLowTemperature);
addLiveViewAlarm(root, "underTemperature", _alarmUnderTemperature);
addLiveViewWarning(root, "highTemperature", _warningHighTemperature);
addLiveViewAlarm(root, "overTemperature", _alarmOverTemperature);
addLiveViewWarning(root, "lowVoltage", _warningLowVoltage);
addLiveViewAlarm(root, "underVoltage", _alarmUnderVoltage);
addLiveViewWarning(root, "highVoltage", _warningHighVoltage);
addLiveViewAlarm(root, "overVoltage", _alarmOverVoltage);
addLiveViewWarning(root, "bmsInternal", _warningBmsInternal);
addLiveViewAlarm(root, "bmsInternal", _alarmBmsInternal);
}
void JkBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const
{
BatteryStats::getLiveViewData(root);
using Label = JkBms::DataPointLabel;
auto oCurrent = _dataPoints.get<Label::BatteryCurrentMilliAmps>();
if (oCurrent.has_value()) {
addLiveViewValue(root, "current",
static_cast<float>(*oCurrent) / 1000, "A", 2);
}
auto oVoltage = _dataPoints.get<Label::BatteryVoltageMilliVolt>();
if (oVoltage.has_value() && oCurrent.has_value()) {
auto current = static_cast<float>(*oCurrent) / 1000;
auto voltage = static_cast<float>(*oVoltage) / 1000;
addLiveViewValue(root, "power", current * voltage , "W", 2);
}
auto oTemperatureBms = _dataPoints.get<Label::BmsTempCelsius>();
if (oTemperatureBms.has_value()) {
addLiveViewValue(root, "bmsTemp", *oTemperatureBms, "°C", 0);
}
// labels BatteryChargeEnabled, BatteryDischargeEnabled, and
// BalancingEnabled refer to the user setting. we want to show the
// actual MOSFETs' state which control whether charging and discharging
// is possible and whether the BMS is currently balancing cells.
auto oStatus = _dataPoints.get<Label::StatusBitmask>();
if (oStatus.has_value()) {
using Bits = JkBms::StatusBits;
auto chargeEnabled = *oStatus & static_cast<uint16_t>(Bits::ChargingActive);
addLiveViewTextValue(root, "chargeEnabled", (chargeEnabled?"yes":"no"));
auto dischargeEnabled = *oStatus & static_cast<uint16_t>(Bits::DischargingActive);
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);
addLiveViewTextInSection(root, "cells", "balancingActive", (balancingActive?"yes":"no"));
}
auto oAlarms = _dataPoints.get<Label::AlarmsBitmask>();
if (oAlarms.has_value()) {
#define ISSUE(t, x) \
auto x = *oAlarms & static_cast<uint16_t>(JkBms::AlarmBits::x); \
addLiveView##t(root, "JkBmsIssue"#x, x > 0);
ISSUE(Warning, LowCapacity);
ISSUE(Alarm, BmsOvertemperature);
ISSUE(Alarm, ChargingOvervoltage);
ISSUE(Alarm, DischargeUndervoltage);
ISSUE(Alarm, BatteryOvertemperature);
ISSUE(Alarm, ChargingOvercurrent);
ISSUE(Alarm, DischargeOvercurrent);
ISSUE(Alarm, CellVoltageDifference);
ISSUE(Alarm, BatteryBoxOvertemperature);
ISSUE(Alarm, BatteryUndertemperature);
ISSUE(Alarm, CellOvervoltage);
ISSUE(Alarm, CellUndervoltage);
ISSUE(Alarm, AProtect);
ISSUE(Alarm, BProtect);
#undef ISSUE
}
}
void BatteryStats::mqttLoop()
{
auto& config = Configuration.get();
if (!MqttSettings.getConnected()
|| (millis() - _lastMqttPublish) < (config.Mqtt.PublishInterval * 1000)) {
return;
}
mqttPublish();
_lastMqttPublish = millis();
}
uint32_t BatteryStats::getMqttFullPublishIntervalMs() const
{
auto& config = Configuration.get();
// this is the default interval, see mqttLoop(). mqttPublish()
// implementations in derived classes may choose to publish some values
// with a lower frequency and hence implement this method with a different
// return value.
return config.Mqtt.PublishInterval * 1000;
}
void BatteryStats::mqttPublish() const
{
MqttSettings.publish("battery/manufacturer", _manufacturer);
MqttSettings.publish("battery/dataAge", String(getAgeSeconds()));
MqttSettings.publish("battery/stateOfCharge", String(_soc));
MqttSettings.publish("battery/voltage", String(_voltage));
}
void PylontechBatteryStats::mqttPublish() const
{
BatteryStats::mqttPublish();
MqttSettings.publish("battery/settings/chargeVoltage", String(_chargeVoltage));
MqttSettings.publish("battery/settings/chargeCurrentLimitation", String(_chargeCurrentLimitation));
MqttSettings.publish("battery/settings/dischargeCurrentLimitation", String(_dischargeCurrentLimitation));
MqttSettings.publish("battery/stateOfHealth", String(_stateOfHealth));
MqttSettings.publish("battery/current", String(_current));
MqttSettings.publish("battery/temperature", String(_temperature));
MqttSettings.publish("battery/alarm/overCurrentDischarge", String(_alarmOverCurrentDischarge));
MqttSettings.publish("battery/alarm/overCurrentCharge", String(_alarmOverCurrentCharge));
MqttSettings.publish("battery/alarm/underTemperature", String(_alarmUnderTemperature));
MqttSettings.publish("battery/alarm/overTemperature", String(_alarmOverTemperature));
MqttSettings.publish("battery/alarm/underVoltage", String(_alarmUnderVoltage));
MqttSettings.publish("battery/alarm/overVoltage", String(_alarmOverVoltage));
MqttSettings.publish("battery/alarm/bmsInternal", String(_alarmBmsInternal));
MqttSettings.publish("battery/warning/highCurrentDischarge", String(_warningHighCurrentDischarge));
MqttSettings.publish("battery/warning/highCurrentCharge", String(_warningHighCurrentCharge));
MqttSettings.publish("battery/warning/lowTemperature", String(_warningLowTemperature));
MqttSettings.publish("battery/warning/highTemperature", String(_warningHighTemperature));
MqttSettings.publish("battery/warning/lowVoltage", String(_warningLowVoltage));
MqttSettings.publish("battery/warning/highVoltage", String(_warningHighVoltage));
MqttSettings.publish("battery/warning/bmsInternal", String(_warningBmsInternal));
MqttSettings.publish("battery/charging/chargeEnabled", String(_chargeEnabled));
MqttSettings.publish("battery/charging/dischargeEnabled", String(_dischargeEnabled));
MqttSettings.publish("battery/charging/chargeImmediately", String(_chargeImmediately));
}
void JkBmsBatteryStats::mqttPublish() const
{
BatteryStats::mqttPublish();
using Label = JkBms::DataPointLabel;
static std::vector<Label> mqttSkip = {
Label::CellsMilliVolt, // complex data format
Label::ModificationPassword, // sensitive data
Label::BatterySoCPercent // already published by base class
// NOTE that voltage is also published by the base class, however, we
// previously published it only from here using the respective topic.
// to avoid a breaking change, we publish the value again using the
// "old" topic.
};
// regularly publish all topics regardless of whether or not their value changed
bool neverFullyPublished = _lastFullMqttPublish == 0;
bool intervalElapsed = _lastFullMqttPublish + getMqttFullPublishIntervalMs() < millis();
bool fullPublish = neverFullyPublished || intervalElapsed;
for (auto iter = _dataPoints.cbegin(); iter != _dataPoints.cend(); ++iter) {
// skip data points that did not change since last published
if (!fullPublish && iter->second.getTimestamp() < _lastMqttPublish) { continue; }
auto skipMatch = std::find(mqttSkip.begin(), mqttSkip.end(), iter->first);
if (skipMatch != mqttSkip.end()) { continue; }
String topic((std::string("battery/") + iter->second.getLabelText()).c_str());
MqttSettings.publish(topic, iter->second.getValueText().c_str());
}
auto oCellVoltages = _dataPoints.get<Label::CellsMilliVolt>();
if (oCellVoltages.has_value() && (fullPublish || _cellVoltageTimestamp > _lastMqttPublish)) {
unsigned idx = 1;
for (auto iter = oCellVoltages->cbegin(); iter != oCellVoltages->cend(); ++iter) {
String topic("battery/Cell");
topic += String(idx);
topic += "MilliVolt";
MqttSettings.publish(topic, String(iter->second));
++idx;
}
MqttSettings.publish("battery/CellMinMilliVolt", String(_cellMinMilliVolt));
MqttSettings.publish("battery/CellAvgMilliVolt", String(_cellAvgMilliVolt));
MqttSettings.publish("battery/CellMaxMilliVolt", String(_cellMaxMilliVolt));
MqttSettings.publish("battery/CellDiffMilliVolt", String(_cellMaxMilliVolt - _cellMinMilliVolt));
}
auto oAlarms = _dataPoints.get<Label::AlarmsBitmask>();
if (oAlarms.has_value()) {
for (auto iter = JkBms::AlarmBitTexts.begin(); iter != JkBms::AlarmBitTexts.end(); ++iter) {
auto bit = iter->first;
String value = (*oAlarms & static_cast<uint16_t>(bit))?"1":"0";
MqttSettings.publish(String("battery/alarms/") + iter->second.data(), value);
}
}
auto oStatus = _dataPoints.get<Label::StatusBitmask>();
if (oStatus.has_value()) {
for (auto iter = JkBms::StatusBitTexts.begin(); iter != JkBms::StatusBitTexts.end(); ++iter) {
auto bit = iter->first;
String value = (*oStatus & static_cast<uint16_t>(bit))?"1":"0";
MqttSettings.publish(String("battery/status/") + iter->second.data(), value);
}
}
_lastMqttPublish = millis();
if (fullPublish) { _lastFullMqttPublish = _lastMqttPublish; }
}
void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)
{
using Label = JkBms::DataPointLabel;
_manufacturer = "JKBMS";
auto oProductId = dp.get<Label::ProductId>();
if (oProductId.has_value()) {
// the first twelve chars are expected to be the "User Private Data"
// setting (see smartphone app). the remainder is expected be the BMS
// name, which can be changed at will using the smartphone app. so
// there is not always a "JK" in this string. if there is, we still cut
// the string there to avoid possible regressions.
_manufacturer = oProductId->substr(12).c_str();
auto pos = oProductId->rfind("JK");
if (pos != std::string::npos) {
_manufacturer = oProductId->substr(pos).c_str();
}
}
auto oSoCValue = dp.get<Label::BatterySoCPercent>();
if (oSoCValue.has_value()) {
auto oSoCDataPoint = dp.getDataPointFor<Label::BatterySoCPercent>();
BatteryStats::setSoC(*oSoCValue, 0/*precision*/,
oSoCDataPoint->getTimestamp());
}
auto oVoltage = dp.get<Label::BatteryVoltageMilliVolt>();
if (oVoltage.has_value()) {
auto oVoltageDataPoint = dp.getDataPointFor<Label::BatteryVoltageMilliVolt>();
BatteryStats::setVoltage(static_cast<float>(*oVoltage) / 1000,
oVoltageDataPoint->getTimestamp());
}
_dataPoints.updateFrom(dp);
auto oCellVoltages = _dataPoints.get<Label::CellsMilliVolt>();
if (oCellVoltages.has_value()) {
for (auto iter = oCellVoltages->cbegin(); iter != oCellVoltages->cend(); ++iter) {
if (iter == oCellVoltages->cbegin()) {
_cellMinMilliVolt = _cellAvgMilliVolt = _cellMaxMilliVolt = iter->second;
continue;
}
_cellMinMilliVolt = std::min(_cellMinMilliVolt, iter->second);
_cellAvgMilliVolt = (_cellAvgMilliVolt + iter->second) / 2;
_cellMaxMilliVolt = std::max(_cellMaxMilliVolt, iter->second);
}
_cellVoltageTimestamp = millis();
}
_lastUpdate = millis();
}
void VictronSmartShuntStats::updateFrom(VeDirectShuntController::data_t const& shuntData) {
BatteryStats::setVoltage(shuntData.batteryVoltage_V_mV / 1000.0, millis());
BatteryStats::setSoC(static_cast<float>(shuntData.SOC) / 10, 1/*precision*/, millis());
_current = static_cast<float>(shuntData.batteryCurrent_I_mA) / 1000;
_modelName = shuntData.getPidAsString().data();
_chargeCycles = shuntData.H4;
_timeToGo = shuntData.TTG / 60;
_chargedEnergy = static_cast<float>(shuntData.H18) / 100;
_dischargedEnergy = static_cast<float>(shuntData.H17) / 100;
_manufacturer = "Victron " + _modelName;
_temperature = shuntData.T;
_tempPresent = shuntData.tempPresent;
_instantaneousPower = shuntData.P;
_consumedAmpHours = static_cast<float>(shuntData.CE) / 1000;
_lastFullCharge = shuntData.H9 / 60;
// shuntData.AR is a bitfield, so we need to check each bit individually
_alarmLowVoltage = shuntData.alarmReason_AR & 1;
_alarmHighVoltage = shuntData.alarmReason_AR & 2;
_alarmLowSOC = shuntData.alarmReason_AR & 4;
_alarmLowTemperature = shuntData.alarmReason_AR & 32;
_alarmHighTemperature = shuntData.alarmReason_AR & 64;
_lastUpdate = VeDirectShunt.getLastUpdate();
}
void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const {
BatteryStats::getLiveViewData(root);
// values go into the "Status" card of the web application
addLiveViewValue(root, "current", _current, "A", 1);
addLiveViewValue(root, "chargeCycles", _chargeCycles, "", 0);
addLiveViewValue(root, "chargedEnergy", _chargedEnergy, "kWh", 2);
addLiveViewValue(root, "dischargedEnergy", _dischargedEnergy, "kWh", 2);
addLiveViewValue(root, "instantaneousPower", _instantaneousPower, "W", 0);
addLiveViewValue(root, "consumedAmpHours", _consumedAmpHours, "Ah", 3);
addLiveViewValue(root, "lastFullCharge", _lastFullCharge, "min", 0);
if (_tempPresent) {
addLiveViewValue(root, "temperature", _temperature, "°C", 0);
}
addLiveViewAlarm(root, "lowVoltage", _alarmLowVoltage);
addLiveViewAlarm(root, "highVoltage", _alarmHighVoltage);
addLiveViewAlarm(root, "lowSOC", _alarmLowSOC);
addLiveViewAlarm(root, "lowTemperature", _alarmLowTemperature);
addLiveViewAlarm(root, "highTemperature", _alarmHighTemperature);
}
void VictronSmartShuntStats::mqttPublish() const {
BatteryStats::mqttPublish();
MqttSettings.publish("battery/current", String(_current));
MqttSettings.publish("battery/chargeCycles", String(_chargeCycles));
MqttSettings.publish("battery/chargedEnergy", String(_chargedEnergy));
MqttSettings.publish("battery/dischargedEnergy", String(_dischargedEnergy));
MqttSettings.publish("battery/instantaneousPower", String(_instantaneousPower));
MqttSettings.publish("battery/consumedAmpHours", String(_consumedAmpHours));
MqttSettings.publish("battery/lastFullCharge", String(_lastFullCharge));
}

View File

@ -56,6 +56,7 @@ bool ConfigurationClass::write()
JsonObject mqtt = doc["mqtt"].to<JsonObject>(); JsonObject mqtt = doc["mqtt"].to<JsonObject>();
mqtt["enabled"] = config.Mqtt.Enabled; mqtt["enabled"] = config.Mqtt.Enabled;
mqtt["verbose_logging"] = config.Mqtt.VerboseLogging;
mqtt["hostname"] = config.Mqtt.Hostname; mqtt["hostname"] = config.Mqtt.Hostname;
mqtt["port"] = config.Mqtt.Port; mqtt["port"] = config.Mqtt.Port;
mqtt["username"] = config.Mqtt.Username; mqtt["username"] = config.Mqtt.Username;
@ -88,6 +89,7 @@ bool ConfigurationClass::write()
JsonObject dtu = doc["dtu"].to<JsonObject>(); JsonObject dtu = doc["dtu"].to<JsonObject>();
dtu["serial"] = config.Dtu.Serial; dtu["serial"] = config.Dtu.Serial;
dtu["poll_interval"] = config.Dtu.PollInterval; dtu["poll_interval"] = config.Dtu.PollInterval;
dtu["verbose_logging"] = config.Dtu.VerboseLogging;
dtu["nrf_pa_level"] = config.Dtu.Nrf.PaLevel; dtu["nrf_pa_level"] = config.Dtu.Nrf.PaLevel;
dtu["cmt_pa_level"] = config.Dtu.Cmt.PaLevel; dtu["cmt_pa_level"] = config.Dtu.Cmt.PaLevel;
dtu["cmt_frequency"] = config.Dtu.Cmt.Frequency; dtu["cmt_frequency"] = config.Dtu.Cmt.Frequency;
@ -139,6 +141,90 @@ bool ConfigurationClass::write()
} }
} }
JsonObject vedirect = doc["vedirect"].to<JsonObject>();
vedirect["enabled"] = config.Vedirect.Enabled;
vedirect["verbose_logging"] = config.Vedirect.VerboseLogging;
vedirect["updates_only"] = config.Vedirect.UpdatesOnly;
JsonObject powermeter = doc["powermeter"].to<JsonObject>();
powermeter["enabled"] = config.PowerMeter.Enabled;
powermeter["verbose_logging"] = config.PowerMeter.VerboseLogging;
powermeter["interval"] = config.PowerMeter.Interval;
powermeter["source"] = config.PowerMeter.Source;
powermeter["mqtt_topic_powermeter_1"] = config.PowerMeter.MqttTopicPowerMeter1;
powermeter["mqtt_topic_powermeter_2"] = config.PowerMeter.MqttTopicPowerMeter2;
powermeter["mqtt_topic_powermeter_3"] = config.PowerMeter.MqttTopicPowerMeter3;
powermeter["sdmbaudrate"] = config.PowerMeter.SdmBaudrate;
powermeter["sdmaddress"] = config.PowerMeter.SdmAddress;
powermeter["http_individual_requests"] = config.PowerMeter.HttpIndividualRequests;
JsonArray powermeter_http_phases = powermeter["http_phases"].to<JsonArray>();
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
JsonObject powermeter_phase = powermeter_http_phases.add<JsonObject>();
powermeter_phase["enabled"] = config.PowerMeter.Http_Phase[i].Enabled;
powermeter_phase["url"] = config.PowerMeter.Http_Phase[i].Url;
powermeter_phase["auth_type"] = config.PowerMeter.Http_Phase[i].AuthType;
powermeter_phase["username"] = config.PowerMeter.Http_Phase[i].Username;
powermeter_phase["password"] = config.PowerMeter.Http_Phase[i].Password;
powermeter_phase["header_key"] = config.PowerMeter.Http_Phase[i].HeaderKey;
powermeter_phase["header_value"] = config.PowerMeter.Http_Phase[i].HeaderValue;
powermeter_phase["timeout"] = config.PowerMeter.Http_Phase[i].Timeout;
powermeter_phase["json_path"] = config.PowerMeter.Http_Phase[i].JsonPath;
powermeter_phase["unit"] = config.PowerMeter.Http_Phase[i].PowerUnit;
powermeter_phase["sign_inverted"] = config.PowerMeter.Http_Phase[i].SignInverted;
}
JsonObject powerlimiter = doc["powerlimiter"].to<JsonObject>();
powerlimiter["enabled"] = config.PowerLimiter.Enabled;
powerlimiter["verbose_logging"] = config.PowerLimiter.VerboseLogging;
powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter.SolarPassThroughEnabled;
powerlimiter["solar_passthrough_losses"] = config.PowerLimiter.SolarPassThroughLosses;
powerlimiter["battery_always_use_at_night"] = config.PowerLimiter.BatteryAlwaysUseAtNight;
powerlimiter["interval"] = config.PowerLimiter.Interval;
powerlimiter["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter;
powerlimiter["is_inverter_solar_powered"] = config.PowerLimiter.IsInverterSolarPowered;
powerlimiter["inverter_id"] = config.PowerLimiter.InverterId;
powerlimiter["inverter_channel_id"] = config.PowerLimiter.InverterChannelId;
powerlimiter["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption;
powerlimiter["target_power_consumption_hysteresis"] = config.PowerLimiter.TargetPowerConsumptionHysteresis;
powerlimiter["lower_power_limit"] = config.PowerLimiter.LowerPowerLimit;
powerlimiter["base_load_limit"] = config.PowerLimiter.BaseLoadLimit;
powerlimiter["upper_power_limit"] = config.PowerLimiter.UpperPowerLimit;
powerlimiter["ignore_soc"] = config.PowerLimiter.IgnoreSoc;
powerlimiter["battery_soc_start_threshold"] = config.PowerLimiter.BatterySocStartThreshold;
powerlimiter["battery_soc_stop_threshold"] = config.PowerLimiter.BatterySocStopThreshold;
powerlimiter["voltage_start_threshold"] = config.PowerLimiter.VoltageStartThreshold;
powerlimiter["voltage_stop_threshold"] = config.PowerLimiter.VoltageStopThreshold;
powerlimiter["voltage_load_correction_factor"] = config.PowerLimiter.VoltageLoadCorrectionFactor;
powerlimiter["inverter_restart_hour"] = config.PowerLimiter.RestartHour;
powerlimiter["full_solar_passthrough_soc"] = config.PowerLimiter.FullSolarPassThroughSoc;
powerlimiter["full_solar_passthrough_start_voltage"] = config.PowerLimiter.FullSolarPassThroughStartVoltage;
powerlimiter["full_solar_passthrough_stop_voltage"] = config.PowerLimiter.FullSolarPassThroughStopVoltage;
JsonObject battery = doc["battery"].to<JsonObject>();
battery["enabled"] = config.Battery.Enabled;
battery["verbose_logging"] = config.Battery.VerboseLogging;
battery["provider"] = config.Battery.Provider;
battery["jkbms_interface"] = config.Battery.JkBmsInterface;
battery["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval;
battery["mqtt_topic"] = config.Battery.MqttSocTopic;
battery["mqtt_voltage_topic"] = config.Battery.MqttVoltageTopic;
JsonObject huawei = doc["huawei"].to<JsonObject>();
huawei["enabled"] = config.Huawei.Enabled;
huawei["verbose_logging"] = config.Huawei.VerboseLogging;
huawei["can_controller_frequency"] = config.Huawei.CAN_Controller_Frequency;
huawei["auto_power_enabled"] = config.Huawei.Auto_Power_Enabled;
huawei["auto_power_batterysoc_limits_enabled"] = config.Huawei.Auto_Power_BatterySoC_Limits_Enabled;
huawei["emergency_charge_enabled"] = config.Huawei.Emergency_Charge_Enabled;
huawei["voltage_limit"] = config.Huawei.Auto_Power_Voltage_Limit;
huawei["enable_voltage_limit"] = config.Huawei.Auto_Power_Enable_Voltage_Limit;
huawei["lower_power_limit"] = config.Huawei.Auto_Power_Lower_Power_Limit;
huawei["upper_power_limit"] = config.Huawei.Auto_Power_Upper_Power_Limit;
huawei["stop_batterysoc_threshold"] = config.Huawei.Auto_Power_Stop_BatterySoC_Threshold;
huawei["target_power_consumption"] = config.Huawei.Auto_Power_Target_Power_Consumption;
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return false; return false;
} }
@ -229,6 +315,7 @@ bool ConfigurationClass::read()
JsonObject mqtt = doc["mqtt"]; JsonObject mqtt = doc["mqtt"];
config.Mqtt.Enabled = mqtt["enabled"] | MQTT_ENABLED; config.Mqtt.Enabled = mqtt["enabled"] | MQTT_ENABLED;
config.Mqtt.VerboseLogging = mqtt["verbose_logging"] | VERBOSE_LOGGING;
strlcpy(config.Mqtt.Hostname, mqtt["hostname"] | MQTT_HOST, sizeof(config.Mqtt.Hostname)); strlcpy(config.Mqtt.Hostname, mqtt["hostname"] | MQTT_HOST, sizeof(config.Mqtt.Hostname));
config.Mqtt.Port = mqtt["port"] | MQTT_PORT; config.Mqtt.Port = mqtt["port"] | MQTT_PORT;
strlcpy(config.Mqtt.Username, mqtt["username"] | MQTT_USER, sizeof(config.Mqtt.Username)); strlcpy(config.Mqtt.Username, mqtt["username"] | MQTT_USER, sizeof(config.Mqtt.Username));
@ -261,6 +348,7 @@ bool ConfigurationClass::read()
JsonObject dtu = doc["dtu"]; JsonObject dtu = doc["dtu"];
config.Dtu.Serial = dtu["serial"] | DTU_SERIAL; config.Dtu.Serial = dtu["serial"] | DTU_SERIAL;
config.Dtu.PollInterval = dtu["poll_interval"] | DTU_POLL_INTERVAL; config.Dtu.PollInterval = dtu["poll_interval"] | DTU_POLL_INTERVAL;
config.Dtu.VerboseLogging = dtu["verbose_logging"] | VERBOSE_LOGGING;
config.Dtu.Nrf.PaLevel = dtu["nrf_pa_level"] | DTU_NRF_PA_LEVEL; config.Dtu.Nrf.PaLevel = dtu["nrf_pa_level"] | DTU_NRF_PA_LEVEL;
config.Dtu.Cmt.PaLevel = dtu["cmt_pa_level"] | DTU_CMT_PA_LEVEL; config.Dtu.Cmt.PaLevel = dtu["cmt_pa_level"] | DTU_CMT_PA_LEVEL;
config.Dtu.Cmt.Frequency = dtu["cmt_frequency"] | DTU_CMT_FREQUENCY; config.Dtu.Cmt.Frequency = dtu["cmt_frequency"] | DTU_CMT_FREQUENCY;
@ -312,6 +400,91 @@ bool ConfigurationClass::read()
} }
} }
JsonObject vedirect = doc["vedirect"];
config.Vedirect.Enabled = vedirect["enabled"] | VEDIRECT_ENABLED;
config.Vedirect.VerboseLogging = vedirect["verbose_logging"] | VEDIRECT_VERBOSE_LOGGING;
config.Vedirect.UpdatesOnly = vedirect["updates_only"] | VEDIRECT_UPDATESONLY;
JsonObject powermeter = doc["powermeter"];
config.PowerMeter.Enabled = powermeter["enabled"] | POWERMETER_ENABLED;
config.PowerMeter.VerboseLogging = powermeter["verbose_logging"] | VERBOSE_LOGGING;
config.PowerMeter.Interval = powermeter["interval"] | POWERMETER_INTERVAL;
config.PowerMeter.Source = powermeter["source"] | POWERMETER_SOURCE;
strlcpy(config.PowerMeter.MqttTopicPowerMeter1, powermeter["mqtt_topic_powermeter_1"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter1));
strlcpy(config.PowerMeter.MqttTopicPowerMeter2, powermeter["mqtt_topic_powermeter_2"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter2));
strlcpy(config.PowerMeter.MqttTopicPowerMeter3, powermeter["mqtt_topic_powermeter_3"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter3));
config.PowerMeter.SdmBaudrate = powermeter["sdmbaudrate"] | POWERMETER_SDMBAUDRATE;
config.PowerMeter.SdmAddress = powermeter["sdmaddress"] | POWERMETER_SDMADDRESS;
config.PowerMeter.HttpIndividualRequests = powermeter["http_individual_requests"] | false;
JsonArray powermeter_http_phases = powermeter["http_phases"];
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
JsonObject powermeter_phase = powermeter_http_phases[i].as<JsonObject>();
config.PowerMeter.Http_Phase[i].Enabled = powermeter_phase["enabled"] | (i == 0);
strlcpy(config.PowerMeter.Http_Phase[i].Url, powermeter_phase["url"] | "", sizeof(config.PowerMeter.Http_Phase[i].Url));
config.PowerMeter.Http_Phase[i].AuthType = powermeter_phase["auth_type"] | PowerMeterHttpConfig::Auth::None;
strlcpy(config.PowerMeter.Http_Phase[i].Username, powermeter_phase["username"] | "", sizeof(config.PowerMeter.Http_Phase[i].Username));
strlcpy(config.PowerMeter.Http_Phase[i].Password, powermeter_phase["password"] | "", sizeof(config.PowerMeter.Http_Phase[i].Password));
strlcpy(config.PowerMeter.Http_Phase[i].HeaderKey, powermeter_phase["header_key"] | "", sizeof(config.PowerMeter.Http_Phase[i].HeaderKey));
strlcpy(config.PowerMeter.Http_Phase[i].HeaderValue, powermeter_phase["header_value"] | "", sizeof(config.PowerMeter.Http_Phase[i].HeaderValue));
config.PowerMeter.Http_Phase[i].Timeout = powermeter_phase["timeout"] | POWERMETER_HTTP_TIMEOUT;
strlcpy(config.PowerMeter.Http_Phase[i].JsonPath, powermeter_phase["json_path"] | "", sizeof(config.PowerMeter.Http_Phase[i].JsonPath));
config.PowerMeter.Http_Phase[i].PowerUnit = powermeter_phase["unit"] | PowerMeterHttpConfig::Unit::Watts;
config.PowerMeter.Http_Phase[i].SignInverted = powermeter_phase["sign_inverted"] | false;
}
JsonObject powerlimiter = doc["powerlimiter"];
config.PowerLimiter.Enabled = powerlimiter["enabled"] | POWERLIMITER_ENABLED;
config.PowerLimiter.VerboseLogging = powerlimiter["verbose_logging"] | VERBOSE_LOGGING;
config.PowerLimiter.SolarPassThroughEnabled = powerlimiter["solar_passtrough_enabled"] | POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED;
config.PowerLimiter.SolarPassThroughLosses = powerlimiter["solar_passthrough_losses"] | powerlimiter["solar_passtrough_losses"] | POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES; // solar_passthrough_losses was previously saved as solar_passtrough_losses. Be nice and also try mistyped key.
config.PowerLimiter.BatteryAlwaysUseAtNight = powerlimiter["battery_always_use_at_night"] | POWERLIMITER_BATTERY_ALWAYS_USE_AT_NIGHT;
if (powerlimiter["battery_drain_strategy"].as<uint8_t>() == 1) { config.PowerLimiter.BatteryAlwaysUseAtNight = true; } // convert legacy setting
config.PowerLimiter.Interval = powerlimiter["interval"] | POWERLIMITER_INTERVAL;
config.PowerLimiter.IsInverterBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER;
config.PowerLimiter.IsInverterSolarPowered = powerlimiter["is_inverter_solar_powered"] | POWERLIMITER_IS_INVERTER_SOLAR_POWERED;
config.PowerLimiter.InverterId = powerlimiter["inverter_id"] | POWERLIMITER_INVERTER_ID;
config.PowerLimiter.InverterChannelId = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID;
config.PowerLimiter.TargetPowerConsumption = powerlimiter["target_power_consumption"] | POWERLIMITER_TARGET_POWER_CONSUMPTION;
config.PowerLimiter.TargetPowerConsumptionHysteresis = powerlimiter["target_power_consumption_hysteresis"] | POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS;
config.PowerLimiter.LowerPowerLimit = powerlimiter["lower_power_limit"] | POWERLIMITER_LOWER_POWER_LIMIT;
config.PowerLimiter.BaseLoadLimit = powerlimiter["base_load_limit"] | POWERLIMITER_BASE_LOAD_LIMIT;
config.PowerLimiter.UpperPowerLimit = powerlimiter["upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT;
config.PowerLimiter.IgnoreSoc = powerlimiter["ignore_soc"] | POWERLIMITER_IGNORE_SOC;
config.PowerLimiter.BatterySocStartThreshold = powerlimiter["battery_soc_start_threshold"] | POWERLIMITER_BATTERY_SOC_START_THRESHOLD;
config.PowerLimiter.BatterySocStopThreshold = powerlimiter["battery_soc_stop_threshold"] | POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD;
config.PowerLimiter.VoltageStartThreshold = powerlimiter["voltage_start_threshold"] | POWERLIMITER_VOLTAGE_START_THRESHOLD;
config.PowerLimiter.VoltageStopThreshold = powerlimiter["voltage_stop_threshold"] | POWERLIMITER_VOLTAGE_STOP_THRESHOLD;
config.PowerLimiter.VoltageLoadCorrectionFactor = powerlimiter["voltage_load_correction_factor"] | POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR;
config.PowerLimiter.RestartHour = powerlimiter["inverter_restart_hour"] | POWERLIMITER_RESTART_HOUR;
config.PowerLimiter.FullSolarPassThroughSoc = powerlimiter["full_solar_passthrough_soc"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_SOC;
config.PowerLimiter.FullSolarPassThroughStartVoltage = powerlimiter["full_solar_passthrough_start_voltage"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_START_VOLTAGE;
config.PowerLimiter.FullSolarPassThroughStopVoltage = powerlimiter["full_solar_passthrough_stop_voltage"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE;
JsonObject battery = doc["battery"];
config.Battery.Enabled = battery["enabled"] | BATTERY_ENABLED;
config.Battery.VerboseLogging = battery["verbose_logging"] | VERBOSE_LOGGING;
config.Battery.Provider = battery["provider"] | BATTERY_PROVIDER;
config.Battery.JkBmsInterface = battery["jkbms_interface"] | BATTERY_JKBMS_INTERFACE;
config.Battery.JkBmsPollingInterval = battery["jkbms_polling_interval"] | BATTERY_JKBMS_POLLING_INTERVAL;
strlcpy(config.Battery.MqttSocTopic, battery["mqtt_topic"] | "", sizeof(config.Battery.MqttSocTopic));
strlcpy(config.Battery.MqttVoltageTopic, battery["mqtt_voltage_topic"] | "", sizeof(config.Battery.MqttVoltageTopic));
JsonObject huawei = doc["huawei"];
config.Huawei.Enabled = huawei["enabled"] | HUAWEI_ENABLED;
config.Huawei.VerboseLogging = huawei["verbose_logging"] | VERBOSE_LOGGING;
config.Huawei.CAN_Controller_Frequency = huawei["can_controller_frequency"] | HUAWEI_CAN_CONTROLLER_FREQUENCY;
config.Huawei.Auto_Power_Enabled = huawei["auto_power_enabled"] | false;
config.Huawei.Auto_Power_BatterySoC_Limits_Enabled = huawei["auto_power_batterysoc_limits_enabled"] | false;
config.Huawei.Emergency_Charge_Enabled = huawei["emergency_charge_enabled"] | false;
config.Huawei.Auto_Power_Voltage_Limit = huawei["voltage_limit"] | HUAWEI_AUTO_POWER_VOLTAGE_LIMIT;
config.Huawei.Auto_Power_Enable_Voltage_Limit = huawei["enable_voltage_limit"] | HUAWEI_AUTO_POWER_ENABLE_VOLTAGE_LIMIT;
config.Huawei.Auto_Power_Lower_Power_Limit = huawei["lower_power_limit"] | HUAWEI_AUTO_POWER_LOWER_POWER_LIMIT;
config.Huawei.Auto_Power_Upper_Power_Limit = huawei["upper_power_limit"] | HUAWEI_AUTO_POWER_UPPER_POWER_LIMIT;
config.Huawei.Auto_Power_Stop_BatterySoC_Threshold = huawei["stop_batterysoc_threshold"] | HUAWEI_AUTO_POWER_STOP_BATTERYSOC_THRESHOLD;
config.Huawei.Auto_Power_Target_Power_Consumption = huawei["target_power_consumption"] | HUAWEI_AUTO_POWER_TARGET_POWER_CONSUMPTION;
f.close(); f.close();
return true; return true;
} }

View File

@ -4,6 +4,8 @@
*/ */
#include "Display_Graphic.h" #include "Display_Graphic.h"
#include "Datastore.h" #include "Datastore.h"
#include "PowerMeter.h"
#include "Configuration.h"
#include <NetworkSettings.h> #include <NetworkSettings.h>
#include <map> #include <map>
#include <time.h> #include <time.h>
@ -33,6 +35,9 @@ static const char* const i18n_offline[] = { "Offline", "Offline", "Offline" };
static const char* const i18n_current_power_w[] = { "%.0f W", "%.0f W", "%.0f W" }; static const char* const i18n_current_power_w[] = { "%.0f W", "%.0f W", "%.0f W" };
static const char* const i18n_current_power_kw[] = { "%.1f kW", "%.1f kW", "%.1f kW" }; static const char* const i18n_current_power_kw[] = { "%.1f kW", "%.1f kW", "%.1f kW" };
static const char* const i18n_meter_power_w[] = { "grid: %.0f W", "Netz: %.0f W", "reseau: %.0f W" };
static const char* const i18n_meter_power_kw[] = { "grid: %.1f kW", "Netz: %.1f kW", "reseau: %.1f kW" };
static const char* const i18n_yield_today_wh[] = { "today: %4.0f Wh", "Heute: %4.0f Wh", "auj.: %4.0f Wh" }; static const char* const i18n_yield_today_wh[] = { "today: %4.0f Wh", "Heute: %4.0f Wh", "auj.: %4.0f Wh" };
static const char* const i18n_yield_today_kwh[] = { "today: %.1f kWh", "Heute: %.1f kWh", "auj.: %.1f kWh" }; static const char* const i18n_yield_today_kwh[] = { "today: %.1f kWh", "Heute: %.1f kWh", "auj.: %.1f kWh" };
@ -273,6 +278,32 @@ void DisplayGraphicClass::loop()
} }
} }
// the IP and time info in the third line use three-second slots. the
// timing for the power meter is chosen such that every third of those
// three-second slots is used to NOT overwrite the total inverter energy.
bool timing = (_mExtra % 9) >= 3;
if (showText && Configuration.get().PowerMeter.Enabled && timing && !displayPowerSave) {
// erase the third line and print the power meter value instead.
// we do it this way to touch as least upstream code as possible
// to make maintenance easier.
setFont(2);
auto lineHeight = _display->getAscent() - _display->getDescent();
auto y = _lineOffsets[2] - _display->getAscent();
_display->setDrawColor(0);
_display->drawBox(0, y, _display->getDisplayWidth(), lineHeight);
_display->setDrawColor(1);
auto acPower = PowerMeter.getPowerTotal(false);
if (acPower > 999) {
snprintf(_fmtText, sizeof(_fmtText), i18n_meter_power_kw[_display_language], (acPower / 1000));
} else {
snprintf(_fmtText, sizeof(_fmtText), i18n_meter_power_w[_display_language], acPower);
}
printText(_fmtText, 2);
}
_display->sendBuffer(); _display->sendBuffer();
_mExtra++; _mExtra++;

357
src/HttpPowerMeter.cpp Normal file
View File

@ -0,0 +1,357 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Configuration.h"
#include "HttpPowerMeter.h"
#include "MessageOutput.h"
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include "mbedtls/sha256.h"
#include <base64.h>
#include <memory>
#include <ESPmDNS.h>
void HttpPowerMeterClass::init()
{
}
float HttpPowerMeterClass::getPower(int8_t phase)
{
if (phase < 1 || phase > POWERMETER_MAX_PHASES) { return 0.0; }
return power[phase - 1];
}
bool HttpPowerMeterClass::updateValues()
{
auto const& config = Configuration.get();
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
auto const& phaseConfig = config.PowerMeter.Http_Phase[i];
if (!phaseConfig.Enabled) {
power[i] = 0.0;
continue;
}
if (i == 0 || config.PowerMeter.HttpIndividualRequests) {
if (!queryPhase(i, phaseConfig)) {
MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d failed.\r\n", i + 1);
MessageOutput.printf("%s\r\n", httpPowerMeterError);
return false;
}
continue;
}
if(!tryGetFloatValueForPhase(i, phaseConfig.JsonPath, phaseConfig.PowerUnit, phaseConfig.SignInverted)) {
MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d (from JSON fetched with Phase 1 config) failed.\r\n", i + 1);
MessageOutput.printf("%s\r\n", httpPowerMeterError);
return false;
}
}
return true;
}
bool HttpPowerMeterClass::queryPhase(int phase, PowerMeterHttpConfig const& config)
{
//hostByName in WiFiGeneric fails to resolve local names. issue described in
//https://github.com/espressif/arduino-esp32/issues/3822
//and in depth analyzed in https://github.com/espressif/esp-idf/issues/2507#issuecomment-761836300
//in conclusion: we cannot rely on httpClient.begin(*wifiClient, url) to resolve IP adresses.
//have to do it manually here. Feels Hacky...
String protocol;
String host;
String uri;
String base64Authorization;
uint16_t port;
extractUrlComponents(config.Url, protocol, host, uri, port, base64Authorization);
IPAddress ipaddr((uint32_t)0);
//first check if "host" is already an IP adress
if (!ipaddr.fromString(host))
{
//"host"" is not an IP address so try to resolve the IP adress
//first try locally via mDNS, then via DNS. WiFiGeneric::hostByName() will spam the console if done the otherway around.
const bool mdnsEnabled = Configuration.get().Mdns.Enabled;
if (!mdnsEnabled) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS, try to enable mDNS in Network Settings"), host.c_str());
//ensure we try resolving via DNS even if mDNS is disabled
if(!WiFiGenericClass::hostByName(host.c_str(), ipaddr)){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str());
}
}
else
{
ipaddr = MDNS.queryHost(host);
if (ipaddr == INADDR_NONE){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via mDNS"), host.c_str());
//when we cannot find local server via mDNS, try resolving via DNS
if(!WiFiGenericClass::hostByName(host.c_str(), ipaddr)){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str());
}
}
}
}
// secureWifiClient MUST be created before HTTPClient
// see discussion: https://github.com/helgeerbe/OpenDTU-OnBattery/issues/381
std::unique_ptr<WiFiClient> wifiClient;
bool https = protocol == "https";
if (https) {
auto secureWifiClient = std::make_unique<WiFiClientSecure>();
secureWifiClient->setInsecure();
wifiClient = std::move(secureWifiClient);
} else {
wifiClient = std::make_unique<WiFiClient>();
}
return httpRequest(phase, *wifiClient, ipaddr.toString(), port, uri, https, config);
}
bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config)
{
if(!httpClient.begin(wifiClient, host, port, uri, https)){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s"), (https ? "https" : "http"), host.c_str());
return false;
}
prepareRequest(config.Timeout, config.HeaderKey, config.HeaderValue);
if (config.AuthType == Auth_t::Digest) {
const char *headers[1] = {"WWW-Authenticate"};
httpClient.collectHeaders(headers, 1);
} else if (config.AuthType == Auth_t::Basic) {
String authString = config.Username;
authString += ":";
authString += config.Password;
String auth = "Basic ";
auth.concat(base64::encode(authString));
httpClient.addHeader("Authorization", auth);
}
int httpCode = httpClient.GET();
if (httpCode == HTTP_CODE_UNAUTHORIZED && config.AuthType == Auth_t::Digest) {
// Handle authentication challenge
if (httpClient.hasHeader("WWW-Authenticate")) {
String authReq = httpClient.header("WWW-Authenticate");
String authorization = getDigestAuth(authReq, String(config.Username), String(config.Password), "GET", String(uri), 1);
httpClient.end();
if(!httpClient.begin(wifiClient, host, port, uri, https)){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s using digest auth"), (https ? "https" : "http"), host.c_str());
return false;
}
prepareRequest(config.Timeout, config.HeaderKey, config.HeaderValue);
httpClient.addHeader("Authorization", authorization);
httpCode = httpClient.GET();
}
}
if (httpCode <= 0) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("HTTP Error %s"), httpClient.errorToString(httpCode).c_str());
return false;
}
if (httpCode != HTTP_CODE_OK) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Bad HTTP code: %d"), httpCode);
return false;
}
httpResponse = httpClient.getString(); // very unfortunate that we cannot parse WifiClient stream directly
httpClient.end();
// TODO(schlimmchen): postpone calling tryGetFloatValueForPhase, as it
// will be called twice for each phase when doing separate requests.
return tryGetFloatValueForPhase(phase, config.JsonPath, config.PowerUnit, config.SignInverted);
}
String HttpPowerMeterClass::extractParam(String& authReq, const String& param, const char delimit) {
int _begin = authReq.indexOf(param);
if (_begin == -1) { return ""; }
return authReq.substring(_begin + param.length(), authReq.indexOf(delimit, _begin + param.length()));
}
String HttpPowerMeterClass::getcNonce(const int len) {
static const char alphanum[] = "0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
String s = "";
for (int i = 0; i < len; ++i) { s += alphanum[rand() % (sizeof(alphanum) - 1)]; }
return s;
}
String HttpPowerMeterClass::getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter) {
// extracting required parameters for RFC 2617 Digest
String realm = extractParam(authReq, "realm=\"", '"');
String nonce = extractParam(authReq, "nonce=\"", '"');
String cNonce = getcNonce(8);
char nc[9];
snprintf(nc, sizeof(nc), "%08x", counter);
//sha256 of the user:realm:password
String ha1 = sha256(username + ":" + realm + ":" + password);
//sha256 of method:uri
String ha2 = sha256(method + ":" + uri);
//sha256 of h1:nonce:nc:cNonce:auth:h2
String response = sha256(ha1 + ":" + nonce + ":" + String(nc) + ":" + cNonce + ":" + "auth" + ":" + ha2);
//Final authorization String;
String authorization = "Digest username=\"";
authorization += username;
authorization += "\", realm=\"";
authorization += realm;
authorization += "\", nonce=\"";
authorization += nonce;
authorization += "\", uri=\"";
authorization += uri;
authorization += "\", cnonce=\"";
authorization += cNonce;
authorization += "\", nc=";
authorization += String(nc);
authorization += ", qop=auth, response=\"";
authorization += response;
authorization += "\", algorithm=SHA-256";
return authorization;
}
bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, String jsonPath, Unit_t unit, bool signInverted)
{
JsonDocument root;
const DeserializationError error = deserializeJson(root, httpResponse);
if (error) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError),
PSTR("[HttpPowerMeter] Unable to parse server response as JSON"));
return false;
}
constexpr char delimiter = '/';
int start = 0;
int end = jsonPath.indexOf(delimiter);
auto value = root.as<JsonVariantConst>();
// NOTE: "Because ArduinoJson implements the Null Object Pattern, it is
// always safe to read the object: if the key doesn't exist, it returns an
// empty value."
while (end != -1) {
String key = jsonPath.substring(start, end);
value = value[key];
start = end + 1;
end = jsonPath.indexOf(delimiter, start);
}
String lastKey = jsonPath.substring(start);
value = value[lastKey];
if (value.isNull()) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError),
PSTR("[HttpPowerMeter] Unable to find a value for phase %i with JSON path \"%s\""),
phase+1, jsonPath.c_str());
return false;
}
// this value is supposed to be in Watts and positive if energy is consumed.
power[phase] = value.as<float>();
switch (unit) {
case Unit_t::MilliWatts:
power[phase] /= 1000;
break;
case Unit_t::KiloWatts:
power[phase] *= 1000;
break;
default:
break;
}
if (signInverted) { power[phase] *= -1; }
return true;
}
//extract url component as done by httpClient::begin(String url, const char* expectedProtocol) https://github.com/espressif/arduino-esp32/blob/da6325dd7e8e152094b19fe63190907f38ef1ff0/libraries/HTTPClient/src/HTTPClient.cpp#L250
bool HttpPowerMeterClass::extractUrlComponents(String url, String& _protocol, String& _host, String& _uri, uint16_t& _port, String& _base64Authorization)
{
// check for : (http: or https:
int index = url.indexOf(':');
if(index < 0) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("failed to parse protocol"));
return false;
}
_protocol = url.substring(0, index);
//initialize port to default values for http or https.
//port will be overwritten below in case port is explicitly defined
_port = (_protocol == "https" ? 443 : 80);
url.remove(0, (index + 3)); // remove http:// or https://
index = url.indexOf('/');
if (index == -1) {
index = url.length();
url += '/';
}
String host = url.substring(0, index);
url.remove(0, index); // remove host part
// get Authorization
index = host.indexOf('@');
if(index >= 0) {
// auth info
String auth = host.substring(0, index);
host.remove(0, index + 1); // remove auth part including @
_base64Authorization = base64::encode(auth);
}
// get port
index = host.indexOf(':');
String the_host;
if(index >= 0) {
the_host = host.substring(0, index); // hostname
host.remove(0, (index + 1)); // remove hostname + :
_port = host.toInt(); // get port
} else {
the_host = host;
}
_host = the_host;
_uri = url;
return true;
}
String HttpPowerMeterClass::sha256(const String& data) {
uint8_t hash[32];
mbedtls_sha256_context ctx;
mbedtls_sha256_init(&ctx);
mbedtls_sha256_starts(&ctx, 0); // select SHA256
mbedtls_sha256_update(&ctx, reinterpret_cast<const unsigned char*>(data.c_str()), data.length());
mbedtls_sha256_finish(&ctx, hash);
mbedtls_sha256_free(&ctx);
char res[sizeof(hash) * 2 + 1];
for (int i = 0; i < sizeof(hash); i++) {
snprintf(res + (i*2), sizeof(res) - (i*2), "%02x", hash[i]);
}
return res;
}
void HttpPowerMeterClass::prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue) {
httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
httpClient.setUserAgent("OpenDTU-OnBattery");
httpClient.setConnectTimeout(timeout);
httpClient.setTimeout(timeout);
httpClient.addHeader("Content-Type", "application/json");
httpClient.addHeader("Accept", "application/json");
if (strlen(httpHeader) > 0) {
httpClient.addHeader(httpHeader, httpValue);
}
}
HttpPowerMeterClass HttpPowerMeter;

524
src/Huawei_can.cpp Normal file
View File

@ -0,0 +1,524 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2023 Malte Schmidt and others
*/
#include "Battery.h"
#include "Huawei_can.h"
#include "MessageOutput.h"
#include "PowerMeter.h"
#include "PowerLimiter.h"
#include "Configuration.h"
#include "Battery.h"
#include <SPI.h>
#include <mcp_can.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <algorithm>
#include <math.h>
HuaweiCanClass HuaweiCan;
HuaweiCanCommClass HuaweiCanComm;
// *******************************************************
// Huawei CAN Communication
// *******************************************************
// Using a C function to avoid static C++ member
void HuaweiCanCommunicationTask(void* parameter) {
for( ;; ) {
HuaweiCanComm.loop();
yield();
}
}
bool HuaweiCanCommClass::init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk,
uint8_t huawei_irq, uint8_t huawei_cs, uint32_t frequency) {
SPI = new SPIClass(HSPI);
SPI->begin(huawei_clk, huawei_miso, huawei_mosi, huawei_cs);
pinMode(huawei_cs, OUTPUT);
digitalWrite(huawei_cs, HIGH);
pinMode(huawei_irq, INPUT_PULLUP);
_huaweiIrq = huawei_irq;
auto mcp_frequency = MCP_8MHZ;
if (16000000UL == frequency) { mcp_frequency = MCP_16MHZ; }
else if (8000000UL != frequency) {
MessageOutput.printf("Huawei CAN: unknown frequency %d Hz, using 8 MHz\r\n", mcp_frequency);
}
_CAN = new MCP_CAN(SPI, huawei_cs);
if (!_CAN->begin(MCP_STDEXT, CAN_125KBPS, mcp_frequency) == CAN_OK) {
return false;
}
const uint32_t myMask = 0xFFFFFFFF; // Look at all incoming bits and...
const uint32_t myFilter = 0x1081407F; // filter for this message only
_CAN->init_Mask(0, 1, myMask);
_CAN->init_Filt(0, 1, myFilter);
_CAN->init_Mask(1, 1, myMask);
// Change to normal mode to allow messages to be transmitted
_CAN->setMode(MCP_NORMAL);
return true;
}
// Public methods need to obtain semaphore
void HuaweiCanCommClass::loop()
{
std::lock_guard<std::mutex> lock(_mutex);
INT32U rxId;
unsigned char len = 0;
unsigned char rxBuf[8];
uint8_t i;
if (!digitalRead(_huaweiIrq)) {
// If CAN_INT pin is low, read receive buffer
_CAN->readMsgBuf(&rxId, &len, rxBuf); // Read data: len = data length, buf = data byte(s)
if((rxId & 0x80000000) == 0x80000000) { // Determine if ID is standard (11 bits) or extended (29 bits)
if ((rxId & 0x1FFFFFFF) == 0x1081407F && len == 8) {
uint32_t value = __bswap32(* reinterpret_cast<uint32_t*> (rxBuf + 4));
// Input power 0x70, Input frequency 0x71, Input current 0x72
// Output power 0x73, Efficiency 0x74, Output Voltage 0x75 and Output Current 0x76
if(rxBuf[1] >= 0x70 && rxBuf[1] <= 0x76 ) {
_recValues[rxBuf[1] - 0x70] = value;
}
// Input voltage
if(rxBuf[1] == 0x78 ) {
_recValues[HUAWEI_INPUT_VOLTAGE_IDX] = value;
}
// Output Temperature
if(rxBuf[1] == 0x7F ) {
_recValues[HUAWEI_OUTPUT_TEMPERATURE_IDX] = value;
}
// Input Temperature 0x80, Output Current 1 0x81 and Output Current 2 0x82
if(rxBuf[1] >= 0x80 && rxBuf[1] <= 0x82 ) {
_recValues[rxBuf[1] - 0x80 + HUAWEI_INPUT_TEMPERATURE_IDX] = value;
}
// This is the last value that is send
if(rxBuf[1] == 0x81) {
_completeUpdateReceived = true;
}
}
}
// Other emitted codes not handled here are: 0x1081407E (Ack), 0x1081807E (Ack Frame), 0x1081D27F (Description), 0x1001117E (Whr meter), 0x100011FE (unclear), 0x108111FE (output enabled), 0x108081FE (unclear). See:
// https://github.com/craigpeacock/Huawei_R4850G2_CAN/blob/main/r4850.c
// https://www.beyondlogic.org/review-huawei-r4850g2-power-supply-53-5vdc-3kw/
}
// Transmit values
for (i = 0; i < HUAWEI_OFFLINE_CURRENT; i++) {
if ( _hasNewTxValue[i] == true) {
uint8_t data[8] = {0x01, i, 0x00, 0x00, 0x00, 0x00, (uint8_t)((_txValues[i] & 0xFF00) >> 8), (uint8_t)(_txValues[i] & 0xFF)};
// Send extended message
byte sndStat = _CAN->sendMsgBuf(0x108180FE, 1, 8, data);
if (sndStat == CAN_OK) {
_hasNewTxValue[i] = false;
} else {
_errorCode |= HUAWEI_ERROR_CODE_TX;
}
}
}
if (_nextRequestMillis < millis()) {
sendRequest();
_nextRequestMillis = millis() + HUAWEI_DATA_REQUEST_INTERVAL_MS;
}
}
uint32_t HuaweiCanCommClass::getParameterValue(uint8_t parameter)
{
std::lock_guard<std::mutex> lock(_mutex);
uint32_t v = 0;
if (parameter < HUAWEI_OUTPUT_CURRENT1_IDX) {
v = _recValues[parameter];
}
return v;
}
bool HuaweiCanCommClass::gotNewRxDataFrame(bool clear)
{
std::lock_guard<std::mutex> lock(_mutex);
bool b = false;
b = _completeUpdateReceived;
if (clear) {
_completeUpdateReceived = false;
}
return b;
}
uint8_t HuaweiCanCommClass::getErrorCode(bool clear)
{
std::lock_guard<std::mutex> lock(_mutex);
uint8_t e = 0;
e = _errorCode;
if (clear) {
_errorCode = 0;
}
return e;
}
void HuaweiCanCommClass::setParameterValue(uint16_t in, uint8_t parameterType)
{
std::lock_guard<std::mutex> lock(_mutex);
if (parameterType < HUAWEI_OFFLINE_CURRENT) {
_txValues[parameterType] = in;
_hasNewTxValue[parameterType] = true;
}
}
// Private methods
// Requests current values from Huawei unit. Response is handled in onReceive
void HuaweiCanCommClass::sendRequest()
{
uint8_t data[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
//Send extended message
byte sndStat = _CAN->sendMsgBuf(0x108040FE, 1, 8, data);
if(sndStat != CAN_OK) {
_errorCode |= HUAWEI_ERROR_CODE_RX;
}
}
// *******************************************************
// Huawei CAN Controller
// *******************************************************
void HuaweiCanClass::init(Scheduler& scheduler, uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power)
{
scheduler.addTask(_loopTask);
_loopTask.setCallback(std::bind(&HuaweiCanClass::loop, this));
_loopTask.setIterations(TASK_FOREVER);
_loopTask.enable();
this->updateSettings(huawei_miso, huawei_mosi, huawei_clk, huawei_irq, huawei_cs, huawei_power);
}
void HuaweiCanClass::updateSettings(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power)
{
if (_initialized) {
return;
}
const CONFIG_T& config = Configuration.get();
if (!config.Huawei.Enabled) {
return;
}
if (!HuaweiCanComm.init(huawei_miso, huawei_mosi, huawei_clk, huawei_irq, huawei_cs, config.Huawei.CAN_Controller_Frequency)) {
MessageOutput.println("[HuaweiCanClass::init] Error Initializing Huawei CAN communication...");
return;
};
pinMode(huawei_power, OUTPUT);
digitalWrite(huawei_power, HIGH);
_huaweiPower = huawei_power;
if (config.Huawei.Auto_Power_Enabled) {
_mode = HUAWEI_MODE_AUTO_INT;
}
xTaskCreate(HuaweiCanCommunicationTask,"HUAWEI_CAN_0",1000,NULL,0,&_HuaweiCanCommunicationTaskHdl);
MessageOutput.println("[HuaweiCanClass::init] MCP2515 Initialized Successfully!");
_initialized = true;
}
RectifierParameters_t * HuaweiCanClass::get()
{
return &_rp;
}
void HuaweiCanClass::processReceivedParameters()
{
_rp.input_power = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_POWER_IDX) / 1024.0;
_rp.input_frequency = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_FREQ_IDX) / 1024.0;
_rp.input_current = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_CURRENT_IDX) / 1024.0;
_rp.output_power = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_POWER_IDX) / 1024.0;
_rp.efficiency = HuaweiCanComm.getParameterValue(HUAWEI_EFFICIENCY_IDX) / 1024.0;
_rp.output_voltage = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_VOLTAGE_IDX) / 1024.0;
_rp.max_output_current = static_cast<float>(HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_CURRENT_MAX_IDX)) / MAX_CURRENT_MULTIPLIER;
_rp.input_voltage = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_VOLTAGE_IDX) / 1024.0;
_rp.output_temp = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_TEMPERATURE_IDX) / 1024.0;
_rp.input_temp = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_TEMPERATURE_IDX) / 1024.0;
_rp.output_current = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_CURRENT_IDX) / 1024.0;
if (HuaweiCanComm.gotNewRxDataFrame(true)) {
_lastUpdateReceivedMillis = millis();
}
}
void HuaweiCanClass::loop()
{
const CONFIG_T& config = Configuration.get();
if (!config.Huawei.Enabled || !_initialized) {
return;
}
bool verboseLogging = config.Huawei.VerboseLogging;
processReceivedParameters();
uint8_t com_error = HuaweiCanComm.getErrorCode(true);
if (com_error & HUAWEI_ERROR_CODE_RX) {
MessageOutput.println("[HuaweiCanClass::loop] Data request error");
}
if (com_error & HUAWEI_ERROR_CODE_TX) {
MessageOutput.println("[HuaweiCanClass::loop] Data set error");
}
// Print updated data
if (HuaweiCanComm.gotNewRxDataFrame(false) && verboseLogging) {
MessageOutput.printf("[HuaweiCanClass::loop] In: %.02fV, %.02fA, %.02fW\n", _rp.input_voltage, _rp.input_current, _rp.input_power);
MessageOutput.printf("[HuaweiCanClass::loop] Out: %.02fV, %.02fA of %.02fA, %.02fW\n", _rp.output_voltage, _rp.output_current, _rp.max_output_current, _rp.output_power);
MessageOutput.printf("[HuaweiCanClass::loop] Eff : %.01f%%, Temp in: %.01fC, Temp out: %.01fC\n", _rp.efficiency * 100, _rp.input_temp, _rp.output_temp);
}
// Internal PSU power pin (slot detect) control
if (_rp.output_current > HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT) {
_outputCurrentOnSinceMillis = millis();
}
if (_outputCurrentOnSinceMillis + HUAWEI_AUTO_MODE_SHUTDOWN_DELAY < millis() &&
(_mode == HUAWEI_MODE_AUTO_EXT || _mode == HUAWEI_MODE_AUTO_INT)) {
digitalWrite(_huaweiPower, 1);
}
if (_mode == HUAWEI_MODE_AUTO_INT || _batteryEmergencyCharging) {
// Set voltage limit in periodic intervals if we're in auto mode or if emergency battery charge is requested.
if ( _nextAutoModePeriodicIntMillis < millis()) {
MessageOutput.printf("[HuaweiCanClass::loop] Periodically setting voltage limit: %f \r\n", config.Huawei.Auto_Power_Voltage_Limit);
_setValue(config.Huawei.Auto_Power_Voltage_Limit, HUAWEI_ONLINE_VOLTAGE);
_nextAutoModePeriodicIntMillis = millis() + 60000;
}
}
// ***********************
// Emergency charge
// ***********************
auto stats = Battery.getStats();
if (config.Huawei.Emergency_Charge_Enabled && stats->getImmediateChargingRequest()) {
_batteryEmergencyCharging = true;
// Set output current
float efficiency = (_rp.efficiency > 0.5 ? _rp.efficiency : 1.0);
float outputCurrent = efficiency * (config.Huawei.Auto_Power_Upper_Power_Limit / _rp.output_voltage);
MessageOutput.printf("[HuaweiCanClass::loop] Emergency Charge Output current %f \r\n", outputCurrent);
_setValue(outputCurrent, HUAWEI_ONLINE_CURRENT);
return;
}
if (_batteryEmergencyCharging && !stats->getImmediateChargingRequest()) {
// Battery request has changed. Set current to 0, wait for PSU to respond and then clear state
_setValue(0, HUAWEI_ONLINE_CURRENT);
if (_rp.output_current < 1) {
_batteryEmergencyCharging = false;
}
return;
}
// ***********************
// Automatic power control
// ***********************
if (_mode == HUAWEI_MODE_AUTO_INT ) {
// Check if we should run automatic power calculation at all.
// We may have set a value recently and still wait for output stabilization
if (_autoModeBlockedTillMillis > millis()) {
return;
}
// Re-enable automatic power control if the output voltage has dropped below threshold
if(_rp.output_voltage < config.Huawei.Auto_Power_Enable_Voltage_Limit ) {
_autoPowerEnabledCounter = 10;
}
// Check if inverter used by the power limiter is active
std::shared_ptr<InverterAbstract> inverter =
Hoymiles.getInverterBySerial(config.PowerLimiter.InverterId);
if (inverter == nullptr && config.PowerLimiter.InverterId < INV_MAX_COUNT) {
// we previously had an index saved as InverterId. fall back to the
// respective positional lookup if InverterId is not a known serial.
inverter = Hoymiles.getInverterByPos(config.PowerLimiter.InverterId);
}
if (inverter != nullptr) {
if(inverter->isProducing()) {
_setValue(0.0, HUAWEI_ONLINE_CURRENT);
// Don't run auto mode for a second now. Otherwise we may send too much over the CAN bus
_autoModeBlockedTillMillis = millis() + 1000;
MessageOutput.printf("[HuaweiCanClass::loop] Inverter is active, disable\r\n");
return;
}
}
if (PowerMeter.getLastPowerMeterUpdate() > _lastPowerMeterUpdateReceivedMillis &&
_autoPowerEnabledCounter > 0) {
// We have received a new PowerMeter value. Also we're _autoPowerEnabled
// So we're good to calculate a new limit
_lastPowerMeterUpdateReceivedMillis = PowerMeter.getLastPowerMeterUpdate();
// Calculate new power limit
float newPowerLimit = -1 * round(PowerMeter.getPowerTotal());
float efficiency = (_rp.efficiency > 0.5 ? _rp.efficiency : 1.0);
// Powerlimit is the requested output power + permissable Grid consumption factoring in the efficiency factor
newPowerLimit += _rp.output_power + config.Huawei.Auto_Power_Target_Power_Consumption / efficiency;
if (verboseLogging){
MessageOutput.printf("[HuaweiCanClass::loop] newPowerLimit: %f, output_power: %f \r\n", newPowerLimit, _rp.output_power);
}
if (config.Battery.Enabled && config.Huawei.Auto_Power_BatterySoC_Limits_Enabled) {
uint8_t _batterySoC = Battery.getStats()->getSoC();
if (_batterySoC >= config.Huawei.Auto_Power_Stop_BatterySoC_Threshold) {
newPowerLimit = 0;
if (verboseLogging) {
MessageOutput.printf("[HuaweiCanClass::loop] Current battery SoC %i reached "
"stop threshold %i, set newPowerLimit to %f \r\n", _batterySoC,
config.Huawei.Auto_Power_Stop_BatterySoC_Threshold, newPowerLimit);
}
}
}
if (newPowerLimit > config.Huawei.Auto_Power_Lower_Power_Limit) {
// Check if the output power has dropped below the lower limit (i.e. the battery is full)
// and if the PSU should be turned off. Also we use a simple counter mechanism here to be able
// to ramp up from zero output power when starting up
if (_rp.output_power < config.Huawei.Auto_Power_Lower_Power_Limit) {
MessageOutput.printf("[HuaweiCanClass::loop] Power and voltage limit reached. Disabling automatic power control .... \r\n");
_autoPowerEnabledCounter--;
if (_autoPowerEnabledCounter == 0) {
_autoPowerEnabled = false;
_setValue(0, HUAWEI_ONLINE_CURRENT);
return;
}
} else {
_autoPowerEnabledCounter = 10;
}
// Limit power to maximum
if (newPowerLimit > config.Huawei.Auto_Power_Upper_Power_Limit) {
newPowerLimit = config.Huawei.Auto_Power_Upper_Power_Limit;
}
// Calculate output current
float calculatedCurrent = efficiency * (newPowerLimit / _rp.output_voltage);
// Limit output current to value requested by BMS
float permissableCurrent = stats->getChargeCurrentLimitation() - (stats->getChargeCurrent() - _rp.output_current); // BMS current limit - current from other sources
float outputCurrent = std::min(calculatedCurrent, permissableCurrent);
outputCurrent= outputCurrent > 0 ? outputCurrent : 0;
if (verboseLogging) {
MessageOutput.printf("[HuaweiCanClass::loop] Setting output current to %.2fA. This is the lower value of calculated %.2fA and BMS permissable %.2fA currents\r\n", outputCurrent, calculatedCurrent, permissableCurrent);
}
_autoPowerEnabled = true;
_setValue(outputCurrent, HUAWEI_ONLINE_CURRENT);
// Don't run auto mode some time to allow for output stabilization after issuing a new value
_autoModeBlockedTillMillis = millis() + 2 * HUAWEI_DATA_REQUEST_INTERVAL_MS;
} else {
// requested PL is below minium. Set current to 0
_autoPowerEnabled = false;
_setValue(0.0, HUAWEI_ONLINE_CURRENT);
}
}
}
}
void HuaweiCanClass::setValue(float in, uint8_t parameterType)
{
if (_mode != HUAWEI_MODE_AUTO_INT) {
_setValue(in, parameterType);
}
}
void HuaweiCanClass::_setValue(float in, uint8_t parameterType)
{
const CONFIG_T& config = Configuration.get();
if (!config.Huawei.Enabled) {
return;
}
uint16_t value;
if (in < 0) {
MessageOutput.printf("[HuaweiCanClass::_setValue] Error: Tried to set voltage/current to negative value %f \r\n", in);
return;
}
// Start PSU if needed
if (in > HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT && parameterType == HUAWEI_ONLINE_CURRENT &&
(_mode == HUAWEI_MODE_AUTO_EXT || _mode == HUAWEI_MODE_AUTO_INT)) {
digitalWrite(_huaweiPower, 0);
_outputCurrentOnSinceMillis = millis();
}
if (parameterType == HUAWEI_OFFLINE_VOLTAGE || parameterType == HUAWEI_ONLINE_VOLTAGE) {
value = in * 1024;
} else if (parameterType == HUAWEI_OFFLINE_CURRENT || parameterType == HUAWEI_ONLINE_CURRENT) {
value = in * MAX_CURRENT_MULTIPLIER;
} else {
return;
}
HuaweiCanComm.setParameterValue(value, parameterType);
}
void HuaweiCanClass::setMode(uint8_t mode) {
const CONFIG_T& config = Configuration.get();
if (!config.Huawei.Enabled) {
return;
}
if(mode == HUAWEI_MODE_OFF) {
digitalWrite(_huaweiPower, 1);
_mode = HUAWEI_MODE_OFF;
}
if(mode == HUAWEI_MODE_ON) {
digitalWrite(_huaweiPower, 0);
_mode = HUAWEI_MODE_ON;
}
if (mode == HUAWEI_MODE_AUTO_INT && !config.Huawei.Auto_Power_Enabled ) {
MessageOutput.println("[HuaweiCanClass::setMode] WARNING: Trying to setmode to internal automatic power control without being enabled in the UI. Ignoring command");
return;
}
if (_mode == HUAWEI_MODE_AUTO_INT && mode != HUAWEI_MODE_AUTO_INT) {
_autoPowerEnabled = false;
_setValue(0, HUAWEI_ONLINE_CURRENT);
}
if(mode == HUAWEI_MODE_AUTO_EXT || mode == HUAWEI_MODE_AUTO_INT) {
_mode = mode;
}
}

View File

@ -68,6 +68,9 @@ void InverterSettingsClass::init(Scheduler& scheduler)
MessageOutput.println(" Setting poll interval... "); MessageOutput.println(" Setting poll interval... ");
Hoymiles.setPollInterval(config.Dtu.PollInterval); Hoymiles.setPollInterval(config.Dtu.PollInterval);
MessageOutput.println(" Setting verbosity... ");
Hoymiles.setVerboseLogging(config.Dtu.VerboseLogging);
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
if (config.Inverter[i].Serial > 0) { if (config.Inverter[i].Serial > 0) {
MessageOutput.print(" Adding inverter: "); MessageOutput.print(" Adding inverter: ");

431
src/JkBmsController.cpp Normal file
View File

@ -0,0 +1,431 @@
#include <Arduino.h>
#include "Configuration.h"
#include "HardwareSerial.h"
#include "PinMapping.h"
#include "MessageOutput.h"
#include "JkBmsDataPoints.h"
#include "JkBmsController.h"
#include <frozen/map.h>
//#define JKBMS_DUMMY_SERIAL
#ifdef JKBMS_DUMMY_SERIAL
class DummySerial {
public:
DummySerial() = default;
void begin(uint32_t, uint32_t, int8_t, int8_t) {
MessageOutput.println("JK BMS Dummy Serial: begin()");
}
void end() { MessageOutput.println("JK BMS Dummy Serial: end()"); }
void flush() { }
bool availableForWrite() const { return true; }
size_t write(const uint8_t *buffer, size_t size) {
MessageOutput.printf("JK BMS Dummy Serial: write(%d Bytes)\r\n", size);
_byte_idx = 0;
_msg_idx = (_msg_idx + 1) % _data.size();
return size;
}
bool available() const {
return _byte_idx < _data[_msg_idx].size();
}
int read() {
if (_byte_idx >= _data[_msg_idx].size()) { return 0; }
return _data[_msg_idx][_byte_idx++];
}
private:
std::vector<std::vector<uint8_t>> const _data =
{
{
0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00,
0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0xfb,
0x02, 0x0c, 0xfb, 0x03, 0x0c, 0xfb, 0x04, 0x0c,
0xfb, 0x05, 0x0c, 0xfb, 0x06, 0x0c, 0xfb, 0x07,
0x0c, 0xfb, 0x08, 0x0c, 0xf7, 0x09, 0x0d, 0x01,
0x0a, 0x0c, 0xf9, 0x0b, 0x0c, 0xfb, 0x0c, 0x0c,
0xfb, 0x0d, 0x0c, 0xfb, 0x0e, 0x0c, 0xf8, 0x0f,
0x0c, 0xf9, 0x10, 0x0c, 0xfb, 0x80, 0x00, 0x1a,
0x81, 0x00, 0x12, 0x82, 0x00, 0x12, 0x83, 0x14,
0xc3, 0x84, 0x83, 0xf4, 0x85, 0x2e, 0x86, 0x02,
0x87, 0x00, 0x15, 0x89, 0x00, 0x00, 0x13, 0x52,
0x8a, 0x00, 0x10, 0x8b, 0x00, 0x00, 0x8c, 0x00,
0x03, 0x8e, 0x16, 0x80, 0x8f, 0x12, 0xc0, 0x90,
0x0e, 0x10, 0x91, 0x0c, 0xda, 0x92, 0x00, 0x05,
0x93, 0x0b, 0xb8, 0x94, 0x0c, 0x80, 0x95, 0x00,
0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98,
0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e,
0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01,
0x9e, 0x00, 0x64, 0x9f, 0x00, 0x50, 0xa0, 0x00,
0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3,
0x00, 0x46, 0xa4, 0x00, 0x46, 0xa5, 0x00, 0x00,
0xa6, 0x00, 0x02, 0xa7, 0xff, 0xec, 0xa8, 0xff,
0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6,
0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae,
0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14,
0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00,
0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65,
0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32,
0x33, 0x30, 0x36, 0xb6, 0x00, 0x01, 0x4a, 0xc3,
0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53,
0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f,
0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba,
0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42,
0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50,
0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00,
0x00, 0x53, 0xbb
},
{
0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00,
0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0xc0,
0x02, 0x0c, 0xc1, 0x03, 0x0c, 0xc0, 0x04, 0x0c,
0xc4, 0x05, 0x0c, 0xc4, 0x06, 0x0c, 0xc2, 0x07,
0x0c, 0xc2, 0x08, 0x0c, 0xc1, 0x09, 0x0c, 0xba,
0x0a, 0x0c, 0xc1, 0x0b, 0x0c, 0xc2, 0x0c, 0x0c,
0xc2, 0x0d, 0x0c, 0xc2, 0x0e, 0x0c, 0xc4, 0x0f,
0x0c, 0xc2, 0x10, 0x0c, 0xc1, 0x80, 0x00, 0x1b,
0x81, 0x00, 0x1b, 0x82, 0x00, 0x1a, 0x83, 0x14,
0x68, 0x84, 0x03, 0x70, 0x85, 0x3c, 0x86, 0x02,
0x87, 0x00, 0x19, 0x89, 0x00, 0x00, 0x16, 0x86,
0x8a, 0x00, 0x10, 0x8b, 0x00, 0x00, 0x8c, 0x00,
0x07, 0x8e, 0x16, 0x80, 0x8f, 0x12, 0xc0, 0x90,
0x0e, 0x10, 0x91, 0x0c, 0xda, 0x92, 0x00, 0x05,
0x93, 0x0b, 0xb8, 0x94, 0x0c, 0x80, 0x95, 0x00,
0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98,
0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e,
0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01,
0x9e, 0x00, 0x64, 0x9f, 0x00, 0x50, 0xa0, 0x00,
0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3,
0x00, 0x46, 0xa4, 0x00, 0x46, 0xa5, 0x00, 0x00,
0xa6, 0x00, 0x02, 0xa7, 0xff, 0xec, 0xa8, 0xff,
0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6,
0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae,
0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14,
0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00,
0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65,
0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32,
0x33, 0x30, 0x36, 0xb6, 0x00, 0x01, 0x7f, 0x2a,
0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53,
0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f,
0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba,
0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42,
0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50,
0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00,
0x00, 0x4f, 0xc1
},
{
0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00,
0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0x13,
0x02, 0x0c, 0x12, 0x03, 0x0c, 0x0f, 0x04, 0x0c,
0x15, 0x05, 0x0c, 0x0d, 0x06, 0x0c, 0x13, 0x07,
0x0c, 0x16, 0x08, 0x0c, 0x13, 0x09, 0x0b, 0xdb,
0x0a, 0x0b, 0xf6, 0x0b, 0x0c, 0x17, 0x0c, 0x0b,
0xf5, 0x0d, 0x0c, 0x16, 0x0e, 0x0c, 0x1a, 0x0f,
0x0c, 0x1b, 0x10, 0x0c, 0x1c, 0x80, 0x00, 0x18,
0x81, 0x00, 0x18, 0x82, 0x00, 0x18, 0x83, 0x13,
0x49, 0x84, 0x00, 0x00, 0x85, 0x00, 0x86, 0x02,
0x87, 0x00, 0x23, 0x89, 0x00, 0x00, 0x20, 0x14,
0x8a, 0x00, 0x10, 0x8b, 0x00, 0x08, 0x8c, 0x00,
0x05, 0x8e, 0x16, 0x80, 0x8f, 0x12, 0xc0, 0x90,
0x0e, 0x10, 0x91, 0x0c, 0xda, 0x92, 0x00, 0x05,
0x93, 0x0b, 0xb8, 0x94, 0x0c, 0x80, 0x95, 0x00,
0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98,
0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e,
0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01,
0x9e, 0x00, 0x64, 0x9f, 0x00, 0x50, 0xa0, 0x00,
0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3,
0x00, 0x46, 0xa4, 0x00, 0x46, 0xa5, 0x00, 0x00,
0xa6, 0x00, 0x02, 0xa7, 0xff, 0xec, 0xa8, 0xff,
0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6,
0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae,
0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14,
0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00,
0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65,
0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32,
0x33, 0x30, 0x36, 0xb6, 0x00, 0x02, 0x17, 0x10,
0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53,
0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f,
0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba,
0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42,
0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50,
0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00,
0x00, 0x45, 0xce
},
{
0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00,
0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0x07,
0x02, 0x0c, 0x0a, 0x03, 0x0c, 0x0b, 0x04, 0x0c,
0x08, 0x05, 0x0c, 0x05, 0x06, 0x0c, 0x0b, 0x07,
0x0c, 0x07, 0x08, 0x0c, 0x0a, 0x09, 0x0c, 0x08,
0x0a, 0x0c, 0x06, 0x0b, 0x0c, 0x0a, 0x0c, 0x0c,
0x05, 0x0d, 0x0c, 0x0a, 0x0e, 0x0c, 0x0a, 0x0f,
0x0c, 0x0a, 0x10, 0x0c, 0x0a, 0x80, 0x00, 0x06,
0x81, 0x00, 0x03, 0x82, 0x00, 0x03, 0x83, 0x13,
0x40, 0x84, 0x00, 0x00, 0x85, 0x29, 0x86, 0x02,
0x87, 0x00, 0x01, 0x89, 0x00, 0x00, 0x01, 0x0a,
0x8a, 0x00, 0x10, 0x8b, 0x02, 0x00, 0x8c, 0x00,
0x02, 0x8e, 0x16, 0x80, 0x8f, 0x10, 0x40, 0x90,
0x0e, 0x10, 0x91, 0x0d, 0xde, 0x92, 0x00, 0x05,
0x93, 0x0a, 0x28, 0x94, 0x0a, 0x5a, 0x95, 0x00,
0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98,
0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e,
0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01,
0x9e, 0x00, 0x5a, 0x9f, 0x00, 0x50, 0xa0, 0x00,
0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3,
0x00, 0x37, 0xa4, 0x00, 0x37, 0xa5, 0x00, 0x03,
0xa6, 0x00, 0x05, 0xa7, 0xff, 0xec, 0xa8, 0xff,
0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6,
0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae,
0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14,
0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00,
0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65,
0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32,
0x33, 0x30, 0x36, 0xb6, 0x00, 0x03, 0xb7, 0x2d,
0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53,
0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f,
0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba,
0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42,
0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50,
0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00,
0x00, 0x41, 0x7b
}
};
size_t _msg_idx = 0;
size_t _byte_idx = 0;
};
DummySerial HwSerial;
#else
HardwareSerial HwSerial((ARDUINO_USB_CDC_ON_BOOT != 1)?2:0);
#endif
namespace JkBms {
bool Controller::init(bool verboseLogging)
{
_verboseLogging = verboseLogging;
std::string ifcType = "transceiver";
if (Interface::Transceiver != getInterface()) { ifcType = "TTL-UART"; }
MessageOutput.printf("[JK BMS] Initialize %s interface...\r\n", ifcType.c_str());
const PinMapping_t& pin = PinMapping.get();
MessageOutput.printf("[JK BMS] rx = %d, rxen = %d, tx = %d, txen = %d\r\n",
pin.battery_rx, pin.battery_rxen, pin.battery_tx, pin.battery_txen);
if (pin.battery_rx < 0 || pin.battery_tx < 0) {
MessageOutput.println("[JK BMS] Invalid RX/TX pin config");
return false;
}
HwSerial.end(); // make sure the UART will be re-initialized
HwSerial.begin(115200, SERIAL_8N1, pin.battery_rx, pin.battery_tx);
HwSerial.flush();
if (Interface::Transceiver != getInterface()) { return true; }
_rxEnablePin = pin.battery_rxen;
_txEnablePin = pin.battery_txen;
if (_rxEnablePin < 0 || _txEnablePin < 0) {
MessageOutput.println("[JK BMS] Invalid transceiver pin config");
return false;
}
pinMode(_rxEnablePin, OUTPUT);
pinMode(_txEnablePin, OUTPUT);
return true;
}
void Controller::deinit()
{
HwSerial.end();
if (_rxEnablePin > 0) { pinMode(_rxEnablePin, INPUT); }
if (_txEnablePin > 0) { pinMode(_txEnablePin, INPUT); }
}
Controller::Interface Controller::getInterface() const
{
CONFIG_T& config = Configuration.get();
if (0x00 == config.Battery.JkBmsInterface) { return Interface::Uart; }
if (0x01 == config.Battery.JkBmsInterface) { return Interface::Transceiver; }
return Interface::Invalid;
}
frozen::string const& Controller::getStatusText(Controller::Status status)
{
static constexpr frozen::string missing = "programmer error: missing status text";
static constexpr frozen::map<Status, frozen::string, 6> texts = {
{ Status::Timeout, "timeout wating for response from BMS" },
{ Status::WaitingForPollInterval, "waiting for poll interval to elapse" },
{ Status::HwSerialNotAvailableForWrite, "UART is not available for writing" },
{ Status::BusyReading, "busy waiting for or reading a message from the BMS" },
{ Status::RequestSent, "request for data sent" },
{ Status::FrameCompleted, "a whole frame was received" }
};
auto iter = texts.find(status);
if (iter == texts.end()) { return missing; }
return iter->second;
}
void Controller::announceStatus(Controller::Status status)
{
if (_lastStatus == status && millis() < _lastStatusPrinted + 10 * 1000) { return; }
MessageOutput.printf("[%11.3f] JK BMS: %s\r\n",
static_cast<double>(millis())/1000, getStatusText(status).data());
_lastStatus = status;
_lastStatusPrinted = millis();
}
void Controller::sendRequest(uint8_t pollInterval)
{
if (ReadState::Idle != _readState) {
return announceStatus(Status::BusyReading);
}
if ((millis() - _lastRequest) < pollInterval * 1000) {
return announceStatus(Status::WaitingForPollInterval);
}
if (!HwSerial.availableForWrite()) {
return announceStatus(Status::HwSerialNotAvailableForWrite);
}
SerialCommand readAll(SerialCommand::Command::ReadAll);
if (Interface::Transceiver == getInterface()) {
digitalWrite(_rxEnablePin, HIGH); // disable reception (of our own data)
digitalWrite(_txEnablePin, HIGH); // enable transmission
}
HwSerial.write(readAll.data(), readAll.size());
if (Interface::Transceiver == getInterface()) {
HwSerial.flush();
digitalWrite(_rxEnablePin, LOW); // enable reception
digitalWrite(_txEnablePin, LOW); // disable transmission (free the bus)
}
_lastRequest = millis();
setReadState(ReadState::WaitingForFrameStart);
return announceStatus(Status::RequestSent);
}
void Controller::loop()
{
CONFIG_T& config = Configuration.get();
uint8_t pollInterval = config.Battery.JkBmsPollingInterval;
while (HwSerial.available()) {
rxData(HwSerial.read());
}
sendRequest(pollInterval);
if (millis() > _lastRequest + 2 * pollInterval * 1000 + 250) {
reset();
return announceStatus(Status::Timeout);
}
}
void Controller::rxData(uint8_t inbyte)
{
_buffer.push_back(inbyte);
switch(_readState) {
case ReadState::Idle: // unsolicited message from BMS
case ReadState::WaitingForFrameStart:
if (inbyte == 0x4E) {
return setReadState(ReadState::FrameStartReceived);
}
break;
case ReadState::FrameStartReceived:
if (inbyte == 0x57) {
return setReadState(ReadState::StartMarkerReceived);
}
break;
case ReadState::StartMarkerReceived:
_frameLength = inbyte << 8 | 0x00;
return setReadState(ReadState::FrameLengthMsbReceived);
break;
case ReadState::FrameLengthMsbReceived:
_frameLength |= inbyte;
_frameLength -= 2; // length field already read
return setReadState(ReadState::ReadingFrame);
break;
case ReadState::ReadingFrame:
_frameLength--;
if (_frameLength == 0) {
return frameComplete();
}
return setReadState(ReadState::ReadingFrame);
break;
}
reset();
}
void Controller::reset()
{
_buffer.clear();
return setReadState(ReadState::Idle);
}
void Controller::frameComplete()
{
announceStatus(Status::FrameCompleted);
if (_verboseLogging) {
double ts = static_cast<double>(millis())/1000;
MessageOutput.printf("[%11.3f] JK BMS: raw data (%d Bytes):",
ts, _buffer.size());
for (size_t ctr = 0; ctr < _buffer.size(); ++ctr) {
if (ctr % 16 == 0) {
MessageOutput.printf("\r\n[%11.3f] JK BMS:", ts);
}
MessageOutput.printf(" %02x", _buffer[ctr]);
}
MessageOutput.println();
}
auto pResponse = std::make_unique<SerialResponse>(std::move(_buffer), _protocolVersion);
if (pResponse->isValid()) {
processDataPoints(pResponse->getDataPoints());
} // if invalid, error message has been produced by SerialResponse c'tor
reset();
}
void Controller::processDataPoints(DataPointContainer const& dataPoints)
{
_stats->updateFrom(dataPoints);
using Label = JkBms::DataPointLabel;
auto oProtocolVersion = dataPoints.get<Label::ProtocolVersion>();
if (oProtocolVersion.has_value()) { _protocolVersion = *oProtocolVersion; }
if (!_verboseLogging) { return; }
auto iter = dataPoints.cbegin();
while ( iter != dataPoints.cend() ) {
MessageOutput.printf("[%11.3f] JK BMS: %s: %s%s\r\n",
static_cast<double>(iter->second.getTimestamp())/1000,
iter->second.getLabelText().c_str(),
iter->second.getValueText().c_str(),
iter->second.getUnitText().c_str());
++iter;
}
}
} /* namespace JkBms */

63
src/JkBmsDataPoints.cpp Normal file
View File

@ -0,0 +1,63 @@
#include <stdio.h>
#include "JkBmsDataPoints.h"
namespace JkBms {
static char conversionBuffer[16];
template<typename T>
std::string dataPointValueToStr(T const& v) {
snprintf(conversionBuffer, sizeof(conversionBuffer), "%d", v);
return conversionBuffer;
}
// explicit instanciations for the above unspecialized implementation
template std::string dataPointValueToStr(int16_t const& v);
template std::string dataPointValueToStr(int32_t const& v);
template std::string dataPointValueToStr(uint8_t const& v);
template std::string dataPointValueToStr(uint16_t const& v);
template std::string dataPointValueToStr(uint32_t const& v);
template<>
std::string dataPointValueToStr(std::string const& v) {
return v;
}
template<>
std::string dataPointValueToStr(bool const& v) {
return v?"yes":"no";
}
template<>
std::string dataPointValueToStr(tCells const& v) {
std::string res;
res.reserve(v.size()*(2+2+1+4)); // separator, index, equal sign, value
res += "(";
std::string sep = "";
for(auto const& mapval : v) {
snprintf(conversionBuffer, sizeof(conversionBuffer), "%s%d=%d",
sep.c_str(), mapval.first, mapval.second);
res += conversionBuffer;
sep = ", ";
}
res += ")";
return std::move(res);
}
void DataPointContainer::updateFrom(DataPointContainer const& source)
{
for (auto iter = source.cbegin(); iter != source.cend(); ++iter) {
auto pos = _dataPoints.find(iter->first);
if (pos != _dataPoints.end()) {
// do not update existing data points with the same value
if (pos->second == iter->second) { continue; }
_dataPoints.erase(pos);
}
_dataPoints.insert(*iter);
}
}
} /* namespace JkBms */

363
src/JkBmsSerialMessage.cpp Normal file
View File

@ -0,0 +1,363 @@
#include <numeric>
#include "JkBmsSerialMessage.h"
#include "MessageOutput.h"
namespace JkBms {
SerialCommand::SerialCommand(SerialCommand::Command cmd)
: SerialMessage(20, 0x00)
{
set(_raw.begin(), startMarker);
set(_raw.begin() + 2, static_cast<uint16_t>(_raw.size() - 2)); // frame length
set(_raw.begin() + 8, static_cast<uint8_t>(cmd));
set(_raw.begin() + 9, static_cast<uint8_t>(Source::Host));
set(_raw.begin() + 10, static_cast<uint8_t>(Type::Command));
set(_raw.end() - 5, endMarker);
updateChecksum();
}
using Label = JkBms::DataPointLabel;
template<Label L> using Traits = DataPointLabelTraits<L>;
SerialResponse::SerialResponse(tData&& raw, uint8_t protocolVersion)
: SerialMessage(std::move(raw))
{
if (!isValid()) { return; }
auto pos = _raw.cbegin() + 11;
auto end = pos + getVariableFieldLength();
while ( pos < end ) {
uint8_t fieldType = *(pos++);
/**
* there seems to be no way to make this more generic. the main reason
* is that a non-constexpr value (fieldType cast as Label) cannot be
* used as a template parameter.
*/
switch(fieldType) {
case 0x79:
{
uint8_t cellAmount = *(pos++) / 3;
std::map<uint8_t, uint16_t> voltages;
for (size_t cellCounter = 0; cellCounter < cellAmount; ++cellCounter) {
uint8_t idx = *(pos++);
auto cellMilliVolt = get<uint16_t>(pos);
voltages[idx] = cellMilliVolt;
}
_dp.add<Label::CellsMilliVolt>(voltages);
break;
}
case 0x80:
_dp.add<Label::BmsTempCelsius>(getTemperature(pos));
break;
case 0x81:
_dp.add<Label::BatteryTempOneCelsius>(getTemperature(pos));
break;
case 0x82:
_dp.add<Label::BatteryTempTwoCelsius>(getTemperature(pos));
break;
case 0x83:
_dp.add<Label::BatteryVoltageMilliVolt>(static_cast<uint32_t>(get<uint16_t>(pos)) * 10);
break;
case 0x84:
processBatteryCurrent(pos, protocolVersion);
break;
case 0x85:
_dp.add<Label::BatterySoCPercent>(get<uint8_t>(pos));
break;
case 0x86:
_dp.add<Label::BatteryTemperatureSensorAmount>(get<uint8_t>(pos));
break;
case 0x87:
_dp.add<Label::BatteryCycles>(get<uint16_t>(pos));
break;
case 0x89:
_dp.add<Label::BatteryCycleCapacity>(get<uint32_t>(pos));
break;
case 0x8a:
_dp.add<Label::BatteryCellAmount>(get<uint16_t>(pos));
break;
case 0x8b:
_dp.add<Label::AlarmsBitmask>(get<uint16_t>(pos));
break;
case 0x8c:
_dp.add<Label::StatusBitmask>(get<uint16_t>(pos));
break;
case 0x8e:
_dp.add<Label::TotalOvervoltageThresholdMilliVolt>(static_cast<uint32_t>(get<uint16_t>(pos)) * 10);
break;
case 0x8f:
_dp.add<Label::TotalUndervoltageThresholdMilliVolt>(static_cast<uint32_t>(get<uint16_t>(pos)) * 10);
break;
case 0x90:
_dp.add<Label::CellOvervoltageThresholdMilliVolt>(get<uint16_t>(pos));
break;
case 0x91:
_dp.add<Label::CellOvervoltageRecoveryMilliVolt>(get<uint16_t>(pos));
break;
case 0x92:
_dp.add<Label::CellOvervoltageProtectionDelaySeconds>(get<uint16_t>(pos));
break;
case 0x93:
_dp.add<Label::CellUndervoltageThresholdMilliVolt>(get<uint16_t>(pos));
break;
case 0x94:
_dp.add<Label::CellUndervoltageRecoveryMilliVolt>(get<uint16_t>(pos));
break;
case 0x95:
_dp.add<Label::CellUndervoltageProtectionDelaySeconds>(get<uint16_t>(pos));
break;
case 0x96:
_dp.add<Label::CellVoltageDiffThresholdMilliVolt>(get<uint16_t>(pos));
break;
case 0x97:
_dp.add<Label::DischargeOvercurrentThresholdAmperes>(get<uint16_t>(pos));
break;
case 0x98:
_dp.add<Label::DischargeOvercurrentDelaySeconds>(get<uint16_t>(pos));
break;
case 0x99:
_dp.add<Label::ChargeOvercurrentThresholdAmps>(get<uint16_t>(pos));
break;
case 0x9a:
_dp.add<Label::ChargeOvercurrentDelaySeconds>(get<uint16_t>(pos));
break;
case 0x9b:
_dp.add<Label::BalanceCellVoltageThresholdMilliVolt>(get<uint16_t>(pos));
break;
case 0x9c:
_dp.add<Label::BalanceVoltageDiffThresholdMilliVolt>(get<uint16_t>(pos));
break;
case 0x9d:
_dp.add<Label::BalancingEnabled>(get<bool>(pos));
break;
case 0x9e:
_dp.add<Label::BmsTempProtectionThresholdCelsius>(get<uint16_t>(pos));
break;
case 0x9f:
_dp.add<Label::BmsTempRecoveryThresholdCelsius>(get<uint16_t>(pos));
break;
case 0xa0:
_dp.add<Label::BatteryTempProtectionThresholdCelsius>(get<uint16_t>(pos));
break;
case 0xa1:
_dp.add<Label::BatteryTempRecoveryThresholdCelsius>(get<uint16_t>(pos));
break;
case 0xa2:
_dp.add<Label::BatteryTempDiffThresholdCelsius>(get<uint16_t>(pos));
break;
case 0xa3:
_dp.add<Label::ChargeHighTempThresholdCelsius>(get<uint16_t>(pos));
break;
case 0xa4:
_dp.add<Label::DischargeHighTempThresholdCelsius>(get<uint16_t>(pos));
break;
case 0xa5:
_dp.add<Label::ChargeLowTempThresholdCelsius>(get<int16_t>(pos));
break;
case 0xa6:
_dp.add<Label::ChargeLowTempRecoveryCelsius>(get<int16_t>(pos));
break;
case 0xa7:
_dp.add<Label::DischargeLowTempThresholdCelsius>(get<int16_t>(pos));
break;
case 0xa8:
_dp.add<Label::DischargeLowTempRecoveryCelsius>(get<int16_t>(pos));
break;
case 0xa9:
_dp.add<Label::CellAmountSetting>(get<uint8_t>(pos));
break;
case 0xaa:
_dp.add<Label::BatteryCapacitySettingAmpHours>(get<uint32_t>(pos));
break;
case 0xab:
_dp.add<Label::BatteryChargeEnabled>(get<bool>(pos));
break;
case 0xac:
_dp.add<Label::BatteryDischargeEnabled>(get<bool>(pos));
break;
case 0xad:
_dp.add<Label::CurrentCalibrationMilliAmps>(get<uint16_t>(pos));
break;
case 0xae:
_dp.add<Label::BmsAddress>(get<uint8_t>(pos));
break;
case 0xaf:
_dp.add<Label::BatteryType>(get<uint8_t>(pos));
break;
case 0xb0:
_dp.add<Label::SleepWaitTime>(get<uint16_t>(pos));
break;
case 0xb1:
_dp.add<Label::LowCapacityAlarmThresholdPercent>(get<uint8_t>(pos));
break;
case 0xb2:
_dp.add<Label::ModificationPassword>(getString(pos, 10));
break;
case 0xb3:
_dp.add<Label::DedicatedChargerSwitch>(getBool(pos));
break;
case 0xb4:
_dp.add<Label::EquipmentId>(getString(pos, 8));
break;
case 0xb5:
_dp.add<Label::DateOfManufacturing >(getString(pos, 4));
break;
case 0xb6:
_dp.add<Label::BmsHourMeterMinutes>(get<uint32_t>(pos));
break;
case 0xb7:
_dp.add<Label::BmsSoftwareVersion>(getString(pos, 15));
break;
case 0xb8:
_dp.add<Label::CurrentCalibration>(getBool(pos));
break;
case 0xb9:
_dp.add<Label::ActualBatteryCapacityAmpHours>(get<uint32_t>(pos));
break;
case 0xba:
_dp.add<Label::ProductId>(getString(pos, 24, true));
break;
case 0xc0:
_dp.add<Label::ProtocolVersion>(get<uint8_t>(pos));
break;
default:
MessageOutput.printf("unknown field type 0x%02x\r\n", fieldType);
break;
}
}
}
/**
* NOTE that this function moves the iterator by the amount of bytes read.
*/
template<typename T, typename It>
T SerialMessage::get(It&& pos) const
{
// add easy-to-understand error message when called with non-const iter,
// as compiler generated error message is hard to understand.
using ItNoRef = typename std::remove_reference<It>::type;
using PtrType = typename std::iterator_traits<ItNoRef>::pointer;
using ValueType = typename std::remove_pointer<PtrType>::type;
static_assert(std::is_const<ValueType>::value, "get() must be called with a const_iterator");
// avoid out-of-bound read
if (std::distance(pos, _raw.cend()) < sizeof(T)) { return 0; }
T res = 0;
for (unsigned i = 0; i < sizeof(T); ++i) {
res |= static_cast<T>(*(pos++)) << (sizeof(T)-1-i)*8;
}
return res;
}
template<typename It>
bool SerialMessage::getBool(It&& pos) const
{
uint8_t raw = get<uint8_t>(pos);
return raw > 0;
}
template<typename It>
int16_t SerialMessage::getTemperature(It&& pos) const
{
uint16_t raw = get<uint16_t>(pos);
if (raw <= 100) { return static_cast<int16_t>(raw); }
return static_cast<int16_t>(raw - 100) * (-1);
}
template<typename It>
std::string SerialMessage::getString(It&& pos, size_t len, bool replaceZeroes) const
{
// avoid out-of-bound read
len = std::min<size_t>(std::distance(pos, _raw.cend()), len);
auto start = pos;
pos += len;
if (replaceZeroes) {
std::vector<uint8_t> copy(start, pos);
for (auto& c : copy) {
if (c == 0) { c = 0x20; } // replace by ASCII space
}
return std::string(copy.cbegin(), copy.cend());
}
return std::string(start, pos);
}
void SerialMessage::processBatteryCurrent(SerialMessage::tData::const_iterator& pos, uint8_t protocolVersion)
{
uint16_t raw = get<uint16_t>(pos);
if (0x00 == protocolVersion) {
// untested!
_dp.add<Label::BatteryCurrentMilliAmps>((static_cast<int32_t>(10000) - raw) * 10);
return;
}
else if (0x01 == protocolVersion) {
bool charging = (raw & 0x8000) > 0;
_dp.add<Label::BatteryCurrentMilliAmps>(static_cast<int32_t>(raw & 0x7FFF) * (charging ? 10 : -10));
return;
}
MessageOutput.println("cannot decode battery current field without knowing the protocol version");
}
template<typename T>
void SerialMessage::set(tData::iterator const& pos, T val)
{
// avoid out-of-bound write
if (std::distance(pos, _raw.end()) < sizeof(T)) { return; }
for (unsigned i = 0; i < sizeof(T); ++i) {
*(pos+i) = static_cast<uint8_t>(val >> (sizeof(T)-1-i)*8);
}
}
uint16_t SerialMessage::calcChecksum() const
{
return std::accumulate(_raw.cbegin(), _raw.cend()-4, 0);
}
void SerialMessage::updateChecksum()
{
set(_raw.end()-2, calcChecksum());
}
bool SerialMessage::isValid() const {
uint16_t const actualStartMarker = get<uint16_t>(_raw.cbegin());
if (actualStartMarker != startMarker) {
MessageOutput.printf("JkBms::SerialMessage: invalid start marker %04x, expected 0x%04x\r\n",
actualStartMarker, startMarker);
return false;
}
uint16_t const frameLength = get<uint16_t>(_raw.cbegin()+2);
if (frameLength != _raw.size() - 2) {
MessageOutput.printf("JkBms::SerialMessage: unexpected frame length %04x, expected 0x%04x\r\n",
frameLength, _raw.size() - 2);
return false;
}
uint8_t const actualEndMarker = *(_raw.cend()-5);
if (actualEndMarker != endMarker) {
MessageOutput.printf("JkBms::SerialMessage: invalid end marker %02x, expected 0x%02x\r\n",
actualEndMarker, endMarker);
return false;
}
uint16_t const actualChecksum = get<uint16_t>(_raw.cend()-2);
uint16_t const expectedChecksum = calcChecksum();
if (actualChecksum != expectedChecksum) {
MessageOutput.printf("JkBms::SerialMessage: invalid checksum 0x%04x, expected 0x%04x\r\n",
actualChecksum, expectedChecksum);
return false;
}
return true;
}
} /* namespace JkBms */

View File

@ -2,10 +2,9 @@
/* /*
* Copyright (C) 2022-2024 Thomas Basler and others * Copyright (C) 2022-2024 Thomas Basler and others
*/ */
#include <HardwareSerial.h>
#include "MessageOutput.h" #include "MessageOutput.h"
#include <Arduino.h>
MessageOutputClass MessageOutput; MessageOutputClass MessageOutput;
MessageOutputClass::MessageOutputClass() MessageOutputClass::MessageOutputClass()
@ -21,46 +20,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();
} }
} }

114
src/MqttBattery.cpp Normal file
View File

@ -0,0 +1,114 @@
#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.MqttSocTopic;
if (!_socTopic.isEmpty()) {
MqttSettings.subscribe(_socTopic, 0/*QoS*/,
std::bind(&MqttBattery::onMqttMessageSoC,
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' for SoC readings\r\n",
_socTopic.c_str());
}
}
_voltageTopic = config.Battery.MqttVoltageTopic;
if (!_voltageTopic.isEmpty()) {
MqttSettings.subscribe(_voltageTopic, 0/*QoS*/,
std::bind(&MqttBattery::onMqttMessageVoltage,
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' for voltage readings\r\n",
_voltageTopic.c_str());
}
}
return true;
}
void MqttBattery::deinit()
{
if (!_voltageTopic.isEmpty()) {
MqttSettings.unsubscribe(_voltageTopic);
}
if (!_socTopic.isEmpty()) {
MqttSettings.unsubscribe(_socTopic);
}
}
std::optional<float> MqttBattery::getFloat(std::string const& src, char const* topic) {
float res = 0;
try {
res = std::stof(src);
}
catch(std::invalid_argument const& e) {
MessageOutput.printf("MqttBattery: Cannot parse payload '%s' in topic '%s' as float\r\n",
src.c_str(), topic);
return std::nullopt;
}
return res;
}
void MqttBattery::onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties,
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total)
{
auto soc = getFloat(std::string(reinterpret_cast<const char*>(payload), len), topic);
if (!soc.has_value()) { return; }
if (*soc < 0 || *soc > 100) {
MessageOutput.printf("MqttBattery: Implausible SoC '%.2f' in topic '%s'\r\n",
*soc, topic);
return;
}
_stats->setSoC(*soc, 0/*precision*/, millis());
if (_verboseLogging) {
MessageOutput.printf("MqttBattery: Updated SoC to %d from '%s'\r\n",
static_cast<uint8_t>(*soc), topic);
}
}
void MqttBattery::onMqttMessageVoltage(espMqttClientTypes::MessageProperties const& properties,
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total)
{
auto voltage = getFloat(std::string(reinterpret_cast<const char*>(payload), len), topic);
if (!voltage.has_value()) { return; }
// since this project is revolving around Hoymiles microinverters, which can
// only handle up to 65V of input voltage at best, it is safe to assume that
// an even higher voltage is implausible.
if (*voltage < 0 || *voltage > 65) {
MessageOutput.printf("MqttBattery: Implausible voltage '%.2f' in topic '%s'\r\n",
*voltage, topic);
return;
}
_stats->setVoltage(*voltage, millis());
if (_verboseLogging) {
MessageOutput.printf("MqttBattery: Updated voltage to %.2f from '%s'\r\n",
*voltage, topic);
}
}

View File

@ -0,0 +1,225 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Thomas Basler and others
*/
#include "MqttHandleVedirectHass.h"
#include "Configuration.h"
#include "MqttSettings.h"
#include "NetworkSettings.h"
#include "MessageOutput.h"
#include "VictronMppt.h"
#include "Utils.h"
MqttHandleVedirectHassClass MqttHandleVedirectHass;
void MqttHandleVedirectHassClass::init(Scheduler& scheduler)
{
scheduler.addTask(_loopTask);
_loopTask.setCallback([this] { loop(); });
_loopTask.setIterations(TASK_FOREVER);
_loopTask.enable();
}
void MqttHandleVedirectHassClass::loop()
{
if (!Configuration.get().Vedirect.Enabled) {
return;
}
if (_updateForced) {
publishConfig();
_updateForced = false;
}
if (MqttSettings.getConnected() && !_wasConnected) {
// Connection established
_wasConnected = true;
publishConfig();
} else if (!MqttSettings.getConnected() && _wasConnected) {
// Connection lost
_wasConnected = false;
}
}
void MqttHandleVedirectHassClass::forceUpdate()
{
_updateForced = true;
}
void MqttHandleVedirectHassClass::publishConfig()
{
if ((!Configuration.get().Mqtt.Hass.Enabled) ||
(!Configuration.get().Vedirect.Enabled)) {
return;
}
if (!MqttSettings.getConnected()) {
return;
}
// device info
for (int idx = 0; idx < VictronMppt.controllerAmount(); ++idx) {
auto optMpptData = VictronMppt.getData(idx);
if (!optMpptData.has_value()) { continue; }
publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF", *optMpptData);
publishSensor("MPPT serial number", "mdi:counter", "SER", nullptr, nullptr, nullptr, *optMpptData);
publishSensor("MPPT firmware number", "mdi:counter", "FW", nullptr, nullptr, nullptr, *optMpptData);
publishSensor("MPPT state of operation", "mdi:wrench", "CS", nullptr, nullptr, nullptr, *optMpptData);
publishSensor("MPPT error code", "mdi:bell", "ERR", nullptr, nullptr, nullptr, *optMpptData);
publishSensor("MPPT off reason", "mdi:wrench", "OR", nullptr, nullptr, nullptr, *optMpptData);
publishSensor("MPPT tracker operation mode", "mdi:wrench", "MPPT", nullptr, nullptr, nullptr, *optMpptData);
publishSensor("MPPT Day sequence number (0...364)", "mdi:calendar-month-outline", "HSDS", NULL, "total", "d", *optMpptData);
// battery info
publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V", *optMpptData);
publishSensor("Battery current", NULL, "I", "current", "measurement", "A", *optMpptData);
publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W", *optMpptData);
publishSensor("Battery efficiency (calculated)", NULL, "E", NULL, "measurement", "%", *optMpptData);
// panel info
publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V", *optMpptData);
publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A", *optMpptData);
publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W", *optMpptData);
publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh", *optMpptData);
publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh", *optMpptData);
publishSensor("Panel maximum power today", NULL, "H21", "power", "measurement", "W", *optMpptData);
publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh", *optMpptData);
publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W", *optMpptData);
// optional info, provided only if TX is connected to charge controller
if (optMpptData->NetworkTotalDcInputPowerMilliWatts.first != 0) {
publishSensor("VE.Smart network total DC input power", "mdi:solar-power", "NetworkTotalDcInputPower", "power", "measurement", "W", *optMpptData);
}
if (optMpptData->MpptTemperatureMilliCelsius.first != 0) {
publishSensor("MPPT temperature", "mdi:temperature-celsius", "MpptTemperature", "temperature", "measurement", "W", *optMpptData);
}
if (optMpptData->SmartBatterySenseTemperatureMilliCelsius.first != 0) {
publishSensor("Smart Battery Sense temperature", "mdi:temperature-celsius", "SmartBatterySenseTemperature", "temperature", "measurement", "W", *optMpptData);
}
}
yield();
}
void MqttHandleVedirectHassClass::publishSensor(const char *caption, const char *icon, const char *subTopic,
const char *deviceClass, const char *stateClass,
const char *unitOfMeasurement,
const VeDirectMpptController::data_t &mpptData)
{
String serial = mpptData.serialNr_SER;
String sensorId = caption;
sensorId.replace(" ", "_");
sensorId.replace(".", "");
sensorId.replace("(", "");
sensorId.replace(")", "");
sensorId.toLowerCase();
String configTopic = "sensor/dtu_victron_" + serial
+ "/" + sensorId
+ "/config";
String statTopic = MqttSettings.getPrefix() + "victron/";
statTopic.concat(serial);
statTopic.concat("/");
statTopic.concat(subTopic);
JsonDocument root;
root["name"] = caption;
root["stat_t"] = statTopic;
root["uniq_id"] = serial + "_" + sensorId;
if (icon != NULL) {
root["icon"] = icon;
}
if (unitOfMeasurement != NULL) {
root["unit_of_meas"] = unitOfMeasurement;
}
JsonObject deviceObj = root["dev"].to<JsonObject>();
createDeviceInfo(deviceObj, mpptData);
if (Configuration.get().Mqtt.Hass.Expire) {
root["exp_aft"] = Configuration.get().Mqtt.PublishInterval * 3;
}
if (deviceClass != NULL) {
root["dev_cla"] = deviceClass;
}
if (stateClass != NULL) {
root["stat_cla"] = stateClass;
}
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
char buffer[512];
serializeJson(root, buffer);
publish(configTopic, buffer);
}
void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const char *icon, const char *subTopic,
const char *payload_on, const char *payload_off,
const VeDirectMpptController::data_t &mpptData)
{
String serial = mpptData.serialNr_SER;
String sensorId = caption;
sensorId.replace(" ", "_");
sensorId.replace(".", "");
sensorId.replace("(", "");
sensorId.replace(")", "");
sensorId.toLowerCase();
String configTopic = "binary_sensor/dtu_victron_" + serial
+ "/" + sensorId
+ "/config";
String statTopic = MqttSettings.getPrefix() + "victron/";
statTopic.concat(serial);
statTopic.concat("/");
statTopic.concat(subTopic);
JsonDocument root;
root["name"] = caption;
root["uniq_id"] = serial + "_" + sensorId;
root["stat_t"] = statTopic;
root["pl_on"] = payload_on;
root["pl_off"] = payload_off;
if (icon != NULL) {
root["icon"] = icon;
}
JsonObject deviceObj = root["dev"].to<JsonObject>();
createDeviceInfo(deviceObj, mpptData);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
char buffer[512];
serializeJson(root, buffer);
publish(configTopic, buffer);
}
void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject &object,
const VeDirectMpptController::data_t &mpptData)
{
String serial = mpptData.serialNr_SER;
object["name"] = "Victron(" + serial + ")";
object["ids"] = serial;
object["cu"] = String("http://") + NetworkSettings.localIP().toString();
object["mf"] = "OpenDTU";
object["mdl"] = mpptData.getPidAsString();
object["sw"] = AUTO_GIT_HASH;
}
void MqttHandleVedirectHassClass::publish(const String& subtopic, const String& payload)
{
String topic = Configuration.get().Mqtt.Hass.Topic;
topic += subtopic;
MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt.Hass.Retain);
}

View File

@ -0,0 +1,248 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "PylontechCanReceiver.h"
#include "Battery.h"
#include "MqttHandleBatteryHass.h"
#include "Configuration.h"
#include "MqttSettings.h"
#include "Utils.h"
MqttHandleBatteryHassClass MqttHandleBatteryHass;
void MqttHandleBatteryHassClass::init(Scheduler& scheduler)
{
scheduler.addTask(_loopTask);
_loopTask.setCallback(std::bind(&MqttHandleBatteryHassClass::loop, this));
_loopTask.setIterations(TASK_FOREVER);
_loopTask.enable();
}
void MqttHandleBatteryHassClass::loop()
{
CONFIG_T& config = Configuration.get();
if (!config.Battery.Enabled) { return; }
if (!config.Mqtt.Hass.Enabled) { return; }
// TODO(schlimmchen): this cannot make sure that transient
// connection problems are actually always noticed.
if (!MqttSettings.getConnected()) {
_doPublish = true;
return;
}
// only publish HA config once when (re-)connecting
// to the MQTT broker or on config changes.
if (!_doPublish) { return; }
// the MQTT battery provider does not re-publish the SoC under a different
// known topic. we don't know the manufacture either. HASS auto-discovery
// for that provider makes no sense.
if (config.Battery.Provider != 2) {
publishSensor("Manufacturer", "mdi:factory", "manufacturer");
publishSensor("Data Age", "mdi:timer-sand", "dataAge", "duration", "measurement", "s");
publishSensor("State of Charge (SoC)", "mdi:battery-medium", "stateOfCharge", "battery", "measurement", "%");
}
switch (config.Battery.Provider) {
case 0: // Pylontech Battery
publishSensor("Battery voltage", NULL, "voltage", "voltage", "measurement", "V");
publishSensor("Battery current", NULL, "current", "current", "measurement", "A");
publishSensor("Temperature", NULL, "temperature", "temperature", "measurement", "°C");
publishSensor("State of Health (SOH)", "mdi:heart-plus", "stateOfHealth", NULL, "measurement", "%");
publishSensor("Charge voltage (BMS)", NULL, "settings/chargeVoltage", "voltage", "measurement", "V");
publishSensor("Charge current limit", NULL, "settings/chargeCurrentLimitation", "current", "measurement", "A");
publishSensor("Discharge current limit", NULL, "settings/dischargeCurrentLimitation", "current", "measurement", "A");
publishBinarySensor("Alarm Discharge current", "mdi:alert", "alarm/overCurrentDischarge", "1", "0");
publishBinarySensor("Warning Discharge current", "mdi:alert-outline", "warning/highCurrentDischarge", "1", "0");
publishBinarySensor("Alarm Temperature low", "mdi:thermometer-low", "alarm/underTemperature", "1", "0");
publishBinarySensor("Warning Temperature low", "mdi:thermometer-low", "warning/lowTemperature", "1", "0");
publishBinarySensor("Alarm Temperature high", "mdi:thermometer-high", "alarm/overTemperature", "1", "0");
publishBinarySensor("Warning Temperature high", "mdi:thermometer-high", "warning/highTemperature", "1", "0");
publishBinarySensor("Alarm Voltage low", "mdi:alert", "alarm/underVoltage", "1", "0");
publishBinarySensor("Warning Voltage low", "mdi:alert-outline", "warning/lowVoltage", "1", "0");
publishBinarySensor("Alarm Voltage high", "mdi:alert", "alarm/overVoltage", "1", "0");
publishBinarySensor("Warning Voltage high", "mdi:alert-outline", "warning/highVoltage", "1", "0");
publishBinarySensor("Alarm BMS internal", "mdi:alert", "alarm/bmsInternal", "1", "0");
publishBinarySensor("Warning BMS internal", "mdi:alert-outline", "warning/bmsInternal", "1", "0");
publishBinarySensor("Alarm High charge current", "mdi:alert", "alarm/overCurrentCharge", "1", "0");
publishBinarySensor("Warning High charge current", "mdi:alert-outline", "warning/highCurrentCharge", "1", "0");
publishBinarySensor("Charge enabled", "mdi:battery-arrow-up", "charging/chargeEnabled", "1", "0");
publishBinarySensor("Discharge enabled", "mdi:battery-arrow-down", "charging/dischargeEnabled", "1", "0");
publishBinarySensor("Charge immediately", "mdi:alert", "charging/chargeImmediately", "1", "0");
break;
case 1: // JK BMS
// caption icon topic dev. class state class unit
publishSensor("Voltage", "mdi:battery-charging", "BatteryVoltageMilliVolt", "voltage", "measurement", "mV");
publishSensor("Current", "mdi:current-dc", "BatteryCurrentMilliAmps", "current", "measurement", "mA");
publishSensor("BMS Temperature", "mdi:thermometer", "BmsTempCelsius", "temperature", "measurement", "°C");
publishSensor("Cell Voltage Diff", "mdi:battery-alert", "CellDiffMilliVolt", "voltage", "measurement", "mV");
publishSensor("Charge Cycles", "mdi:counter", "BatteryCycles");
publishSensor("Cycle Capacity", "mdi:battery-sync", "BatteryCycleCapacity");
publishBinarySensor("Charging Possible", "mdi:battery-arrow-up", "status/ChargingActive", "1", "0");
publishBinarySensor("Discharging Possible", "mdi:battery-arrow-down", "status/DischargingActive", "1", "0");
publishBinarySensor("Balancing Active", "mdi:scale-balance", "status/BalancingActive", "1", "0");
#define PBS(a, b, c) publishBinarySensor("Alarm: " a, "mdi:" b, "alarms/" c, "1", "0")
PBS("Low Capacity", "battery-alert-variant-outline", "LowCapacity");
PBS("BMS Overtemperature", "thermometer-alert", "BmsOvertemperature");
PBS("Charging Overvoltage", "fuse-alert", "ChargingOvervoltage");
PBS("Discharge Undervoltage", "fuse-alert", "DischargeUndervoltage");
PBS("Battery Overtemperature", "thermometer-alert", "BatteryOvertemperature");
PBS("Charging Overcurrent", "fuse-alert", "ChargingOvercurrent");
PBS("Discharging Overcurrent", "fuse-alert", "DischargeOvercurrent");
PBS("Cell Voltage Difference", "battery-alert", "CellVoltageDifference");
PBS("Battery Box Overtemperature", "thermometer-alert", "BatteryBoxOvertemperature");
PBS("Battery Undertemperature", "thermometer-alert", "BatteryUndertemperature");
PBS("Cell Overvoltage", "battery-alert", "CellOvervoltage");
PBS("Cell Undervoltage", "battery-alert", "CellUndervoltage");
#undef PBS
break;
case 2: // SoC from MQTT
break;
case 3: // Victron SmartShunt
publishSensor("Voltage", "mdi:battery-charging", "voltage", "voltage", "measurement", "V");
publishSensor("Current", "mdi:current-dc", "current", "current", "measurement", "A");
publishSensor("Instantaneous Power", NULL, "instantaneousPower", "power", "measurement", "W");
publishSensor("Charged Energy", NULL, "chargedEnergy", "energy", "total_increasing", "kWh");
publishSensor("Discharged Energy", NULL, "dischargedEnergy", "energy", "total_increasing", "kWh");
publishSensor("Charge Cycles", "mdi:counter", "chargeCycles");
publishSensor("Consumed Amp Hours", NULL, "consumedAmpHours", NULL, "measurement", "Ah");
publishSensor("Last Full Charge", "mdi:timelapse", "lastFullCharge", NULL, NULL, "min");
break;
}
_doPublish = false;
}
void MqttHandleBatteryHassClass::publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass, const char* stateClass, const char* unitOfMeasurement )
{
String sensorId = caption;
sensorId.replace(" ", "_");
sensorId.replace(".", "");
sensorId.replace("(", "");
sensorId.replace(")", "");
sensorId.toLowerCase();
String configTopic = "sensor/dtu_battery_" + serial
+ "/" + sensorId
+ "/config";
String statTopic = MqttSettings.getPrefix() + "battery/";
// omit serial to avoid a breaking change
// statTopic.concat(serial);
// statTopic.concat("/");
statTopic.concat(subTopic);
JsonDocument root;
root["name"] = caption;
root["stat_t"] = statTopic;
root["uniq_id"] = serial + "_" + sensorId;
if (icon != NULL) {
root["icon"] = icon;
}
if (unitOfMeasurement != NULL) {
root["unit_of_meas"] = unitOfMeasurement;
}
JsonObject deviceObj = root["dev"].to<JsonObject>();
createDeviceInfo(deviceObj);
if (Configuration.get().Mqtt.Hass.Expire) {
root["exp_aft"] = Battery.getStats()->getMqttFullPublishIntervalMs() / 1000 * 3;
}
if (deviceClass != NULL) {
root["dev_cla"] = deviceClass;
}
if (stateClass != NULL) {
root["stat_cla"] = stateClass;
}
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
char buffer[512];
serializeJson(root, buffer);
publish(configTopic, buffer);
}
void MqttHandleBatteryHassClass::publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off)
{
String sensorId = caption;
sensorId.replace(" ", "_");
sensorId.replace(".", "");
sensorId.replace("(", "");
sensorId.replace(")", "");
sensorId.replace(":", "");
sensorId.toLowerCase();
String configTopic = "binary_sensor/dtu_battery_" + serial
+ "/" + sensorId
+ "/config";
String statTopic = MqttSettings.getPrefix() + "battery/";
// omit serial to avoid a breaking change
// statTopic.concat(serial);
// statTopic.concat("/");
statTopic.concat(subTopic);
JsonDocument root;
root["name"] = caption;
root["uniq_id"] = serial + "_" + sensorId;
root["stat_t"] = statTopic;
root["pl_on"] = payload_on;
root["pl_off"] = payload_off;
if (icon != NULL) {
root["icon"] = icon;
}
auto deviceObj = root["dev"].to<JsonObject>();
createDeviceInfo(deviceObj);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
char buffer[512];
serializeJson(root, buffer);
publish(configTopic, buffer);
}
void MqttHandleBatteryHassClass::createDeviceInfo(JsonObject& object)
{
object["name"] = "Battery(" + serial + ")";
auto& config = Configuration.get();
if (config.Battery.Provider == 1) {
object["name"] = "JK BMS (" + Battery.getStats()->getManufacturer() + ")";
}
object["ids"] = serial;
object["cu"] = String("http://") + NetworkSettings.localIP().toString();
object["mf"] = "OpenDTU";
object["mdl"] = Battery.getStats()->getManufacturer();
object["sw"] = AUTO_GIT_HASH;
}
void MqttHandleBatteryHassClass::publish(const String& subtopic, const String& payload)
{
String topic = Configuration.get().Mqtt.Hass.Topic;
topic += subtopic;
MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt.Hass.Retain);
}

View File

@ -323,9 +323,7 @@ void MqttHandleHassClass::publishDtuSensor(const char* name, const char* device_
createDtuInfo(root); createDtuInfo(root);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer; String buffer;
const String configTopic = "sensor/" + getDtuUniqueId() + "/" + id + "/config"; const String configTopic = "sensor/" + getDtuUniqueId() + "/" + id + "/config";

162
src/MqttHandleHuawei.cpp Normal file
View File

@ -0,0 +1,162 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Thomas Basler and others
*/
#include "MqttHandleHuawei.h"
#include "MessageOutput.h"
#include "MqttSettings.h"
#include "Huawei_can.h"
// #include "Failsafe.h"
#include "WebApi_Huawei.h"
#include <ctime>
MqttHandleHuaweiClass MqttHandleHuawei;
void MqttHandleHuaweiClass::init(Scheduler& scheduler)
{
scheduler.addTask(_loopTask);
_loopTask.setCallback(std::bind(&MqttHandleHuaweiClass::loop, this));
_loopTask.setIterations(TASK_FOREVER);
_loopTask.enable();
String const& prefix = MqttSettings.getPrefix();
auto subscribe = [&prefix, this](char const* subTopic, Topic t) {
String fullTopic(prefix + "huawei/cmd/" + subTopic);
MqttSettings.subscribe(fullTopic.c_str(), 0,
std::bind(&MqttHandleHuaweiClass::onMqttMessage, this, t,
std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4,
std::placeholders::_5, std::placeholders::_6));
};
subscribe("limit_online_voltage", Topic::LimitOnlineVoltage);
subscribe("limit_online_current", Topic::LimitOnlineCurrent);
subscribe("limit_offline_voltage", Topic::LimitOfflineVoltage);
subscribe("limit_offline_current", Topic::LimitOfflineCurrent);
subscribe("mode", Topic::Mode);
_lastPublish = millis();
}
void MqttHandleHuaweiClass::loop()
{
const CONFIG_T& config = Configuration.get();
std::unique_lock<std::mutex> mqttLock(_mqttMutex);
if (!config.Huawei.Enabled) {
_mqttCallbacks.clear();
return;
}
for (auto& callback : _mqttCallbacks) { callback(); }
_mqttCallbacks.clear();
mqttLock.unlock();
if (!MqttSettings.getConnected() ) {
return;
}
const RectifierParameters_t *rp = HuaweiCan.get();
if ((millis() - _lastPublish) > (config.Mqtt.PublishInterval * 1000) ) {
MqttSettings.publish("huawei/data_age", String((millis() - HuaweiCan.getLastUpdate()) / 1000));
MqttSettings.publish("huawei/input_voltage", String(rp->input_voltage));
MqttSettings.publish("huawei/input_current", String(rp->input_current));
MqttSettings.publish("huawei/input_power", String(rp->input_power));
MqttSettings.publish("huawei/output_voltage", String(rp->output_voltage));
MqttSettings.publish("huawei/output_current", String(rp->output_current));
MqttSettings.publish("huawei/max_output_current", String(rp->max_output_current));
MqttSettings.publish("huawei/output_power", String(rp->output_power));
MqttSettings.publish("huawei/input_temp", String(rp->input_temp));
MqttSettings.publish("huawei/output_temp", String(rp->output_temp));
MqttSettings.publish("huawei/efficiency", String(rp->efficiency));
MqttSettings.publish("huawei/mode", String(HuaweiCan.getMode()));
yield();
_lastPublish = millis();
}
}
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)
{
std::string strValue(reinterpret_cast<const char*>(payload), len);
float payload_val = -1;
try {
payload_val = std::stof(strValue);
}
catch (std::invalid_argument const& e) {
MessageOutput.printf("Huawei MQTT handler: cannot parse payload of topic '%s' as float: %s\r\n",
topic, strValue.c_str());
return;
}
std::lock_guard<std::mutex> mqttLock(_mqttMutex);
switch (t) {
case Topic::LimitOnlineVoltage:
MessageOutput.printf("Limit Voltage: %f V\r\n", payload_val);
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
&HuaweiCan, payload_val, HUAWEI_ONLINE_VOLTAGE));
break;
case Topic::LimitOfflineVoltage:
MessageOutput.printf("Offline Limit Voltage: %f V\r\n", payload_val);
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
&HuaweiCan, payload_val, HUAWEI_OFFLINE_VOLTAGE));
break;
case Topic::LimitOnlineCurrent:
MessageOutput.printf("Limit Current: %f A\r\n", payload_val);
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
&HuaweiCan, payload_val, HUAWEI_ONLINE_CURRENT));
break;
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;
}
}

View File

@ -0,0 +1,181 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Thomas Basler, Malte Schmidt and others
*/
#include "MessageOutput.h"
#include "MqttSettings.h"
#include "MqttHandlePowerLimiter.h"
#include "PowerLimiter.h"
#include <ctime>
#include <string>
MqttHandlePowerLimiterClass MqttHandlePowerLimiter;
void MqttHandlePowerLimiterClass::init(Scheduler& scheduler)
{
scheduler.addTask(_loopTask);
_loopTask.setCallback(std::bind(&MqttHandlePowerLimiterClass::loop, this));
_loopTask.setIterations(TASK_FOREVER);
_loopTask.enable();
using std::placeholders::_1;
using std::placeholders::_2;
using std::placeholders::_3;
using std::placeholders::_4;
using std::placeholders::_5;
using std::placeholders::_6;
String const& prefix = MqttSettings.getPrefix();
auto subscribe = [&prefix, this](char const* subTopic, MqttPowerLimiterCommand command) {
String fullTopic(prefix + "powerlimiter/cmd/" + subTopic);
MqttSettings.subscribe(fullTopic.c_str(), 0,
std::bind(&MqttHandlePowerLimiterClass::onMqttCmd, this, command,
std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4,
std::placeholders::_5, std::placeholders::_6));
};
subscribe("threshold/soc/start", MqttPowerLimiterCommand::BatterySoCStartThreshold);
subscribe("threshold/soc/stop", MqttPowerLimiterCommand::BatterySoCStopThreshold);
subscribe("threshold/soc/full_solar_passthrough", MqttPowerLimiterCommand::FullSolarPassthroughSoC);
subscribe("threshold/voltage/start", MqttPowerLimiterCommand::VoltageStartThreshold);
subscribe("threshold/voltage/stop", MqttPowerLimiterCommand::VoltageStopThreshold);
subscribe("threshold/voltage/full_solar_passthrough_start", MqttPowerLimiterCommand::FullSolarPassThroughStartVoltage);
subscribe("threshold/voltage/full_solar_passthrough_stop", MqttPowerLimiterCommand::FullSolarPassThroughStopVoltage);
subscribe("mode", MqttPowerLimiterCommand::Mode);
_lastPublish = millis();
}
void MqttHandlePowerLimiterClass::loop()
{
std::unique_lock<std::mutex> mqttLock(_mqttMutex);
const CONFIG_T& config = Configuration.get();
if (!config.PowerLimiter.Enabled) {
_mqttCallbacks.clear();
return;
}
for (auto& callback : _mqttCallbacks) { callback(); }
_mqttCallbacks.clear();
mqttLock.unlock();
if (!MqttSettings.getConnected() ) { return; }
if ((millis() - _lastPublish) < (config.Mqtt.PublishInterval * 1000)) {
return;
}
_lastPublish = millis();
auto val = static_cast<unsigned>(PowerLimiter.getMode());
MqttSettings.publish("powerlimiter/status/mode", String(val));
MqttSettings.publish("powerlimiter/status/inverter_update_timeouts", String(PowerLimiter.getInverterUpdateTimeouts()));
// no thresholds are relevant for setups without a battery
if (config.PowerLimiter.IsInverterSolarPowered) { return; }
MqttSettings.publish("powerlimiter/status/threshold/voltage/start", String(config.PowerLimiter.VoltageStartThreshold));
MqttSettings.publish("powerlimiter/status/threshold/voltage/stop", String(config.PowerLimiter.VoltageStopThreshold));
if (config.Vedirect.Enabled) {
MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_start", String(config.PowerLimiter.FullSolarPassThroughStartVoltage));
MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_stop", String(config.PowerLimiter.FullSolarPassThroughStopVoltage));
}
if (!config.Battery.Enabled || config.PowerLimiter.IgnoreSoc) { return; }
MqttSettings.publish("powerlimiter/status/threshold/soc/start", String(config.PowerLimiter.BatterySocStartThreshold));
MqttSettings.publish("powerlimiter/status/threshold/soc/stop", String(config.PowerLimiter.BatterySocStopThreshold));
if (config.Vedirect.Enabled) {
MqttSettings.publish("powerlimiter/status/threshold/soc/full_solar_passthrough", String(config.PowerLimiter.FullSolarPassThroughSoc));
}
}
void MqttHandlePowerLimiterClass::onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
{
CONFIG_T& config = Configuration.get();
std::string strValue(reinterpret_cast<const char*>(payload), len);
float payload_val = -1;
try {
payload_val = std::stof(strValue);
}
catch (std::invalid_argument const& e) {
MessageOutput.printf("PowerLimiter MQTT handler: cannot parse payload of topic '%s' as float: %s\r\n",
topic, strValue.c_str());
return;
}
const int intValue = static_cast<int>(payload_val);
std::lock_guard<std::mutex> mqttLock(_mqttMutex);
switch (command) {
case MqttPowerLimiterCommand::Mode:
{
using Mode = PowerLimiterClass::Mode;
Mode mode = static_cast<Mode>(intValue);
if (mode == Mode::UnconditionalFullSolarPassthrough) {
MessageOutput.println("Power limiter unconditional full solar PT");
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
&PowerLimiter, Mode::UnconditionalFullSolarPassthrough));
} else if (mode == Mode::Disabled) {
MessageOutput.println("Power limiter disabled (override)");
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
&PowerLimiter, Mode::Disabled));
} else if (mode == Mode::Normal) {
MessageOutput.println("Power limiter normal operation");
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
&PowerLimiter, Mode::Normal));
} else {
MessageOutput.printf("PowerLimiter - unknown mode %d\r\n", intValue);
}
return;
}
case MqttPowerLimiterCommand::BatterySoCStartThreshold:
if (config.PowerLimiter.BatterySocStartThreshold == intValue) { return; }
MessageOutput.printf("Setting battery SoC start threshold to: %d %%\r\n", intValue);
config.PowerLimiter.BatterySocStartThreshold = intValue;
break;
case MqttPowerLimiterCommand::BatterySoCStopThreshold:
if (config.PowerLimiter.BatterySocStopThreshold == intValue) { return; }
MessageOutput.printf("Setting battery SoC stop threshold to: %d %%\r\n", intValue);
config.PowerLimiter.BatterySocStopThreshold = intValue;
break;
case MqttPowerLimiterCommand::FullSolarPassthroughSoC:
if (config.PowerLimiter.FullSolarPassThroughSoc == intValue) { return; }
MessageOutput.printf("Setting full solar passthrough SoC to: %d %%\r\n", intValue);
config.PowerLimiter.FullSolarPassThroughSoc = intValue;
break;
case MqttPowerLimiterCommand::VoltageStartThreshold:
if (config.PowerLimiter.VoltageStartThreshold == payload_val) { return; }
MessageOutput.printf("Setting voltage start threshold to: %.2f V\r\n", payload_val);
config.PowerLimiter.VoltageStartThreshold = payload_val;
break;
case MqttPowerLimiterCommand::VoltageStopThreshold:
if (config.PowerLimiter.VoltageStopThreshold == payload_val) { return; }
MessageOutput.printf("Setting voltage stop threshold to: %.2f V\r\n", payload_val);
config.PowerLimiter.VoltageStopThreshold = payload_val;
break;
case MqttPowerLimiterCommand::FullSolarPassThroughStartVoltage:
if (config.PowerLimiter.FullSolarPassThroughStartVoltage == payload_val) { return; }
MessageOutput.printf("Setting full solar passthrough start voltage to: %.2f V\r\n", payload_val);
config.PowerLimiter.FullSolarPassThroughStartVoltage = payload_val;
break;
case MqttPowerLimiterCommand::FullSolarPassThroughStopVoltage:
if (config.PowerLimiter.FullSolarPassThroughStopVoltage == payload_val) { return; }
MessageOutput.printf("Setting full solar passthrough stop voltage to: %.2f V\r\n", payload_val);
config.PowerLimiter.FullSolarPassThroughStopVoltage = payload_val;
break;
}
// not reached if the value did not change
Configuration.write();
}

View File

@ -0,0 +1,204 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Thomas Basler and others
*/
#include "MqttHandlePowerLimiterHass.h"
#include "Configuration.h"
#include "MqttSettings.h"
#include "NetworkSettings.h"
#include "MessageOutput.h"
#include "Utils.h"
MqttHandlePowerLimiterHassClass MqttHandlePowerLimiterHass;
void MqttHandlePowerLimiterHassClass::init(Scheduler& scheduler)
{
scheduler.addTask(_loopTask);
_loopTask.setCallback(std::bind(&MqttHandlePowerLimiterHassClass::loop, this));
_loopTask.setIterations(TASK_FOREVER);
_loopTask.enable();
}
void MqttHandlePowerLimiterHassClass::loop()
{
if (!Configuration.get().PowerLimiter.Enabled) {
return;
}
if (_updateForced) {
publishConfig();
_updateForced = false;
}
if (MqttSettings.getConnected() && !_wasConnected) {
// Connection established
_wasConnected = true;
publishConfig();
} else if (!MqttSettings.getConnected() && _wasConnected) {
// Connection lost
_wasConnected = false;
}
}
void MqttHandlePowerLimiterHassClass::forceUpdate()
{
_updateForced = true;
}
void MqttHandlePowerLimiterHassClass::publishConfig()
{
auto const& config = Configuration.get();
if (!config.Mqtt.Hass.Enabled) {
return;
}
if (!MqttSettings.getConnected()) {
return;
}
if (!config.PowerLimiter.Enabled) {
return;
}
publishSelect("DPL Mode", "mdi:gauge", "config", "mode", "mode");
if (config.PowerLimiter.IsInverterSolarPowered) {
return;
}
// as this project revolves around Hoymiles inverters, 16 - 60 V is a reasonable voltage range
publishNumber("DPL battery voltage start threshold", "mdi:battery-charging",
"config", "threshold/voltage/start", "threshold/voltage/start", "V", 16, 60);
publishNumber("DPL battery voltage stop threshold", "mdi:battery-charging",
"config", "threshold/voltage/stop", "threshold/voltage/stop", "V", 16, 60);
if (config.Vedirect.Enabled) {
publishNumber("DPL full solar passthrough start voltage",
"mdi:transmission-tower-import", "config",
"threshold/voltage/full_solar_passthrough_start",
"threshold/voltage/full_solar_passthrough_start", "V", 16, 60);
publishNumber("DPL full solar passthrough stop voltage",
"mdi:transmission-tower-import", "config",
"threshold/voltage/full_solar_passthrough_stop",
"threshold/voltage/full_solar_passthrough_stop", "V", 16, 60);
}
if (config.Battery.Enabled && !config.PowerLimiter.IgnoreSoc) {
publishNumber("DPL battery SoC start threshold", "mdi:battery-charging",
"config", "threshold/soc/start", "threshold/soc/start", "%", 0, 100);
publishNumber("DPL battery SoC stop threshold", "mdi:battery-charging",
"config", "threshold/soc/stop", "threshold/soc/stop", "%", 0, 100);
if (config.Vedirect.Enabled) {
publishNumber("DPL full solar passthrough SoC",
"mdi:transmission-tower-import", "config",
"threshold/soc/full_solar_passthrough",
"threshold/soc/full_solar_passthrough", "%", 0, 100);
}
}
}
void MqttHandlePowerLimiterHassClass::publishSelect(
const char* caption, const char* icon, const char* category,
const char* commandTopic, const char* stateTopic)
{
String selectId = caption;
selectId.replace(" ", "_");
selectId.toLowerCase();
const String configTopic = "select/powerlimiter/" + selectId + "/config";
const String cmdTopic = MqttSettings.getPrefix() + "powerlimiter/cmd/" + commandTopic;
const String statTopic = MqttSettings.getPrefix() + "powerlimiter/status/" + stateTopic;
JsonDocument root;
root["name"] = caption;
root["uniq_id"] = selectId;
if (strcmp(icon, "")) {
root["ic"] = icon;
}
root["ent_cat"] = category;
root["cmd_t"] = cmdTopic;
root["stat_t"] = statTopic;
JsonArray options = root["options"].to<JsonArray>();
options.add("0");
options.add("1");
options.add("2");
JsonObject deviceObj = root["dev"].to<JsonObject>();
createDeviceInfo(deviceObj);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
serializeJson(root, buffer);
publish(configTopic, buffer);
}
void MqttHandlePowerLimiterHassClass::publishNumber(
const char* caption, const char* icon, const char* category,
const char* commandTopic, const char* stateTopic, const char* unitOfMeasure,
const int16_t min, const int16_t max)
{
String numberId = caption;
numberId.replace(" ", "_");
numberId.toLowerCase();
const String configTopic = "number/powerlimiter/" + numberId + "/config";
const String cmdTopic = MqttSettings.getPrefix() + "powerlimiter/cmd/" + commandTopic;
const String statTopic = MqttSettings.getPrefix() + "powerlimiter/status/" + stateTopic;
JsonDocument root;
root["name"] = caption;
root["uniq_id"] = numberId;
if (strcmp(icon, "")) {
root["ic"] = icon;
}
root["ent_cat"] = category;
root["cmd_t"] = cmdTopic;
root["stat_t"] = statTopic;
root["unit_of_meas"] = unitOfMeasure;
root["min"] = min;
root["max"] = max;
root["mode"] = "box";
auto const& config = Configuration.get();
if (config.Mqtt.Hass.Expire) {
root["exp_aft"] = config.Mqtt.PublishInterval * 3;
}
JsonObject deviceObj = root["dev"].to<JsonObject>();
createDeviceInfo(deviceObj);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
serializeJson(root, buffer);
publish(configTopic, buffer);
}
void MqttHandlePowerLimiterHassClass::createDeviceInfo(JsonObject& object)
{
object["name"] = "Dynamic Power Limiter";
object["ids"] = "0002";
object["cu"] = String("http://") + NetworkSettings.localIP().toString();
object["mf"] = "OpenDTU";
object["mdl"] = "Dynamic Power Limiter";
object["sw"] = AUTO_GIT_HASH;
}
void MqttHandlePowerLimiterHassClass::publish(const String& subtopic, const String& payload)
{
String topic = Configuration.get().Mqtt.Hass.Topic;
topic += subtopic;
MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt.Hass.Retain);
}

143
src/MqttHandleVedirect.cpp Normal file
View File

@ -0,0 +1,143 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Helge Erbe and others
*/
#include "VictronMppt.h"
#include "MqttHandleVedirect.h"
#include "MqttSettings.h"
#include "MessageOutput.h"
MqttHandleVedirectClass MqttHandleVedirect;
// #define MQTTHANDLEVEDIRECT_DEBUG
void MqttHandleVedirectClass::init(Scheduler& scheduler)
{
scheduler.addTask(_loopTask);
_loopTask.setCallback([this] { loop(); });
_loopTask.setIterations(TASK_FOREVER);
_loopTask.enable();
// initially force a full publish
this->forceUpdate();
}
void MqttHandleVedirectClass::forceUpdate()
{
// initially force a full publish
_nextPublishUpdatesOnly = 0;
_nextPublishFull = 1;
}
void MqttHandleVedirectClass::loop()
{
CONFIG_T& config = Configuration.get();
if (!MqttSettings.getConnected() || !config.Vedirect.Enabled) {
return;
}
if ((millis() >= _nextPublishFull) || (millis() >= _nextPublishUpdatesOnly)) {
// determine if this cycle should publish full values or updates only
if (_nextPublishFull <= _nextPublishUpdatesOnly) {
_PublishFull = true;
} else {
_PublishFull = !config.Vedirect.UpdatesOnly;
}
#ifdef MQTTHANDLEVEDIRECT_DEBUG
MessageOutput.printf("\r\n\r\nMqttHandleVedirectClass::loop millis %lu _nextPublishUpdatesOnly %u _nextPublishFull %u\r\n", millis(), _nextPublishUpdatesOnly, _nextPublishFull);
if (_PublishFull) {
MessageOutput.println("MqttHandleVedirectClass::loop publish full");
} else {
MessageOutput.println("MqttHandleVedirectClass::loop publish updates only");
}
#endif
for (int idx = 0; idx < VictronMppt.controllerAmount(); ++idx) {
std::optional<VeDirectMpptController::data_t> optMpptData = VictronMppt.getData(idx);
if (!optMpptData.has_value()) { continue; }
auto const& kvFrame = _kvFrames[optMpptData->serialNr_SER];
publish_mppt_data(*optMpptData, kvFrame);
if (!_PublishFull) {
_kvFrames[optMpptData->serialNr_SER] = *optMpptData;
}
}
// now calculate next points of time to publish
_nextPublishUpdatesOnly = millis() + (config.Mqtt.PublishInterval * 1000);
if (_PublishFull) {
// when Home Assistant MQTT-Auto-Discovery is active,
// and "enable expiration" is active, all values must be published at
// least once before the announced expiry interval is reached
if ((config.Vedirect.UpdatesOnly) && (config.Mqtt.Hass.Enabled) && (config.Mqtt.Hass.Expire)) {
_nextPublishFull = millis() + (((config.Mqtt.PublishInterval * 3) - 1) * 1000);
#ifdef MQTTHANDLEVEDIRECT_DEBUG
uint32_t _tmpNextFullSeconds = (config.Mqtt_PublishInterval * 3) - 1;
MessageOutput.printf("MqttHandleVedirectClass::loop _tmpNextFullSeconds %u - _nextPublishFull %u \r\n", _tmpNextFullSeconds, _nextPublishFull);
#endif
} else {
// no future publish full needed
_nextPublishFull = UINT32_MAX;
}
}
#ifdef MQTTHANDLEVEDIRECT_DEBUG
MessageOutput.printf("MqttHandleVedirectClass::loop _nextPublishUpdatesOnly %u _nextPublishFull %u\r\n", _nextPublishUpdatesOnly, _nextPublishFull);
#endif
}
}
void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::data_t &currentData,
const VeDirectMpptController::data_t &previousData) const {
String value;
String topic = "victron/";
topic.concat(currentData.serialNr_SER);
topic.concat("/");
#define PUBLISH(sm, t, val) \
if (_PublishFull || currentData.sm != previousData.sm) { \
MqttSettings.publish(topic + t, String(val)); \
}
PUBLISH(productID_PID, "PID", currentData.getPidAsString().data());
PUBLISH(serialNr_SER, "SER", currentData.serialNr_SER);
PUBLISH(firmwareNr_FW, "FW", currentData.firmwareNr_FW);
PUBLISH(loadOutputState_LOAD, "LOAD", (currentData.loadOutputState_LOAD ? "ON" : "OFF"));
PUBLISH(currentState_CS, "CS", currentData.getCsAsString().data());
PUBLISH(errorCode_ERR, "ERR", currentData.getErrAsString().data());
PUBLISH(offReason_OR, "OR", currentData.getOrAsString().data());
PUBLISH(stateOfTracker_MPPT, "MPPT", currentData.getMpptAsString().data());
PUBLISH(daySequenceNr_HSDS, "HSDS", currentData.daySequenceNr_HSDS);
PUBLISH(batteryVoltage_V_mV, "V", currentData.batteryVoltage_V_mV / 1000.0);
PUBLISH(batteryCurrent_I_mA, "I", currentData.batteryCurrent_I_mA / 1000.0);
PUBLISH(batteryOutputPower_W, "P", currentData.batteryOutputPower_W);
PUBLISH(panelVoltage_VPV_mV, "VPV", currentData.panelVoltage_VPV_mV / 1000.0);
PUBLISH(panelCurrent_mA, "IPV", currentData.panelCurrent_mA / 1000.0);
PUBLISH(panelPower_PPV_W, "PPV", currentData.panelPower_PPV_W);
PUBLISH(mpptEfficiency_Percent, "E", currentData.mpptEfficiency_Percent);
PUBLISH(yieldTotal_H19_Wh, "H19", currentData.yieldTotal_H19_Wh / 1000.0);
PUBLISH(yieldToday_H20_Wh, "H20", currentData.yieldToday_H20_Wh / 1000.0);
PUBLISH(maxPowerToday_H21_W, "H21", currentData.maxPowerToday_H21_W);
PUBLISH(yieldYesterday_H22_Wh, "H22", currentData.yieldYesterday_H22_Wh / 1000.0);
PUBLISH(maxPowerYesterday_H23_W, "H23", currentData.maxPowerYesterday_H23_W);
#undef PUBLILSH
#define PUBLISH_OPT(sm, t, val) \
if (currentData.sm.first != 0 && (_PublishFull || currentData.sm.second != previousData.sm.second)) { \
MqttSettings.publish(topic + t, String(val)); \
}
PUBLISH_OPT(NetworkTotalDcInputPowerMilliWatts, "NetworkTotalDcInputPower", currentData.NetworkTotalDcInputPowerMilliWatts.second / 1000.0);
PUBLISH_OPT(MpptTemperatureMilliCelsius, "MpptTemperature", currentData.MpptTemperatureMilliCelsius.second / 1000.0);
PUBLISH_OPT(SmartBatterySenseTemperatureMilliCelsius, "SmartBatterySenseTemperature", currentData.SmartBatterySenseTemperatureMilliCelsius.second / 1000.0);
#undef PUBLILSH_OPT
}

View File

@ -91,8 +91,10 @@ void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason re
void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total) void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total)
{ {
MessageOutput.print("Received MQTT message on topic: "); if (_verboseLogging) {
MessageOutput.println(topic); MessageOutput.print("Received MQTT message on topic: ");
MessageOutput.println(topic);
}
_mqttSubscribeParser.handle_message(properties, topic, payload, len, index, total); _mqttSubscribeParser.handle_message(properties, topic, payload, len, index, total);
} }
@ -114,6 +116,7 @@ void MqttSettingsClass::performConnect()
MessageOutput.println("Connecting to MQTT..."); MessageOutput.println("Connecting to MQTT...");
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
_verboseLogging = config.Mqtt.VerboseLogging;
const String willTopic = getPrefix() + config.Mqtt.Lwt.Topic; const String willTopic = getPrefix() + config.Mqtt.Lwt.Topic;
const String clientId = NetworkSettings.getApName(); const String clientId = NetworkSettings.getApName();
if (config.Mqtt.Tls.Enabled) { if (config.Mqtt.Tls.Enabled) {

View File

@ -84,6 +84,84 @@
#define CMT_SDIO -1 #define CMT_SDIO -1
#endif #endif
#ifndef VICTRON_PIN_TX
#define VICTRON_PIN_TX -1
#endif
#ifndef VICTRON_PIN_RX
#define VICTRON_PIN_RX -1
#endif
#ifndef VICTRON_PIN_TX2
#define VICTRON_PIN_TX2 -1
#endif
#ifndef VICTRON_PIN_RX2
#define VICTRON_PIN_RX2 -1
#endif
#ifndef BATTERY_PIN_RX
#define BATTERY_PIN_RX -1
#endif
#ifdef PYLONTECH_PIN_RX
#undef BATTERY_PIN_RX
#define BATTERY_PIN_RX PYLONTECH_PIN_RX
#endif
#ifndef BATTERY_PIN_RXEN
#define BATTERY_PIN_RXEN -1
#endif
#ifndef BATTERY_PIN_TX
#define BATTERY_PIN_TX -1
#endif
#ifdef PYLONTECH_PIN_TX
#undef BATTERY_PIN_TX
#define BATTERY_PIN_TX PYLONTECH_PIN_TX
#endif
#ifndef BATTERY_PIN_TXEN
#define BATTERY_PIN_TXEN -1
#endif
#ifndef HUAWEI_PIN_MISO
#define HUAWEI_PIN_MISO -1
#endif
#ifndef HUAWEI_PIN_MOSI
#define HUAWEI_PIN_MOSI -1
#endif
#ifndef HUAWEI_PIN_SCLK
#define HUAWEI_PIN_SCLK -1
#endif
#ifndef HUAWEI_PIN_CS
#define HUAWEI_PIN_CS -1
#endif
#ifndef HUAWEI_PIN_IRQ
#define HUAWEI_PIN_IRQ -1
#endif
#ifndef HUAWEI_PIN_POWER
#define HUAWEI_PIN_POWER -1
#endif
#ifndef POWERMETER_PIN_RX
#define POWERMETER_PIN_RX -1
#endif
#ifndef POWERMETER_PIN_TX
#define POWERMETER_PIN_TX -1
#endif
#ifndef POWERMETER_PIN_DERE
#define POWERMETER_PIN_DERE -1
#endif
PinMappingClass PinMapping; PinMappingClass PinMapping;
PinMappingClass::PinMappingClass() PinMappingClass::PinMappingClass()
@ -124,6 +202,29 @@ PinMappingClass::PinMappingClass()
_pinMapping.led[0] = LED0; _pinMapping.led[0] = LED0;
_pinMapping.led[1] = LED1; _pinMapping.led[1] = LED1;
// OpenDTU-OnBattery-specific pins below
_pinMapping.victron_rx = VICTRON_PIN_RX;
_pinMapping.victron_tx = VICTRON_PIN_TX;
_pinMapping.victron_rx2 = VICTRON_PIN_RX;
_pinMapping.victron_tx2 = VICTRON_PIN_TX;
_pinMapping.battery_rx = BATTERY_PIN_RX;
_pinMapping.battery_rxen = BATTERY_PIN_RXEN;
_pinMapping.battery_tx = BATTERY_PIN_TX;
_pinMapping.battery_txen = BATTERY_PIN_TXEN;
_pinMapping.huawei_miso = HUAWEI_PIN_MISO;
_pinMapping.huawei_mosi = HUAWEI_PIN_MOSI;
_pinMapping.huawei_clk = HUAWEI_PIN_SCLK;
_pinMapping.huawei_cs = HUAWEI_PIN_CS;
_pinMapping.huawei_irq = HUAWEI_PIN_IRQ;
_pinMapping.huawei_power = HUAWEI_PIN_POWER;
_pinMapping.powermeter_rx = POWERMETER_PIN_RX;
_pinMapping.powermeter_tx = POWERMETER_PIN_TX;
_pinMapping.powermeter_dere = POWERMETER_PIN_DERE;
} }
PinMapping_t& PinMappingClass::get() PinMapping_t& PinMappingClass::get()
@ -186,6 +287,28 @@ bool PinMappingClass::init(const String& deviceMapping)
_pinMapping.led[0] = doc[i]["led"]["led0"] | LED0; _pinMapping.led[0] = doc[i]["led"]["led0"] | LED0;
_pinMapping.led[1] = doc[i]["led"]["led1"] | LED1; _pinMapping.led[1] = doc[i]["led"]["led1"] | LED1;
// OpenDTU-OnBattery-specific pins below
_pinMapping.victron_rx = doc[i]["victron"]["rx"] | VICTRON_PIN_RX;
_pinMapping.victron_tx = doc[i]["victron"]["tx"] | VICTRON_PIN_TX;
_pinMapping.victron_rx2 = doc[i]["victron"]["rx2"] | VICTRON_PIN_RX2;
_pinMapping.victron_tx2 = doc[i]["victron"]["tx2"] | VICTRON_PIN_TX2;
_pinMapping.battery_rx = doc[i]["battery"]["rx"] | BATTERY_PIN_RX;
_pinMapping.battery_rxen = doc[i]["battery"]["rxen"] | BATTERY_PIN_RXEN;
_pinMapping.battery_tx = doc[i]["battery"]["tx"] | BATTERY_PIN_TX;
_pinMapping.battery_txen = doc[i]["battery"]["txen"] | BATTERY_PIN_TXEN;
_pinMapping.huawei_miso = doc[i]["huawei"]["miso"] | HUAWEI_PIN_MISO;
_pinMapping.huawei_mosi = doc[i]["huawei"]["mosi"] | HUAWEI_PIN_MOSI;
_pinMapping.huawei_clk = doc[i]["huawei"]["clk"] | HUAWEI_PIN_SCLK;
_pinMapping.huawei_irq = doc[i]["huawei"]["irq"] | HUAWEI_PIN_IRQ;
_pinMapping.huawei_cs = doc[i]["huawei"]["cs"] | HUAWEI_PIN_CS;
_pinMapping.huawei_power = doc[i]["huawei"]["power"] | HUAWEI_PIN_POWER;
_pinMapping.powermeter_rx = doc[i]["powermeter"]["rx"] | POWERMETER_PIN_RX;
_pinMapping.powermeter_tx = doc[i]["powermeter"]["tx"] | POWERMETER_PIN_TX;
_pinMapping.powermeter_dere = doc[i]["powermeter"]["dere"] | POWERMETER_PIN_DERE;
return true; return true;
} }
} }
@ -215,3 +338,13 @@ bool PinMappingClass::isValidEthConfig() const
{ {
return _pinMapping.eth_enabled; return _pinMapping.eth_enabled;
} }
bool PinMappingClass::isValidHuaweiConfig() const
{
return _pinMapping.huawei_miso >= 0
&& _pinMapping.huawei_mosi >= 0
&& _pinMapping.huawei_clk >= 0
&& _pinMapping.huawei_irq >= 0
&& _pinMapping.huawei_cs >= 0
&& _pinMapping.huawei_power >= 0;
}

Some files were not shown because too many files have changed in this diff Show More