Compare commits

...

247 Commits

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

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

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

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

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

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

the font size of the icon is increased slightly.

as the font size of h1 headings changes with the viewport size, we need
to center both the heading and the button vertically.
2024-12-02 22:19:03 +01:00
Florian
041ae7bae7 add Sum of DC Powrr
add Sum of DC power of all enabled inverters to Homeassistant MQTT autodiscovery
2024-11-22 08:21:13 +01:00
stefan123t
680863fb00
Update README.md 2024-11-20 08:02:07 +01:00
stefan123t
8297591853
change markdown table to github style 2024-11-20 08:00:26 +01:00
Thomas Basler
cc3290be8e Use correct variable 2024-11-13 19:21:41 +01:00
Thomas Basler
9bfded055a Remove not required string generation 2024-11-13 19:20:56 +01:00
Thomas Basler
2b07e3c2c8 Organize includes 2024-11-12 20:23:28 +01:00
Thomas Basler
eac2e2fb39 Fix: Really always execute the generate of the factory.bin file 2024-11-11 23:52:59 +01:00
Thomas Basler
d843ac6422 Fix comment 2024-11-11 22:11:57 +01:00
Thomas Basler
33a9b7454c Make function getClientId const 2024-11-10 02:45:42 +01:00
Thomas Basler
3dc70ab40a webapp: add app.js.gz 2024-11-07 19:13:45 +01:00
Thomas Basler
ecb5e9cc32 Build factory.bin in every compile attempt
This is required to apply changes  which are maybe only related to  the data directory.
2024-11-07 19:10:41 +01:00
Thomas Basler
9a53d6e209 Fix lint errors 2024-11-07 18:30:29 +01:00
Thomas Basler
63405a712c Upgrade ESPAsyncWebServer from 3.3.21 to 3.3.22 2024-11-07 18:21:59 +01:00
Thomas Basler
69c67f96e7 webapp: Update dependencies 2024-11-07 18:16:18 +01:00
Bernhard Kirchen
d088021902 webapp: declare emitted event in FormFooter component
fixes an annoying warning (visible in the browser console):

[Vue warn]: Extraneous non-emits event listeners (reload) were passed to
component but could not be automatically inherited because component
renders fragment or text root nodes. If the listener is intended to be a
component custom event listener only, declare it using the "emits"
option.
2024-11-06 22:34:56 +01:00
Thomas Basler
74e3947cb2 Merge branch 'pr2360' into dev 2024-11-06 19:55:03 +01:00
Thomas Basler
ca060e406e Remove not required include 2024-11-06 19:39:39 +01:00
Thomas Basler
53b496fd00 Replace multiline print by printf 2024-11-06 19:39:24 +01:00
Thomas Basler
ab60875142 Remove not required include 2024-11-06 19:24:37 +01:00
Bernhard Kirchen
3948adf460 keep console.log() when serving webapp
the removal of console and debugger statements by esbuild even when not
building for production seems to be a regression, as these were
definitely working in the past.

this change uses the command parameter to configure esbuild to either
keep or indeed remove the respective statements. they are only kept if
command is not "serve".

to avoid having to indent everything in defineConfig() by one block, the
return statement and closing curly brace were added "inline".
2024-11-05 19:21:45 +01:00
Bernhard Kirchen
3c56ec3738 webapp: fix inverter selection button breaking
on small viewports, the icon and the inverter label would be displayed
in two lines. this change keeps the icon and the label tied together in
any case, and the icon is centered vertically around the label.
2024-11-03 20:35:14 +01:00
Bernhard Kirchen
661ea6c022 webapp: always scroll up when navigating to another view 2024-11-02 00:32:49 +01:00
Bernhard Kirchen
3fa864ce52 webapp: device manager: optimize cards for tab nav
the top border of the card was breaking the design of the tabs, where
the active tab would be "visually connected" to the content. also, the
rounded border at the top did not blend in with the navbar's bottom
border.
2024-11-02 00:30:03 +01:00
Bernhard Kirchen
71f312d830 webapp: show pin mapping categories as cards
on a desktop browser, this approach allows to display all categories at
once. we also increase readability as the values are much closer to
their label. previously, the values were far to the right of the screen
and it was unpleasent to read which value belonged to which setting. the
grouping of values per category was also not very well conceived.

by using cards, we also avoid some styling issues, namely the use of
rowspan, which caused a spurious table cell border at the end of the old
table layout.
2024-11-02 00:28:18 +01:00
Bernhard Kirchen
f1c095e41d webapp: optimize placement of device profile doc buttons
* remove empty container for device profile links. if a device profile
  has no links, no buttons are generated, but a row was still part of
  the DOM, adding spurious space between the select and the alert with
  the hint.
2024-11-02 00:25:36 +01:00
Bernhard Kirchen
bac7179f73 webapp: consistently use no colon in form labels
there are no colons for table headers as well. some form labels had no
colon already, so this change uses a unified look among form labels.
2024-11-02 00:10:05 +01:00
Bernhard Kirchen
9f315207d4 webapp: optimize body bottom padding and length
long forms, when scrolled to the bottom, would leave no space between
the bottom of the viewport and the buttons, which is unpleasent.

short views would still createa large (high) body, for apparently no
reason.
2024-11-01 23:56:48 +01:00
Bernhard Kirchen
08f4d623c7 webapp: optimize look of login page
improve spacing and align login buton to the right, where all our
buttons are.
2024-11-01 23:55:25 +01:00
Bernhard Kirchen
f85297d52f webapp: inverter advanced tab needs space at the top
this avoids the input text box from colliding with the tab navigation
bottom border.
2024-11-01 23:54:39 +01:00
Bernhard Kirchen
e724fb8375 webapp: optimize look of firmware update cards 2024-11-01 23:53:24 +01:00
Bernhard Kirchen
54b4a2e9e8 webapp: properly space alert with hint for hostname 2024-11-01 23:50:28 +01:00
Bernhard Kirchen
94cecc23f5 webapp: avoid inline style for inverter channel info value 2024-11-01 23:46:35 +01:00
Bernhard Kirchen
866b539757 webapp: MQTT: no login with cert if TLS disabled
in the settings view we hide the "login with cert" setting while TLS is
disabled, so we should also hide that info in the info view when TLS is
disabled.
2024-11-01 23:46:01 +01:00
Bernhard Kirchen
9132a88963 webapp: MQTT: use v-if in favor of v-show
if we hide elements (which is done using style="display:none;"), they
are still part of the DOM and mess with CSS rules that shall apply to
the last element of a card or the last row of a table.
2024-11-01 23:44:35 +01:00
Bernhard Kirchen
eecd7f7c28 webapp: optimize spacing on bottom of cards
if the last child in a card (div.card > div.card-body) adds bottom
marging, we don't want the card to add more space through its
padding-bottom. most cards have children that add sufficient space
at the bottom anyways.
2024-11-01 23:43:51 +01:00
Bernhard Kirchen
ba304b2871 webapp: fix inverter "add" and "save order" button positions
the source tells us that the buttons are supposed to be on the right of
tha card, but the CSS broke at some point.
2024-11-01 23:42:56 +01:00
Bernhard Kirchen
0832d3e18c webapp: beautify radio statistics reset button
it would be nice to have this in the header of the accordion, which is
hard, but doable. however, clicking the button then also toggles the
accordion, which is unacceptable. preventing that seems non-trivial, as
the @click.stop() is not enough. also, nesting interactive elements is
simply bad practice. the button can also go to the right of header, with
reasonable effort, but the corner radii are then messed up and would
need to react interactively (accordion collapsed or not), which is also
a pain.

we now "float" the reset button to the right, add a nice icon, and give
the button some space so it at least looks like it belongs there.
2024-11-01 23:42:04 +01:00
Bernhard Kirchen
3c188f2f9f webapp: adjust look of tables in accordions to live view cards
this is relevant for the radio statistics table, as well as the tables
in the grid profile modal.
2024-11-01 23:40:52 +01:00
Bernhard Kirchen
68d2f7bf29 webapp: apply card-table class to info view cards
the cards in all information views still used a div.card-body around the
table, which added a margin on all sides of the table. to achieve a
unified look, these cards and tables now look the same as the inverter
channel cards.
2024-11-01 23:39:54 +01:00
Bernhard Kirchen
ad73fd8abd webapp: align table headers with card headers
set the left margin of table header cells to the same marging the card
header use, such that the text align on the same axis.
2024-11-01 23:39:00 +01:00
Bernhard Kirchen
e6a994fd7a webapp: use reasonable name for radio stats accordion 2024-11-01 23:38:29 +01:00
Bernhard Kirchen
d324a5c83f webapp: equalize style of cards with tables in live view
this change adjusts the style of cards showing tables such that they
look the same as inverter channel info tables.
2024-11-01 23:37:49 +01:00
Bernhard Kirchen
0aba1595df webapp: avoid inline style in inverter channel info card 2024-11-01 23:37:03 +01:00
Bernhard Kirchen
376912d821 webapp: add gap between inverter selectors 2024-11-01 23:36:17 +01:00
Bernhard Kirchen
d3eabc3311 webapp: remove table's bottom margin
we don't need a margin at the bottom of tables in general. not sure why
this is even a thing in bootstrap. this change, in particular, makes the
space between a table and a parent card symmetric on all sides.
2024-11-01 23:35:34 +01:00
Bernhard Kirchen
d06ea51c7a webapp: last table row shall have no bottom border
similar to the first row which has no border at the top.
2024-11-01 23:34:41 +01:00
Bernhard Kirchen
c750defc5f webapp: right-align labels for inputs on non-sm viewports
this change tries to achieve a pleasing look of input forms by
right-aligning the texts of labels. the input form now looks similar
to a table, achieving a cleaner look, especially for forms where the
labels have varying text lenghts.
2024-11-01 23:33:48 +01:00
Thomas Basler
130d90ce04 Merge branch 'pr2328' into dev 2024-11-01 22:12:41 +01:00
Thomas Basler
55c98ef880 Fix: skip BOM in JSON files (pin_mapping and config)
based on #2387
2024-11-01 22:07:48 +01:00
Thomas Basler
6c903abda1 Fix: Lint Error 2024-11-01 22:01:30 +01:00
Thomas Basler
28788070b2 Upgrade ESPAsyncWebServer from 3.3.17 to 3.3.21 2024-11-01 21:56:41 +01:00
Thomas Basler
0fc1ffc4d3 webapp: Update dependencies 2024-11-01 21:52:55 +01:00
Thomas Basler
8019eaf182 Feature: Validate JSON before uploading 2024-11-01 21:52:13 +01:00
Thomas Basler
225cab676a Fix: Take DST into account when recalculating the sunrise sunset time
If it is not considered the correct sunset / sunrise time is only calculated at the next day

Fixes: #2377
2024-10-27 14:03:44 +01:00
Thomas Basler
4594bcb23e Feature: Added device info for HMS-700 2024-10-26 14:51:03 +02:00
Thomas Basler
b21e8f8c80 Added README.md to lang folder 2024-10-25 23:04:14 +02:00
Thomas Basler
8566b08723 Feature: Added italian display translation 2024-10-25 22:42:47 +02:00
Thomas Basler
8452a8d110 Feature: Added spanish display translation 2024-10-25 22:39:25 +02:00
Thomas Basler
70f301941b Feature: Implement language pack support for display texts 2024-10-25 22:38:55 +02:00
Thomas Basler
d259042542 Rewrite display language handling to work with locale strings instead of magic numbers.
This is required to implement further i18n functions using the language packs
2024-10-25 21:43:29 +02:00
Thomas Basler
6113e0737b webapp: Fix: WaitRetstartView showed basic auth dialog 2024-10-25 21:42:52 +02:00
Bernhard Kirchen
b1edb13b3c add and use configuration write guard
the configuration write guard is now required when the configuration
struct shall be mutated. the write guards locks multiple writers against
each other and also, more importantly, makes the writes synchronous to
the main loop. all code running in the main loop can now be sure that
(1) reads from the configuration struct are non-preemtive and (2) the
configuration struct as a whole is in a consistent state when reading
from it.

NOTE that acquiring a write guard from within the main loop's task will
immediately cause a deadlock and the watchdog will trigger a reset. if
writing from inside the main loop should ever become necessary, the
write guard must be updated to only lock the mutex but not wait for a
signal.
2024-10-22 20:39:23 +02:00
Thomas Basler
2a21e53422 webapp: Rename interface to prevent lint errors 2024-10-21 22:41:52 +02:00
Thomas Basler
521fce35e4 webapp: Added global reboot wait screen 2024-10-21 21:59:38 +02:00
Thomas Basler
2e23c7e0ae Check if language pack metadata are valid 2024-10-21 20:15:56 +02:00
Thomas Basler
68c87c9217 Move lookup for translation path to separate method 2024-10-21 20:15:56 +02:00
Thomas Basler
4a247f5e94 Feature: Added italian language pack 2024-10-21 20:15:56 +02:00
Thomas Basler
05006e0642 Feature: Added spanish language pack 2024-10-21 20:15:56 +02:00
Thomas Basler
d9a8461a2e Feature: Allow custom language pack for webapp 2024-10-21 20:15:56 +02:00
Thomas Basler
c3d3d947d7 webapp: Allow upload of language packs 2024-10-21 19:02:50 +02:00
Thomas Basler
e29b86e4dc Add API endpoint to retrieve custom languages and complete language pack 2024-10-21 19:02:50 +02:00
Thomas Basler
8257eb7aa2 webapp: Use global AlertResponse interface 2024-10-19 17:39:12 +02:00
Thomas Basler
1e857b79c1 Feature: Refactor config management interface 2024-10-19 17:35:19 +02:00
Thomas Basler
16901482d9 Refactor file handling API and add endpoint to delete files 2024-10-19 12:40:43 +02:00
Thomas Basler
aa9f36ee8f Rename config API to file API 2024-10-19 11:07:15 +02:00
Thomas Basler
cf1693e1a0 Upgrade ESPAsyncWebServer from 3.3.16 to 3.3.17 2024-10-16 21:25:29 +02:00
Thomas Basler
e5cf12cebd Update nrf24/RF24 from 1.4.9 to 1.4.10 2024-10-16 21:24:01 +02:00
Thomas Basler
bcf4b70dc9 Fix: cpplint errors 2024-10-15 19:11:17 +02:00
Thomas Basler
dc5eb96f50 webapp: add app.js.gz 2024-10-15 18:22:57 +02:00
Thomas Basler
507e86d3b6 Upgrade ESPAsyncWebServer from 3.3.15 to 3.3.16 2024-10-15 18:12:41 +02:00
Thomas Basler
1900d78122 webapp: Update dependencies 2024-10-15 18:11:31 +02:00
Thomas Basler
b2522961cd Upgrade olikraus/U8g2 from 2.35.30 to 2.36.2 2024-10-14 19:46:17 +02:00
Thomas Basler
499f872641 Upgrade ESPAsyncWebServer from 3.3.14 to 3.3.15 2024-10-14 19:42:33 +02:00
Thomas Basler
fcf401d20a Upgrade ESPAsyncWebServer from 3.3.13 to 3.3.14 2024-10-13 12:33:12 +02:00
Thomas Basler
0468ccc34a webapp: Update dependencies 2024-10-12 21:38:07 +02:00
Thomas Basler
f8ad1acca9 Fix: Correct output of wifi disconnect reason code 2024-10-12 21:22:02 +02:00
Thomas Basler
36f0ed9ff8 Upgrade ESPAsyncWebServer from 3.3.12 to 3.3.13 2024-10-12 21:21:39 +02:00
Thomas Basler
ecef32a58e Merge branch 'pr2344' into dev 2024-10-12 13:13:24 +02:00
LennartF22
9ee947e9b0 Hotfix to not use DMA on SPI3 of ESP32-S2
See issue #2343.
2024-10-10 02:06:51 +02:00
Thomas Basler
e533320d92 Merge branch 'pr2115' into dev 2024-10-09 18:38:16 +02:00
Thomas Basler
4e293d4b93 Merge branch 'pr2340' into dev 2024-10-09 18:37:06 +02:00
Bernhard Kirchen
096a1ba3a0 Feature: show task details in system info view
shows whether or not known tasks are alive, and in particular shows how
much of the respective stack is still available.
2024-10-09 18:31:06 +02:00
LennartF22
6297ae3428 Don't set TX timeout to 0 anymore for HW/USB CDC
Due to a change in the Espressif Arduino core, the TX timeout for the HW CDC
(used in the ESP32-S3, for example) must not be set to 0, as otherwise, an
integer underflow occurs.

Removing the TX timeout is not necessary anymore anyways, because it is now
detected when CDC is not active, and attempts to write will return immediately
until the host read something again. Only when the transmit buffer becomes
full initially, the default timeout of just 100ms takes effect once.

For USB CDC (used with the ESP32-S2, for example), the timeout is not relevant
either.
2024-10-09 02:36:37 +02:00
Thomas Basler
e3b66f7ffe webapp: Update dependencies 2024-10-08 18:15:03 +02:00
Bernhard Kirchen
da9fb13079 webapp: pin assignment: hide unsupported pins
if the pin_mapping.json includes unsupported pins, e.g., `eth` pins on
an ESP32-S3, the whole category should still be hidden in the device
manager.
2024-10-06 22:37:05 +02:00
Thomas Basler
b7f830f64e webapp: add app.js.gz 2024-10-06 18:43:06 +02:00
Thomas Basler
90ea73b2ba Upgrade ESPAsyncWebServer from 3.3.11 to 3.3.12 2024-10-06 18:40:36 +02:00
Thomas Basler
eaa2f07cf3 Merge branch 'pr2333' into dev 2024-10-06 11:46:00 +02:00
Thomas Basler
b5ca2cfd21 Fix: "Equal brightness" in LED settings does not work correctly
fixes: #2332
2024-10-06 11:39:09 +02:00
Thomas Basler
2659204d96 Initialize the last rssi value with -127 instead of 0 to indicate a non existing connection of no data was received yet 2024-10-06 11:08:10 +02:00
LennartF22
6d048ae01d Remove EMAC related code for devices that don't have one 2024-10-06 03:08:58 +02:00
CommanderRedYT
d3d96b51ce
webapp: Fix eslint issues 2024-10-05 23:33:23 +02:00
Thomas Basler
4cd5d79c73 webapp: add app.js.gz 2024-10-05 22:14:14 +02:00
Thomas Basler
2c10e2510b webapp: Update dependencies 2024-10-05 22:12:49 +02:00
Thomas Basler
8f4b89a193 Replace format strings by platform independent macros 2024-10-05 00:50:13 +02:00
Thomas Basler
7dac96810f Rename NetworkEventCbList_t to DtuNetworkEventCbList_t for further upgrades 2024-10-04 23:02:12 +02:00
Thomas Basler
10b97fabb4 webapp: Update dependencies 2024-10-04 18:59:01 +02:00
Thomas Basler
d5abdc6d74 Upgrade ESPAsyncWebServer from 3.3.7 to 3.3.11 2024-10-04 18:44:23 +02:00
Thomas Basler
edfe06e31e Feature: Show RSSI of last received packet in radio stats
The value is also published via MQTT
2024-10-04 17:36:17 +02:00
janrombold
d0b2b972e2
Update UpgradePartition.md
Fixed typo
2024-10-04 00:19:26 +02:00
Thomas Basler
0c2b6f1a61 Fix: Add state_class to several Home Assistant sensors
state_class was added to yieldtotal, yieldday ac power and temperature for the whole dtu

closes: #2324
2024-10-02 18:13:12 +02:00
Thomas Basler
68793001a2 Merge branch 'pr2323' into dev 2024-10-02 11:50:43 +02:00
Thomas Basler
5040636aa2 Merge branch 'pr2322' into dev 2024-10-02 11:50:08 +02:00
mbo18
9df3e30bb2
Remove unused DEVICE_CLASS_TEMP 2024-10-02 11:02:52 +02:00
mbo18
38b5807ef7
Remove icon because device_class is set 2024-10-02 10:44:43 +02:00
Thomas Basler
2234ac9703 Upgrade ESPAsyncWebServer from 3.3.1 to 3.3.7 2024-10-02 10:32:58 +02:00
Thomas Basler
99a37fe01c webapp: Update dependencies 2024-09-30 18:47:41 +02:00
Thomas Basler
aa5087cc8a Merge branch 'pr2320' into dev 2024-09-30 16:02:58 +02:00
Bernhard Kirchen
d5d1a9982f Fix: force websocket clients to authenticate
when changing the security settings (disabling read-only access or
changing the password), existing websocket connections are now closed,
forcing the respective clients to authenticate (with the new password).
otherwise, existing websocket clients keep connected even though the
security settings now expect authentication with a (changed) password.
2024-09-30 15:54:55 +02:00
Bernhard Kirchen
ebb225f6c0 Fix: avoid deprecated setAuthentication() to fix memory exhaustion
with ESPAsyncWebServer 3.3.0, the setAuthentication() method became
deprecated and a replacement method was provided which acts as a shim
and uses the new middleware-based approach to setup authentication. in
order to eventually apply a changed "read-only access allowed" setting,
the setAuthentication() method was called periodically. the shim
implementation each time allocates a new AuthenticationMiddleware and
adds it to the chain of middlewares, eventually exhausting the memory.

we now use the new middleware-based approach ourselves and only add the
respective AuthenticatonMiddleware instance once to the respective
websocket server instance.

a regression where enabling unauthenticated read-only access is not
applied until reboot is also fixed. all the AuthenticationMiddleware
instances were never removed from the chain of middlewares when calling
setAuthentication("", "").
2024-09-30 15:16:30 +02:00
Thomas Basler
3a7295c341 Merge branch 'pr2311' into dev 2024-09-28 10:45:09 +02:00
LennartF22
69d2727106 Add device profiles for OpenDTU Fusion v2 PoE with displays 2024-09-28 02:42:31 +02:00
LennartF22
cafdb305a3 Adjust name of OpenDTU Fusion v2 PoE build environment 2024-09-28 02:37:09 +02:00
LennartF22
b05975b97c Prevent warning on GPIO ISR service registration 2024-09-28 02:26:40 +02:00
LennartF22
251bb7bd89 Add connection check for W5500 before full initialization 2024-09-28 02:26:36 +02:00
Bernhard Kirchen
6f9ded5f20 issue template: fix typo 2024-09-28 02:02:44 +02:00
Thomas Basler
b206cee820 webapp: add app.js.gz 2024-09-28 00:52:28 +02:00
Thomas Basler
759f899620 webapp: Update dependencies 2024-09-28 00:50:57 +02:00
Thomas Basler
d758a347eb Update espressif32 from 6.8.1 to 6.9.0 2024-09-27 19:36:52 +02:00
Thomas Basler
0fcf6061c1 Added required include to work with IDF 5 2024-09-27 18:30:44 +02:00
Thomas Basler
8b05bd22b5 Take care of different signature of ETH.begin method in Arduino Core 3.x 2024-09-27 18:27:26 +02:00
Thomas Basler
b85e0ab574 Add default values for ethernet pins in case they are not defined for a specific board 2024-09-27 17:35:33 +02:00
Thomas Basler
b43383007a Rename NetworkEventCb to DtuNetworkEventCb to prevent further upgrade issues 2024-09-27 17:32:28 +02:00
vaterlangen
d770566aec increase chunkSizeWarningLimit for webapp build (#1287)
increase from 500k (default) to 1024k in order to get rid of the warning messages.
2024-09-26 21:31:53 +02:00
Thomas Basler
12b9542f72 Added device profile for OpenDTU Fusion v2 PoE 2024-09-26 20:15:19 +02:00
Thomas Basler
a18e298cdd Apply automatic code formatting 2024-09-26 19:22:30 +02:00
Thomas Basler
7746d01fc0 Apply license headers and automatic code formatting to SpiManager 2024-09-26 18:47:27 +02:00
Thomas Basler
326525c961 Merge branch 'pr2306' into dev 2024-09-26 18:34:07 +02:00
Thomas Basler
355900743d webapp: add app.js.gz 2024-09-26 18:21:16 +02:00
Thomas Basler
818fdc42c9 Simplify inverter handling 2024-09-26 18:17:11 +02:00
Thomas Basler
595b153bbf Simplify network callback handling 2024-09-26 18:08:48 +02:00
Thomas Basler
cc7145361e webapp: Update dependencies 2024-09-26 18:01:59 +02:00
Thomas Basler
8db267b21a webapp: Apply auto format 2024-09-26 18:00:30 +02:00
Thomas Basler
8e26ef4e2e Fix: Only count RF RX packets when packets where sent
This mainly occours after a reset of  the statistics that receive count is higher then transmit count
2024-09-26 17:45:34 +02:00
Thomas Basler
67cae68e83 GitHub Build Action: Automatically generate littlefs image
If a data directory exists, the content of this directory will be placed in the littlefs image and embedded into the factory.bin file
2024-09-26 17:43:07 +02:00
Thomas Basler
468cbad4f3 Upgrade github actions/checkout to v4 2024-09-25 21:53:30 +02:00
Thomas Basler
d69a43373e Slight adjustments to github bug_report template 2024-09-25 21:49:38 +02:00
Thomas Basler
155735c828 Embed current branch into building process 2024-09-25 21:46:38 +02:00
Thomas Basler
0847f021f1 webapp: Update dependencies 2024-09-25 20:21:24 +02:00
Thomas Basler
9b565596d5 Feature: Allow reset of radio statistics via WebApp 2024-09-25 20:18:36 +02:00
LennartF22
31cf756a7e Only use a single SPI device for CMT 2024-09-25 00:37:06 +02:00
LennartF22
36da830f96 Use shared SPI bus for CMT and W5500 2024-09-25 00:37:06 +02:00
LennartF22
5457db269c Use SpiManager for nRF, CMT and W5500 2024-09-25 00:37:06 +02:00
LennartF22
ece4520687 Add Arduino SPI translation 2024-09-25 00:37:06 +02:00
LennartF22
1a583e765d Change cmt_spi3 implementation from C to C++ 2024-09-25 00:37:06 +02:00
LennartF22
4364daf54c Optimize CMT FIFO access 2024-09-25 00:37:06 +02:00
LennartF22
9b9c1e29f1 Add SpiManager library 2024-09-25 00:37:06 +02:00
LennartF22
851190dbcc Implement W5500 support 2024-09-25 00:37:03 +02:00
LennartF22
992e174bb2 Remove unnecessary delays 2024-09-25 00:31:05 +02:00
LennartF22
ec47e8978f Fix cs_ena_posttrans calculation 2024-09-25 00:31:05 +02:00
LennartF22
a02ad8b52c Remove unnecessary CMT SPI inversions 2024-09-25 00:31:05 +02:00
Thomas Basler
d3903d8602 MQTT Hass: Implement method to add common metadata to json output 2024-09-24 23:23:08 +02:00
Thomas Basler
2230850201 MQTT Hass: Implement device class as enum instead of String 2024-09-24 22:55:18 +02:00
Thomas Basler
bb4be0bbf7 MQTT Hass: Implement category as enum instead of String 2024-09-24 22:38:52 +02:00
Thomas Basler
2fb026074a Feature: Publish YieldTotal, YieldDay and Power of all inverters to Home Assistant 2024-09-24 22:16:17 +02:00
Thomas Basler
01e43777d2 MQTT Hass: Append dtu prefix topic for each single sensor 2024-09-24 22:04:07 +02:00
Thomas Basler
2213ad7bce MQTT Hass: Move serialization and allocation check into own method 2024-09-24 21:47:56 +02:00
Thomas Basler
9a318d5170 MQTT Hass: Reorder defines 2024-09-24 20:47:43 +02:00
Thomas Basler
c699f1b487 MQTT Hass: Add device_type and category to publishInverterBinarySensor 2024-09-24 20:45:55 +02:00
Thomas Basler
ac5a960581 MQTT Hass: Move yield into the publish method 2024-09-24 20:42:38 +02:00
Thomas Basler
239a77198d MQTT Hass: Move publishSensor logic into separate method 2024-09-24 20:38:12 +02:00
Thomas Basler
e5ca0ab784 MQTT Hass: Reorder binary sensor methods 2024-09-24 20:06:45 +02:00
Thomas Basler
f46a5017c7 MQTT Hass: Move publishBinarySensor logic into separate method 2024-09-24 20:03:42 +02:00
Thomas Basler
27910042ea MQTT Hass: Remove no more required checks 2024-09-24 19:47:23 +02:00
Thomas Basler
d899ea7364 MQTT Hass: Harmonise parameter names 2024-09-24 19:44:58 +02:00
Thomas Basler
7aca72b8fd MQTT Hass: Change parameter order for publishInverterNumber 2024-09-24 19:39:14 +02:00
Thomas Basler
483c10785b MQTT Hass: Change parameter order for publishInverterButton 2024-09-24 19:30:21 +02:00
Thomas Basler
a7100f238b MQTT Hass: Change parameter order for publishDtuBinarySensor 2024-09-24 19:23:04 +02:00
Thomas Basler
57c5b8c97e MQTT Hass: Make publish methods static 2024-09-24 19:22:05 +02:00
Thomas Basler
1c3e7de390 MQTT Hass: Change parameter order for publishDtuSensor 2024-09-24 19:21:06 +02:00
Thomas Basler
96e83f3d37 MQTT Hass: Change parameter order for publishInverterSensor 2024-09-24 19:18:56 +02:00
Thomas Basler
8e68632ed9 MQTT Hass: Rename caption parameter to name 2024-09-24 18:17:42 +02:00
Thomas Basler
8de1f7e70f MQTT Hass: Change char* to String& 2024-09-24 18:15:38 +02:00
Thomas Basler
bef81eed45 Feature: Publish Radio statistics to home assistant 2024-09-23 23:13:23 +02:00
Thomas Basler
181802a76b Feature: Allow reset of radio statistics via mqtt 2024-09-23 22:46:23 +02:00
Thomas Basler
0c012bf62a Move inverter housekeeping tasks inside the InverterAbstract class 2024-09-23 22:08:53 +02:00
Thomas Basler
93b6e5a885 Optimize MQTT subscription handling 2024-09-23 21:59:43 +02:00
Thomas Basler
d6a5fef4e7 Decrease restart delay to 1 second
This prevents a reload of the webapp (during firmware update) before the esp is online again
2024-09-23 18:33:01 +02:00
Thomas Basler
00584a0787 webapp: add app.js.gz 2024-09-23 18:25:49 +02:00
Thomas Basler
e29ac4f171 webapp: Fix data type for all range inputs 2024-09-23 18:24:22 +02:00
Thomas Basler
e37baedddb webapp: Update dependencies 2024-09-23 18:16:05 +02:00
Thomas Basler
e785904fca Fix: Restart was triggered before all website data was sent
This led to the effect that e.g. the confirmation messages where  not shown.

It is somehow related to ESPAsyncWebServer 3.3.0
2024-09-23 18:11:52 +02:00
Thomas Basler
5c460e26c9 Fix: Unable to CMT transmit power in WebApp
The pa_level was sent as string instead of a number.

fixes #2299
2024-09-23 17:57:29 +02:00
Thomas Basler
a3bd6dd7fb webapp: add app.js.gz 2024-09-22 19:01:43 +02:00
Thomas Basler
c4efda2e0c Added icon to radio statistics 2024-09-22 18:51:07 +02:00
Thomas Basler
a54b19bf5b Feature: Inverter radio statistics (rx/tx statistics)
The  statistics are shown in the WebApp and published via MQTT.
Statistics are reset at midnight.
2024-09-22 18:51:07 +02:00
Thomas Basler
1115418ce1 Publish temperature only if its not NAN 2024-09-22 18:51:07 +02:00
Thomas Basler
84e5c0821c Fix: Saving DTU config values just returned "Values are missing" 2024-09-22 13:16:34 +02:00
Thomas Basler
0c5e702a28 Fix: Wrong topic in home assistant auto discovery for maxalloc and minfree 2024-09-22 13:01:34 +02:00
Thomas Basler
a1fddb4ac1 Merge branch 'pr2293' into dev 2024-09-22 12:52:06 +02:00
Tobias Diedrich
fdcbf9de95 Publish ESP heap and temperature details on MQTT
I noticed that some useful ESP stats are missing on the MQTT feed, so this adds:

- ESP temperature
- ESP heap stats (size, free, minFree, maxAlloc)
2024-09-21 22:39:48 +02:00
Thomas Basler
175e5752fe Github Action: Update node version from 20 to 22 2024-09-21 20:30:42 +02:00
Thomas Basler
98f4aedbfb webapp: add app.js.gz 2024-09-21 19:09:32 +02:00
Thomas Basler
2f41f43d49 Update bblanchon/ArduinoJson from 7.1.0 to 7.2.0 2024-09-21 00:12:15 +02:00
Thomas Basler
3b3e6995c2 Fix: WebApp was not reloaded after firmware update
With the upgrade from ESPAsyncWebServer to 3.3.1 it seems that something has changed. Have to trigger the reboot from the main context.
2024-09-21 00:04:27 +02:00
Thomas Basler
34e1c43ca7 webapp: Fix html error in eventlog 2024-09-20 23:08:08 +02:00
Bernhard Kirchen
43394bc1bc actions: enable corepack to use fixed version of yarn
this allows us to fix the version of yarn, the Node.js package manager,
to a particular version. using corepack is the recommended way to use
yarn these days.
2024-09-20 22:30:46 +02:00
Marc-Philip
5ee411fcc6
Update patch_apply.py 2024-07-01 06:53:10 +02:00
Marc-Philip
1d92d9ed08
fix comment 2024-06-30 20:46:12 +02:00
Marc-Philip
fc1267fe55 massage file handling 2024-06-30 18:55:33 +02:00
186 changed files with 8621 additions and 4317 deletions

View File

@ -47,7 +47,8 @@ body:
label: Install Method label: Install Method
description: How did you install OpenDTU? description: How did you install OpenDTU?
options: options:
- Pre-Compiled binary from GitHub - Pre-Compiled binary from GitHub releases
- Pre-Compiled binary from GitHub actions/pull-request
- Self-Compiled - Self-Compiled
validations: validations:
required: true required: true
@ -92,5 +93,5 @@ body:
required: true required: true
- label: I have updated the title field above with a concise description. - label: I have updated the title field above with a concise description.
required: true required: true
- label: I have double checked that my inverter does not contain a W in the model name (like HMS-xxxW) as they are not supported - label: I have double checked that my inverter does not contain a W in the model name (like HMS-xxxW) as they are not supported.
required: true required: true

View File

@ -79,18 +79,27 @@ jobs:
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install --upgrade platformio setuptools pip install --upgrade platformio setuptools
- name: Enable Corepack
run: |
cd webapp
corepack enable
- name: Setup Node.js and yarn - name: Setup Node.js and yarn
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "20" node-version: "22"
cache: "yarn" cache: "yarn"
cache-dependency-path: "webapp/yarn.lock" cache-dependency-path: "webapp/yarn.lock"
- name: Install WebApp dependencies - name: Install WebApp dependencies
run: yarn --cwd webapp install --frozen-lockfile run: |
cd webapp
yarn install --frozen-lockfile
- name: Build WebApp - name: Build WebApp
run: yarn --cwd webapp build run: |
cd webapp
yarn build
- name: Build firmware - name: Build firmware
run: pio run -e ${{ matrix.environment }} run: pio run -e ${{ matrix.environment }}

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:

View File

@ -6,17 +6,23 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults:
run:
working-directory: webapp
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Enable Corepack
run: corepack enable
- name: Setup Node.js and yarn - name: Setup Node.js and yarn
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "20" node-version: "22"
cache: "yarn" cache: "yarn"
cache-dependency-path: "webapp/yarn.lock" cache-dependency-path: "webapp/yarn.lock"
- name: Install WebApp dependencies - name: Install WebApp dependencies
run: yarn --cwd webapp install --frozen-lockfile run: yarn install --frozen-lockfile
- name: Linting - name: Linting
run: yarn --cwd webapp lint run: yarn lint

View File

@ -6,17 +6,23 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults:
run:
working-directory: webapp
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Enable Corepack
run: corepack enable
- name: Setup Node.js and yarn - name: Setup Node.js and yarn
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "20" node-version: "22"
cache: "yarn" cache: "yarn"
cache-dependency-path: "webapp/yarn.lock" cache-dependency-path: "webapp/yarn.lock"
- name: Install WebApp dependencies - name: Install WebApp dependencies
run: yarn --cwd webapp install --frozen-lockfile run: yarn install --frozen-lockfile
- name: Check Formatting - name: Check Formatting
run: yarn --cwd webapp prettier --check src/ run: yarn prettier --check src/

View File

@ -1,6 +1,9 @@
[ [
{ {
"name": "OpenDTU Fusion v1", "name": "OpenDTU Fusion v1",
"links": [
{"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"}
],
"nrf24": { "nrf24": {
"miso": 48, "miso": 48,
"mosi": 35, "mosi": 35,
@ -25,6 +28,9 @@
}, },
{ {
"name": "OpenDTU Fusion v1 with SSD1306 Display", "name": "OpenDTU Fusion v1 with SSD1306 Display",
"links": [
{"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"}
],
"nrf24": { "nrf24": {
"miso": 48, "miso": 48,
"mosi": 35, "mosi": 35,
@ -54,6 +60,9 @@
}, },
{ {
"name": "OpenDTU Fusion v1 with SH1106 Display", "name": "OpenDTU Fusion v1 with SH1106 Display",
"links": [
{"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"}
],
"nrf24": { "nrf24": {
"miso": 48, "miso": 48,
"mosi": 35, "mosi": 35,
@ -83,6 +92,9 @@
}, },
{ {
"name": "OpenDTU Fusion v2 with CMT2300A and NRF24", "name": "OpenDTU Fusion v2 with CMT2300A and NRF24",
"links": [
{"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"}
],
"nrf24": { "nrf24": {
"miso": 48, "miso": 48,
"mosi": 35, "mosi": 35,
@ -115,6 +127,9 @@
}, },
{ {
"name": "OpenDTU Fusion v2 with CMT2300A, NRF24 and SH1106 Display", "name": "OpenDTU Fusion v2 with CMT2300A, NRF24 and SH1106 Display",
"links": [
{"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"}
],
"nrf24": { "nrf24": {
"miso": 48, "miso": 48,
"mosi": 35, "mosi": 35,
@ -152,6 +167,9 @@
}, },
{ {
"name": "OpenDTU Fusion v2 with CMT2300A, NRF24 and SSD1306 Display", "name": "OpenDTU Fusion v2 with CMT2300A, NRF24 and SSD1306 Display",
"links": [
{"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"}
],
"nrf24": { "nrf24": {
"miso": 48, "miso": 48,
"mosi": 35, "mosi": 35,
@ -186,5 +204,122 @@
"data": 2, "data": 2,
"clk": 1 "clk": 1
} }
},
{
"name": "OpenDTU Fusion v2 PoE",
"links": [
{"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"}
],
"nrf24": {
"miso": 48,
"mosi": 35,
"clk": 36,
"irq": 47,
"en": 38,
"cs": 37
},
"cmt": {
"clk": 6,
"cs": 4,
"fcs": 21,
"sdio": 5,
"gpio2": 3,
"gpio3": 8
},
"w5500": {
"mosi": 40,
"miso": 41,
"sclk": 39,
"cs": 42,
"int": 44,
"rst": 43
},
"led": {
"led0": 17,
"led1": 18
},
"display": {
"type": 0,
"data": 2,
"clk": 1
}
},
{
"name": "OpenDTU Fusion v2 PoE with SH1106 Display",
"links": [
{"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"}
],
"nrf24": {
"miso": 48,
"mosi": 35,
"clk": 36,
"irq": 47,
"en": 38,
"cs": 37
},
"cmt": {
"clk": 6,
"cs": 4,
"fcs": 21,
"sdio": 5,
"gpio2": 3,
"gpio3": 8
},
"w5500": {
"mosi": 40,
"miso": 41,
"sclk": 39,
"cs": 42,
"int": 44,
"rst": 43
},
"led": {
"led0": 17,
"led1": 18
},
"display": {
"type": 3,
"data": 2,
"clk": 1
}
},
{
"name": "OpenDTU Fusion v2 PoE with SSD1306 Display",
"links": [
{"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"}
],
"nrf24": {
"miso": 48,
"mosi": 35,
"clk": 36,
"irq": 47,
"en": 38,
"cs": 37
},
"cmt": {
"clk": 6,
"cs": 4,
"fcs": 21,
"sdio": 5,
"gpio2": 3,
"gpio3": 8
},
"w5500": {
"mosi": 40,
"miso": 41,
"sclk": 39,
"cs": 42,
"int": 44,
"rst": 43
},
"led": {
"led0": 17,
"led1": 18
},
"display": {
"type": 2,
"data": 2,
"clk": 1
}
} }
] ]

View File

@ -1,3 +1,3 @@
# Upgrade Partition # Upgrade Partition
This documentation will has been moved and can be found here: <https://tbnobody.github.io/OpenDTU-docs/firmware/howto/upgrade_partition/> This documentation has been moved and can be found here: <https://tbnobody.github.io/OpenDTU-docs/firmware/howto/upgrade_partition/>

View File

@ -3,9 +3,12 @@
#include "PinMapping.h" #include "PinMapping.h"
#include <cstdint> #include <cstdint>
#include <TaskSchedulerDeclarations.h>
#include <mutex>
#include <condition_variable>
#define CONFIG_FILENAME "/config.json" #define CONFIG_FILENAME "/config.json"
#define CONFIG_VERSION 0x00011c00 // 0.1.28 // make sure to clean all after change #define CONFIG_VERSION 0x00011d00 // 0.1.29 // make sure to clean all after change
#define WIFI_MAX_SSID_STRLEN 32 #define WIFI_MAX_SSID_STRLEN 32
#define WIFI_MAX_PASSWORD_STRLEN 64 #define WIFI_MAX_PASSWORD_STRLEN 64
@ -30,6 +33,7 @@
#define CHAN_MAX_NAME_STRLEN 31 #define CHAN_MAX_NAME_STRLEN 31
#define DEV_MAX_MAPPING_NAME_STRLEN 63 #define DEV_MAX_MAPPING_NAME_STRLEN 63
#define LOCALE_STRLEN 2
struct CHANNEL_CONFIG_T { struct CHANNEL_CONFIG_T {
uint16_t MaxChannelPower; uint16_t MaxChannelPower;
@ -144,7 +148,7 @@ struct CONFIG_T {
bool ScreenSaver; bool ScreenSaver;
uint8_t Rotation; uint8_t Rotation;
uint8_t Contrast; uint8_t Contrast;
uint8_t Language; char Locale[LOCALE_STRLEN + 1];
struct { struct {
uint32_t Duration; uint32_t Duration;
uint8_t Mode; uint8_t Mode;
@ -161,15 +165,32 @@ struct CONFIG_T {
class ConfigurationClass { class ConfigurationClass {
public: public:
void init(); void init(Scheduler& scheduler);
bool read(); bool read();
bool write(); bool write();
void migrate(); void migrate();
CONFIG_T& get(); CONFIG_T const& get();
class WriteGuard {
public:
WriteGuard();
CONFIG_T& getConfig();
~WriteGuard();
private:
std::unique_lock<std::mutex> _lock;
};
WriteGuard getWriteGuard();
INVERTER_CONFIG_T* getFreeInverterSlot(); INVERTER_CONFIG_T* getFreeInverterSlot();
INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial); INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial);
void deleteInverterById(const uint8_t id); void deleteInverterById(const uint8_t id);
private:
void loop();
Task _loopTask;
}; };
extern ConfigurationClass Configuration; extern ConfigurationClass Configuration;

View File

@ -40,7 +40,7 @@ public:
void setContrast(const uint8_t contrast); void setContrast(const uint8_t contrast);
void setStatus(const bool turnOn); void setStatus(const bool turnOn);
void setOrientation(const uint8_t rotation = DISPLAY_ROTATION); void setOrientation(const uint8_t rotation = DISPLAY_ROTATION);
void setLanguage(const uint8_t language); void setLocale(const String& locale);
void setDiagramMode(DiagramMode_t mode); void setDiagramMode(DiagramMode_t mode);
void setStartupDisplay(); void setStartupDisplay();
@ -65,7 +65,7 @@ private:
DisplayType_t _display_type = DisplayType_t::None; DisplayType_t _display_type = DisplayType_t::None;
DiagramMode_t _diagram_mode = DiagramMode_t::Off; DiagramMode_t _diagram_mode = DiagramMode_t::Off;
uint8_t _display_language = DISPLAY_LANGUAGE; String _display_language = DISPLAY_LOCALE;
uint8_t _mExtra; uint8_t _mExtra;
const uint16_t _period = 1000; const uint16_t _period = 1000;
const uint16_t _interval = 60000; // interval at which to power save (milliseconds) const uint16_t _interval = 60000; // interval at which to power save (milliseconds)
@ -73,6 +73,15 @@ private:
char _fmtText[32]; char _fmtText[32];
bool _isLarge = false; bool _isLarge = false;
uint8_t _lineOffsets[5]; uint8_t _lineOffsets[5];
String _i18n_offline;
String _i18n_yield_today_kwh;
String _i18n_yield_today_wh;
String _i18n_date_format;
String _i18n_current_power_kw;
String _i18n_current_power_w;
String _i18n_yield_total_mwh;
String _i18n_yield_total_kwh;
}; };
extern DisplayGraphicClass Display; extern DisplayGraphicClass Display;

35
include/I18n.h Normal file
View File

@ -0,0 +1,35 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <TaskSchedulerDeclarations.h>
#include <WString.h>
#include <list>
struct LanguageInfo_t {
String code;
String name;
String filename;
};
class I18nClass {
public:
I18nClass();
void init(Scheduler& scheduler);
std::list<LanguageInfo_t> getAvailableLanguages();
String getFilenameByLocale(const String& locale) const;
void readDisplayStrings(
const String& locale,
String& date_format,
String& offline,
String& power_w, String& power_kw,
String& yield_today_wh, String& yield_today_kwh,
String& yield_total_kwh, String& yield_total_mwh);
private:
void readLangPacks();
void readConfig(String file);
std::list<LanguageInfo_t> _availLanguages;
};
extern I18nClass I18n;

View File

@ -6,29 +6,42 @@
#include <TaskSchedulerDeclarations.h> #include <TaskSchedulerDeclarations.h>
// mqtt discovery device classes // mqtt discovery device classes
enum { enum DeviceClassType {
DEVICE_CLS_NONE = 0, DEVICE_CLS_NONE = 0,
DEVICE_CLS_CURRENT, DEVICE_CLS_CURRENT,
DEVICE_CLS_ENERGY, DEVICE_CLS_ENERGY,
DEVICE_CLS_PWR, DEVICE_CLS_PWR,
DEVICE_CLS_VOLTAGE, DEVICE_CLS_VOLTAGE,
DEVICE_CLS_FREQ, DEVICE_CLS_FREQ,
DEVICE_CLS_TEMP,
DEVICE_CLS_POWER_FACTOR, DEVICE_CLS_POWER_FACTOR,
DEVICE_CLS_REACTIVE_POWER DEVICE_CLS_REACTIVE_POWER,
DEVICE_CLS_CONNECTIVITY,
DEVICE_CLS_DURATION,
DEVICE_CLS_SIGNAL_STRENGTH,
DEVICE_CLS_TEMPERATURE,
DEVICE_CLS_RESTART
}; };
const char* const deviceClasses[] = { 0, "current", "energy", "power", "voltage", "frequency", "temperature", "power_factor", "reactive_power" }; const char* const deviceClass_name[] = { 0, "current", "energy", "power", "voltage", "frequency", "power_factor", "reactive_power", "connectivity", "duration", "signal_strength", "temperature", "restart" };
enum {
enum StateClassType {
STATE_CLS_NONE = 0, STATE_CLS_NONE = 0,
STATE_CLS_MEASUREMENT, STATE_CLS_MEASUREMENT,
STATE_CLS_TOTAL_INCREASING STATE_CLS_TOTAL_INCREASING
}; };
const char* const stateClasses[] = { 0, "measurement", "total_increasing" }; const char* const stateClass_name[] = { 0, "measurement", "total_increasing" };
enum CategoryType {
CATEGORY_NONE = 0,
CATEGORY_CONFIG,
CATEGORY_DIAGNOSTIC
};
const char* const category_name[] = { 0, "config", "diagnostic" };
typedef struct { typedef struct {
FieldId_t fieldId; // field id FieldId_t fieldId; // field id
uint8_t deviceClsId; // device class DeviceClassType deviceClsId; // device class
uint8_t stateClsId; // state class StateClassType stateClsId; // state class
} byteAssign_fieldDeviceClass_t; } byteAssign_fieldDeviceClass_t;
const byteAssign_fieldDeviceClass_t deviceFieldAssignment[] = { const byteAssign_fieldDeviceClass_t deviceFieldAssignment[] = {
@ -41,7 +54,7 @@ const byteAssign_fieldDeviceClass_t deviceFieldAssignment[] = {
{ FLD_IAC, DEVICE_CLS_CURRENT, STATE_CLS_MEASUREMENT }, { FLD_IAC, DEVICE_CLS_CURRENT, STATE_CLS_MEASUREMENT },
{ FLD_PAC, DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT }, { FLD_PAC, DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT },
{ FLD_F, DEVICE_CLS_FREQ, STATE_CLS_MEASUREMENT }, { FLD_F, DEVICE_CLS_FREQ, STATE_CLS_MEASUREMENT },
{ FLD_T, DEVICE_CLS_TEMP, STATE_CLS_MEASUREMENT }, { FLD_T, DEVICE_CLS_TEMPERATURE, STATE_CLS_MEASUREMENT },
{ FLD_PF, DEVICE_CLS_POWER_FACTOR, STATE_CLS_MEASUREMENT }, { FLD_PF, DEVICE_CLS_POWER_FACTOR, STATE_CLS_MEASUREMENT },
{ FLD_EFF, DEVICE_CLS_NONE, STATE_CLS_NONE }, { FLD_EFF, DEVICE_CLS_NONE, STATE_CLS_NONE },
{ FLD_IRR, DEVICE_CLS_NONE, STATE_CLS_NONE }, { FLD_IRR, DEVICE_CLS_NONE, STATE_CLS_NONE },
@ -58,13 +71,24 @@ public:
private: private:
void loop(); void loop();
void publish(const String& subtopic, const String& payload); static void publish(const String& subtopic, const String& payload);
void publishDtuSensor(const char* name, const char* device_class, const char* category, const char* icon, const char* unit_of_measure, const char* subTopic); static void publish(const String& subtopic, const JsonDocument& doc);
void publishDtuBinarySensor(const char* name, const char* device_class, const char* category, const char* payload_on, const char* payload_off, const char* subTopic = "");
void publishInverterField(std::shared_ptr<InverterAbstract> inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear = false); static void addCommonMetadata(JsonDocument& doc, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);
void publishInverterButton(std::shared_ptr<InverterAbstract> inv, const char* caption, const char* icon, const char* category, const char* deviceClass, const char* subTopic, const char* payload);
void publishInverterNumber(std::shared_ptr<InverterAbstract> inv, const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min = 1, const int16_t max = 100, float step = 1.0); // Binary Sensor
void publishInverterBinarySensor(std::shared_ptr<InverterAbstract> inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off); static void publishBinarySensor(JsonDocument& doc, const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);
static void publishDtuBinarySensor(const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);
static void publishInverterBinarySensor(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);
// Sensor
static void publishSensor(JsonDocument& doc, const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);
static void publishDtuSensor(const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);
static void publishInverterSensor(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);
static void publishInverterField(std::shared_ptr<InverterAbstract> inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear = false);
static void publishInverterButton(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& payload, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);
static void publishInverterNumber(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& command_topic, const int16_t min, const int16_t max, float step, const String& unit_of_measure, const String& icon, const StateClassType state_class, const CategoryType category);
static void createInverterInfo(JsonDocument& doc, std::shared_ptr<InverterAbstract> inv); static void createInverterInfo(JsonDocument& doc, std::shared_ptr<InverterAbstract> inv);
static void createDtuInfo(JsonDocument& doc); static void createDtuInfo(JsonDocument& doc);

View File

@ -5,6 +5,8 @@
#include <Hoymiles.h> #include <Hoymiles.h>
#include <TaskSchedulerDeclarations.h> #include <TaskSchedulerDeclarations.h>
#include <espMqttClient.h> #include <espMqttClient.h>
#include <frozen/map.h>
#include <frozen/string.h>
class MqttHandleInverterClass { class MqttHandleInverterClass {
public: public:
@ -19,7 +21,6 @@ public:
private: private:
void loop(); void loop();
void publishField(std::shared_ptr<InverterAbstract> inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId); void publishField(std::shared_ptr<InverterAbstract> inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId);
void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total);
Task _loopTask; Task _loopTask;
@ -41,6 +42,29 @@ private:
FLD_IRR, FLD_IRR,
FLD_Q FLD_Q
}; };
enum class Topic : unsigned {
LimitPersistentRelative,
LimitPersistentAbsolute,
LimitNonPersistentRelative,
LimitNonPersistentAbsolute,
Power,
Restart,
ResetRfStats,
};
static constexpr frozen::string _cmdtopic = "+/cmd/";
static constexpr frozen::map<frozen::string, Topic, 7> _subscriptions = {
{ "limit_persistent_relative", Topic::LimitPersistentRelative },
{ "limit_persistent_absolute", Topic::LimitPersistentAbsolute },
{ "limit_nonpersistent_relative", Topic::LimitNonPersistentRelative },
{ "limit_nonpersistent_absolute", Topic::LimitNonPersistentAbsolute },
{ "power", Topic::Power },
{ "restart", Topic::Restart },
{ "reset_rf_stats", Topic::ResetRfStats },
};
void onMqttMessage(Topic t, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total);
}; };
extern MqttHandleInverterClass MqttHandleInverter; extern MqttHandleInverterClass MqttHandleInverter;

View File

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

View File

@ -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 "W5500.h"
#include <DNSServer.h> #include <DNSServer.h>
#include <TaskSchedulerDeclarations.h> #include <TaskSchedulerDeclarations.h>
#include <WiFi.h> #include <WiFi.h>
@ -23,18 +24,18 @@ enum class network_event {
NETWORK_EVENT_MAX NETWORK_EVENT_MAX
}; };
typedef std::function<void(network_event event)> NetworkEventCb; typedef std::function<void(network_event event)> DtuNetworkEventCb;
typedef struct NetworkEventCbList { typedef struct DtuNetworkEventCbList {
NetworkEventCb cb; DtuNetworkEventCb cb;
network_event event; network_event event;
NetworkEventCbList() DtuNetworkEventCbList()
: cb(nullptr) : cb(nullptr)
, event(network_event::NETWORK_UNKNOWN) , event(network_event::NETWORK_UNKNOWN)
{ {
} }
} NetworkEventCbList_t; } DtuNetworkEventCbList_t;
class NetworkSettingsClass { class NetworkSettingsClass {
public: public:
@ -53,7 +54,7 @@ public:
bool isConnected() const; bool isConnected() const;
network_mode NetworkMode() const; network_mode NetworkMode() const;
bool onEvent(NetworkEventCb cbEvent, const network_event event = network_event::NETWORK_EVENT_MAX); bool onEvent(DtuNetworkEventCb cbEvent, const network_event event = network_event::NETWORK_EVENT_MAX);
void raiseEvent(const network_event event); void raiseEvent(const network_event event);
private: private:
@ -81,8 +82,9 @@ private:
bool _dnsServerStatus = false; bool _dnsServerStatus = false;
network_mode _networkMode = network_mode::Undefined; network_mode _networkMode = network_mode::Undefined;
bool _ethConnected = false; bool _ethConnected = false;
std::vector<NetworkEventCbList_t> _cbEventList; std::vector<DtuNetworkEventCbList_t> _cbEventList;
bool _lastMdnsEnabled = false; bool _lastMdnsEnabled = false;
std::unique_ptr<W5500> _w5500;
}; };
extern NetworkSettingsClass NetworkSettings; extern NetworkSettingsClass NetworkSettings;

View File

@ -12,6 +12,7 @@
struct PinMapping_t { struct PinMapping_t {
char name[MAPPING_NAME_STRLEN + 1]; char name[MAPPING_NAME_STRLEN + 1];
int8_t nrf24_miso; int8_t nrf24_miso;
int8_t nrf24_mosi; int8_t nrf24_mosi;
int8_t nrf24_clk; int8_t nrf24_clk;
@ -26,6 +27,14 @@ struct PinMapping_t {
int8_t cmt_gpio3; int8_t cmt_gpio3;
int8_t cmt_sdio; int8_t cmt_sdio;
int8_t w5500_mosi;
int8_t w5500_miso;
int8_t w5500_sclk;
int8_t w5500_cs;
int8_t w5500_int;
int8_t w5500_rst;
#if CONFIG_ETH_USE_ESP32_EMAC
int8_t eth_phy_addr; int8_t eth_phy_addr;
bool eth_enabled; bool eth_enabled;
int eth_power; int eth_power;
@ -33,11 +42,14 @@ struct PinMapping_t {
int eth_mdio; int eth_mdio;
eth_phy_type_t eth_type; eth_phy_type_t eth_type;
eth_clock_mode_t eth_clk_mode; eth_clock_mode_t eth_clk_mode;
#endif
uint8_t display_type; uint8_t display_type;
uint8_t display_data; uint8_t display_data;
uint8_t display_clk; uint8_t display_clk;
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];
}; };
@ -47,12 +59,19 @@ public:
bool init(const String& deviceMapping); bool init(const String& deviceMapping);
PinMapping_t& get(); PinMapping_t& get();
bool isMappingSelected() const { return _mappingSelected; }
bool isValidNrf24Config() const; bool isValidNrf24Config() const;
bool isValidCmt2300Config() const; bool isValidCmt2300Config() const;
bool isValidW5500Config() const;
#if CONFIG_ETH_USE_ESP32_EMAC
bool isValidEthConfig() const; bool isValidEthConfig() const;
#endif
private: private:
PinMapping_t _pinMapping; PinMapping_t _pinMapping;
bool _mappingSelected = false;
}; };
extern PinMappingClass PinMapping; extern PinMappingClass PinMapping;

18
include/RestartHelper.h Normal file
View File

@ -0,0 +1,18 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <TaskSchedulerDeclarations.h>
class RestartHelperClass {
public:
RestartHelperClass();
void init(Scheduler& scheduler);
void triggerRestart();
private:
void loop();
Task _rebootTask;
};
extern RestartHelperClass RestartHelper;

View File

@ -2,6 +2,7 @@
#pragma once #pragma once
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <LittleFS.h>
#include <cstdint> #include <cstdint>
class Utils { class Utils {
@ -9,7 +10,8 @@ public:
static uint32_t getChipId(); static uint32_t getChipId();
static uint64_t generateDtuSerial(); static uint64_t generateDtuSerial();
static int getTimezoneOffset(); static int getTimezoneOffset();
static void restartDtu();
static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line); static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line);
static void removeAllFiles(); static void removeAllFiles();
static String generateMd5FromFile(String file);
static void skipBom(File& f);
}; };

29
include/W5500.h Normal file
View File

@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Arduino.h>
#include <driver/spi_master.h>
#include <esp_eth.h> // required for esp_eth_handle_t
#include <esp_netif.h>
#include <memory>
class W5500 {
private:
explicit W5500(spi_device_handle_t spi, gpio_num_t pin_int);
public:
W5500(const W5500&) = delete;
W5500& operator=(const W5500&) = delete;
~W5500();
static std::unique_ptr<W5500> setup(int8_t pin_mosi, int8_t pin_miso, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst);
String macAddress();
private:
static bool connection_check_spi(spi_device_handle_t spi);
static bool connection_check_interrupt(gpio_num_t pin_int);
esp_eth_handle_t eth_handle;
esp_netif_t* eth_netif;
};

View File

@ -1,14 +1,15 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include "WebApi_config.h"
#include "WebApi_device.h" #include "WebApi_device.h"
#include "WebApi_devinfo.h" #include "WebApi_devinfo.h"
#include "WebApi_dtu.h" #include "WebApi_dtu.h"
#include "WebApi_errors.h" #include "WebApi_errors.h"
#include "WebApi_eventlog.h" #include "WebApi_eventlog.h"
#include "WebApi_file.h"
#include "WebApi_firmware.h" #include "WebApi_firmware.h"
#include "WebApi_gridprofile.h" #include "WebApi_gridprofile.h"
#include "WebApi_i18n.h"
#include "WebApi_inverter.h" #include "WebApi_inverter.h"
#include "WebApi_limit.h" #include "WebApi_limit.h"
#include "WebApi_maintenance.h" #include "WebApi_maintenance.h"
@ -30,6 +31,7 @@ class WebApiClass {
public: public:
WebApiClass(); WebApiClass();
void init(Scheduler& scheduler); void init(Scheduler& scheduler);
void reload();
static bool checkCredentials(AsyncWebServerRequest* request); static bool checkCredentials(AsyncWebServerRequest* request);
static bool checkCredentialsReadonly(AsyncWebServerRequest* request); static bool checkCredentialsReadonly(AsyncWebServerRequest* request);
@ -45,13 +47,14 @@ public:
private: private:
AsyncWebServer _server; AsyncWebServer _server;
WebApiConfigClass _webApiConfig;
WebApiDeviceClass _webApiDevice; WebApiDeviceClass _webApiDevice;
WebApiDevInfoClass _webApiDevInfo; WebApiDevInfoClass _webApiDevInfo;
WebApiDtuClass _webApiDtu; WebApiDtuClass _webApiDtu;
WebApiEventlogClass _webApiEventlog; WebApiEventlogClass _webApiEventlog;
WebApiFileClass _webApiFile;
WebApiFirmwareClass _webApiFirmware; WebApiFirmwareClass _webApiFirmware;
WebApiGridProfileClass _webApiGridprofile; WebApiGridProfileClass _webApiGridprofile;
WebApiI18nClass _webApiI18n;
WebApiInverterClass _webApiInverter; WebApiInverterClass _webApiInverter;
WebApiLimitClass _webApiLimit; WebApiLimitClass _webApiLimit;
WebApiMaintenanceClass _webApiMaintenance; WebApiMaintenanceClass _webApiMaintenance;

View File

@ -1,17 +0,0 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
class WebApiConfigClass {
public:
void init(AsyncWebServer& server, Scheduler& scheduler);
private:
void onConfigGet(AsyncWebServerRequest* request);
void onConfigDelete(AsyncWebServerRequest* request);
void onConfigListGet(AsyncWebServerRequest* request);
void onConfigUploadFinish(AsyncWebServerRequest* request);
void onConfigUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final);
};

View File

@ -18,9 +18,10 @@ enum WebApiError {
DtuInvalidCmtFrequency, DtuInvalidCmtFrequency,
DtuInvalidCmtCountry, DtuInvalidCmtCountry,
ConfigBase = 3000, FileBase = 3000,
ConfigNotDeleted, FileNotDeleted,
ConfigSuccess, FileSuccess,
FileDeleteSuccess,
InverterBase = 4000, InverterBase = 4000,
InverterSerialZero, InverterSerialZero,
@ -32,6 +33,7 @@ enum WebApiError {
InverterChanged, InverterChanged,
InverterDeleted, InverterDeleted,
InverterOrdered, InverterOrdered,
InverterStatsResetted,
LimitBase = 5000, LimitBase = 5000,
LimitSerialZero, LimitSerialZero,

18
include/WebApi_file.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 WebApiFileClass {
public:
void init(AsyncWebServer& server, Scheduler& scheduler);
private:
void onFileGet(AsyncWebServerRequest* request);
void onFileDelete(AsyncWebServerRequest* request);
void onFileDeleteAll(AsyncWebServerRequest* request);
void onFileListGet(AsyncWebServerRequest* request);
void onFileUploadFinish(AsyncWebServerRequest* request);
void onFileUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final);
};

14
include/WebApi_i18n.h Normal file
View File

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

View File

@ -14,4 +14,5 @@ private:
void onInverterEdit(AsyncWebServerRequest* request); void onInverterEdit(AsyncWebServerRequest* request);
void onInverterDelete(AsyncWebServerRequest* request); void onInverterDelete(AsyncWebServerRequest* request);
void onInverterOrder(AsyncWebServerRequest* request); void onInverterOrder(AsyncWebServerRequest* request);
void onInverterStatReset(AsyncWebServerRequest* request);
}; };

View File

@ -8,9 +8,11 @@ class WebApiWsConsoleClass {
public: public:
WebApiWsConsoleClass(); WebApiWsConsoleClass();
void init(AsyncWebServer& server, Scheduler& scheduler); void init(AsyncWebServer& server, Scheduler& scheduler);
void reload();
private: private:
AsyncWebSocket _ws; AsyncWebSocket _ws;
AsyncAuthenticationMiddleware _simpleDigestAuth;
Task _wsCleanupTask; Task _wsCleanupTask;
void wsCleanupTaskCb(); void wsCleanupTaskCb();

View File

@ -11,6 +11,7 @@ class WebApiWsLiveClass {
public: public:
WebApiWsLiveClass(); WebApiWsLiveClass();
void init(AsyncWebServer& server, Scheduler& scheduler); void init(AsyncWebServer& server, Scheduler& scheduler);
void reload();
private: private:
static void generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv); static void generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv);
@ -24,6 +25,7 @@ private:
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
AsyncWebSocket _ws; AsyncWebSocket _ws;
AsyncAuthenticationMiddleware _simpleDigestAuth;
uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 }; uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 };

View File

@ -5,4 +5,5 @@
extern const char *__COMPILED_GIT_HASH__; extern const char *__COMPILED_GIT_HASH__;
extern const char *__COMPILED_GIT_BRANCH__;
// extern const char *__COMPILED_DATE_TIME_UTC_STR__; // extern const char *__COMPILED_DATE_TIME_UTC_STR__;

View File

@ -99,7 +99,7 @@
#define DISPLAY_SCREENSAVER true #define DISPLAY_SCREENSAVER true
#define DISPLAY_ROTATION 2U #define DISPLAY_ROTATION 2U
#define DISPLAY_CONTRAST 60U #define DISPLAY_CONTRAST 60U
#define DISPLAY_LANGUAGE 0U #define DISPLAY_LOCALE "en"
#define DISPLAY_DIAGRAM_DURATION (10UL * 60UL * 60UL) #define DISPLAY_DIAGRAM_DURATION (10UL * 60UL * 60UL)
#define DISPLAY_DIAGRAM_MODE 1U #define DISPLAY_DIAGRAM_MODE 1U
@ -108,3 +108,5 @@
#define LED_BRIGHTNESS 100U #define LED_BRIGHTNESS 100U
#define MAX_INVERTER_LIMIT 2250 #define MAX_INVERTER_LIMIT 2250
#define LANG_PACK_SUFFIX ".lang.json"

9
lang/README.md Normal file
View File

@ -0,0 +1,9 @@
# Language Packs
This folder contains language packs for OpenDTU which can be uploaded to the
device using the "Config Management" function.
Select "Language Pack" in the restore section, select a `.json` file containing
your language and press "Restore". Afterwards all language selection drop down
menues contain the new language.
Create a pull to request to share your own language pack (or corrections) with the community.

696
lang/es.lang.json Normal file
View File

@ -0,0 +1,696 @@
{
"meta": {
"name": "Español",
"code": "es"
},
"display": {
"date_format": "%d/%m/%Y %H:%M",
"offline": "Apagado",
"power_w": "%.0f W",
"power_kw": "%.1f kW",
"yield_today_wh": "Hoy: %4.0f Wh",
"yield_today_kwh": "Hoy: %.1f kWh",
"yield_total_kwh": "Total: %.1f kWh",
"yield_total_mwh": "Total: %.0f kWh"
},
"webapp": {
"menu": {
"LiveView": "Vista en directo",
"Settings": "Ajustes",
"NetworkSettings": "Ajustes de Red",
"NTPSettings": "Ajustes NTP",
"MQTTSettings": "Ajustes MQTT",
"InverterSettings": "Ajustes Inversor",
"SecuritySettings": "Ajustes Seguridad",
"DTUSettings": "Ajustes DTU",
"DeviceManager": "Administrador Dispositivos",
"ConfigManagement": "Gestión configuración",
"FirmwareUpgrade": "Actualización Firmware",
"DeviceReboot": "Reinicio Dispositivo",
"Info": "Info",
"System": "Sistema",
"Network": "Red",
"NTP": "NTP",
"MQTT": "MQTT",
"Console": "Consola",
"About": "Acerca",
"Logout": "Logout",
"Login": "Login"
},
"base": {
"Loading": "Cargando...",
"Reload": "Recargar",
"Cancel": "Cancelar",
"Save": "Guardar",
"Refreshing": "Refrescando",
"Pull": "Tira hacia abajo para refrescar",
"Release": "Soltar para refrescar",
"Close": "Cerrar",
"Yes": "Yes",
"No": "No"
},
"wait": {
"NotReady": "OpenDTU is not yet ready",
"PleaseWait": "Please wait. You will be automatically redirected to the home page."
},
"Error": {
"Oops": "Oops!"
},
"localeswitcher": {
"Dark": "Oscuro",
"Light": "Claro",
"Auto": "Automático"
},
"apiresponse": {
"1001": "¡Opciones guardadas!",
"1002": "No se encontraron valores",
"1003": "Datos demasiado grandes",
"1004": "Fallo al procesar los datos",
"1005": "Faltan valores",
"1006": "Fallo en la escritura",
"2001": "¡El número de serie no puede ser cero!",
"2002": "Intervalo de Poll interval debe ser mayor que cero!",
"2003": "Configuración de potencia incorrecta!",
"2004": "La frecuencia debe estar entre {min} y {max} kHz y debe ser un múltiplo de 250 kHz!",
"2005": "Modelo desconocido! Por favor, informe el \"Modelo de pieza de hardware\" y el modelo (por ejemplo, HM-350) como un problema en <a href=\"https://github.com/tbnobody/OpenDTU/issues\" target=\"_blank\">aquí</a>.",
"3001": "No se eliminó nada",
"3002": "Configuración borrada. Reinicio en curso...",
"4001": "@:apiresponse.2001",
"4002": "El nombre debe tener entre 1 y {max} caracteres de longitud!",
"4003": "Solo se admiten {max} inversores!",
"4004": "Inversor creado!",
"4005": "ID no válido especificado",
"4006": "Cantidad de canales máxima incorrecta dada!",
"4007": "Inversor modificado!",
"4008": "Inversor eliminado!",
"4009": "Orden de inversores guardado!",
"5001": "@:apiresponse.2001",
"5002": "Límite debe estar entre 1 y {max}!",
"5003": "Tipo incorrecto especificado!",
"5004": "Inversor incorrecto especificado!",
"6001": "Reinicio desencadenado!",
"6002": "Reinicio cancelado!",
"7001": "¡El servidor MQTT debe tener entre 1 y {max} caracteres de longitud!",
"7002": "¡El nombre de usuario debe no tener más de {max} caracteres!",
"7003": "¡La contraseña debe no tener más de {max} caracteres!",
"7004": "¡El tema debe tener entre 1 y {max} caracteres de longitud!",
"7005": "¡El tema no debe contener caracteres de espacio!",
"7006": "¡El tema debe terminar con barra inclinada (/)!",
"7007": "¡El puerto debe ser un número entre 1 y 65535!",
"7008": "¡El certificado debe tener entre 1 y {max} caracteres de longitud!",
"7009": "¡El tema LWT debe tener entre 1 y {max} caracteres de longitud!",
"7010": "¡El tema LWT no debe contener caracteres de espacio!",
"7011": "¡El valor LWT en línea debe tener entre 1 y {max} caracteres de longitud!",
"7012": "¡El valor LWT fuera de línea debe tener entre 1 y {max} caracteres de longitud!",
"7013": "¡El intervalo de publicación debe ser un número entre {min} y {max}!",
"7014": "¡El tema Hass debe tener entre 1 y {max} caracteres de longitud!",
"7015": "¡El tema Hass no debe contener caracteres de espacio!",
"7016": "¡La QoS LWT no debe ser mayor que {max}!",
"7017": "Client ID must not longer then {max} characters!",
"8001": "¡La dirección IP no es válida!",
"8002": "¡La máscara de red no es válida!",
"8003": "¡El gateway no es válido!",
"8004": "¡La dirección IP del servidor DNS 1 no es válida!",
"8005": "¡La dirección IP del servidor DNS 2 no es válida!",
"8006": "¡El valor de tiempo de espera del punto de acceso administrativo es inválido!",
"9001": "¡El servidor NTP debe tener entre 1 y {max} caracteres de longitud!",
"9002": "¡La zona horaria debe tener entre 1 y {max} caracteres de longitud!",
"9003": "¡La descripción de la zona horaria debe tener entre 1 y {max} caracteres de longitud!",
"9004": "¡El año debe ser un número entre {min} y {max}!",
"9005": "¡El mes debe ser un número entre {min} y {max}!",
"9006": "¡El día debe ser un número entre {min} y {max}!",
"9007": "¡La hora debe ser un número entre {min} y {max}!",
"9008": "¡Los minutos deben ser un número entre {min} y {max}!",
"9009": "¡Los segundos deben ser un número entre {min} y {max}!",
"9010": "¡Hora actualizada!",
"10001": "¡La contraseña debe tener entre 8 y {max} caracteres de longitud!",
"10002": "¡Autenticación exitosa!",
"11001": "¡@:apiresponse.2001",
"11002": "¡@:apiresponse:5004",
"12001": "¡El perfil debe tener entre 1 y {max} caracteres de longitud!"
},
"home": {
"LiveData": "Datos en Vivo",
"SerialNumber": "Número de Serie: ",
"CurrentLimit": "Límite de Corriente: ",
"DataAge": "Edad de los Datos: ",
"Seconds": "{val} segundos",
"ShowSetInverterLimit": "Ver / Establecer Límite del Inversor",
"TurnOnOff": "Encender/Apagar el Inversor",
"ShowInverterInfo": "Ver Información del Inversor",
"ShowEventlog": "Ver Registro de Eventos",
"UnreadMessages": "mensajes sin leer",
"Loading": "@:base.Cargando",
"EventLog": "Registro de Eventos",
"InverterInfo": "Información del Inversor",
"LimitSettings": "Configuración de Límites",
"LastLimitSetStatus": "Último Estado de Configuración del Límite:",
"SetLimit": "Establecer Límite:",
"Relative": "Relativo (%)",
"Absolute": "Absoluto (W)",
"LimitHint": "<b>Consejo:</b> Si establece el límite como un valor absoluto, la visualización del valor actual solo se actualizará después de ~4 minutos.",
"SetPersistent": "Establecer Límite Permanente",
"SetNonPersistent": "Establecer Límite No Permanente",
"PowerSettings": "Configuración de Energía",
"LastPowerSetStatus": "Último Estado de Configuración de Energía:",
"TurnOn": "Encender",
"TurnOff": "Apagar",
"Restart": "Reiniciar",
"Failure": "Fallo",
"Pending": "Pendiente",
"Ok": "Aceptar",
"Unknown": "Desconocido",
"ShowGridProfile": "Ver Perfil de la Red",
"GridProfile": "Perfil de la Red",
"LoadingInverter": "Waiting for data... (can take up to 10 seconds)",
"RadioStats": "Radio Statistics",
"TxRequest": "TX Request Count",
"RxSuccess": "RX Success",
"RxFailNothing": "RX Fail: Receive Nothing",
"RxFailPartial": "RX Fail: Receive Partial",
"RxFailCorrupt": "RX Fail: Receive Corrupt",
"TxReRequest": "TX Re-Request Fragment",
"StatsReset": "Reset Statistics",
"StatsResetting": "Resetting...",
"Rssi": "RSSI of last received packet",
"RssiHint": "HM inverters only support RSSI values < -64 dBm and > -64 dBm. In this case, -80 dbm and -30 dbm is shown.",
"dBm": "{dbm} dBm"
},
"eventlog": {
"Start": "Iniciar",
"Stop": "Parar",
"Id": "ID",
"Message": "Mensaje"
},
"devinfo": {
"NoInfo": "Sin información disponible",
"NoInfoLong": "No se ha recibido ningún dato válido del inversor hasta ahora. Todavía estamos intentando...",
"UnknownModel": "¡Modelo desconocido! Por favor, informe el \"Número de parte de hardware\" y el modelo (por ejemplo, HM-350) como un problema <a href=\"https://github.com/tbnobody/OpenDTU/issues\" target=\"_blank\">aquí</a>.",
"Serial": "Número de serie",
"ProdYear": "Año de producción",
"ProdWeek": "Semana de producción",
"Model": "Modelo",
"DetectedMaxPower": "Potencia máxima detectada",
"BootloaderVersion": "Versión del cargador de arranque",
"FirmwareVersion": "Versión del firmware",
"FirmwareBuildDate": "Fecha de construcción del firmware",
"HardwarePartNumber": "Número de parte de hardware",
"HardwareVersion": "Versión de hardware",
"SupportsPowerDistributionLogic": "'Power Distribution Logic' supported",
"Yes": "@:base.Yes",
"No": "@:base.No"
},
"gridprofile": {
"NoInfo": "@:devinfo.NoInfo",
"NoInfoLong": "@:devinfo.NoInfoLong",
"Name": "Nombre",
"Version": "Versión",
"Enabled": "@:wifistationinfo.Enabled",
"Disabled": "@:wifistationinfo.Disabled",
"GridprofileSupport": "Apoyar el desarrollo",
"GridprofileSupportLong": "Por favor, consulte <a href=\"https://github.com/tbnobody/OpenDTU/wiki/Grid-Profile-Parser\" target=\"_blank\">aquí</a> para obtener más información."
},
"systeminfo": {
"SystemInfo": "Información del sistema",
"VersionError": "Error al obtener información de la versión",
"VersionNew": "¡Nueva versión disponible! ¡Mostrar cambios!",
"VersionOk": "¡Actualizado!"
},
"firmwareinfo": {
"FirmwareInformation": "Información del firmware",
"Hostname": "Hostname",
"SdkVersion": "Versión del SDK",
"ConfigVersion": "Versión de la configuración",
"FirmwareVersion": "Versión del firmware / Hash de Git",
"PioEnv": "Entorno PIO",
"FirmwareVersionHint": "Haga clic aquí para mostrar información sobre su versión actual",
"FirmwareUpdate": "Actualización de firmware",
"FirmwareUpdateHint": "Haga clic aquí para ver las diferencias entre su versión y la última versión",
"FrmwareUpdateAllow": "Al activar la comprobación de actualización, se envía una solicitud a GitHub.com cada vez que se llama a la página para recuperar la versión actualmente disponible. Si no está de acuerdo con esto, deje esta función desactivada.",
"ResetReason0": "Razón de reinicio CPU 0",
"ResetReason1": "Razón de reinicio CPU 1",
"ConfigSaveCount": "Contador de guardado de configuración",
"Uptime": "Tiempo de actividad",
"UptimeValue": "0 días {time} | 1 día {time} | {count} días {time}"
},
"hardwareinfo": {
"HardwareInformation": "Información del hardware",
"ChipModel": "Modelo de chip",
"ChipRevision": "Revisión de chip",
"ChipCores": "Núcleos del chip",
"CpuFrequency": "Frecuencia de la CPU",
"Mhz": "MHz",
"CpuTemperature": "CPU Temperature",
"FlashSize": "Flash Memory Size"
},
"memoryinfo": {
"MemoryInformation": "Información de la memoria",
"Type": "Tipo",
"Usage": "Uso",
"Free": "Libre",
"Used": "Usado",
"Size": "Tamaño",
"Heap": "Montón",
"PsRam": "PSRAM",
"LittleFs": "LittleFs",
"Sketch": "Boceto"
},
"heapdetails": {
"HeapDetails": "Detalles del montón",
"TotalFree": "Total libre",
"LargestFreeBlock": "Bloque libre contiguo más grande",
"MaxUsage": "Uso máximo desde el inicio",
"Fragmentation": "Nivel de fragmentación"
},
"taskdetails": {
"TaskDetails": "Task Details",
"Name": "Name",
"StackFree": "Stack Free",
"Priority": "Priority",
"Task_idle0": "Idle (CPU Core 0)",
"Task_idle1": "Idle (CPU Core 1)",
"Task_wifi": "Wi-Fi",
"Task_tit": "TCP/IP",
"Task_looptask": "Arduino Main Loop",
"Task_asynctcp": "Async TCP",
"Task_mqttclient": "MQTT Client",
"Task_huaweican0": "AC Charger CAN",
"Task_pmsdm": "PowerMeter (SDM)",
"Task_pmhttpjson": "PowerMeter (HTTP+JSON)",
"Task_pmsml": "PowerMeter (Serial SML)",
"Task_pmhttpsml": "PowerMeter (HTTP+SML)"
},
"radioinfo": {
"RadioInformation": "Información de la radio",
"Status": "Estado de {module}",
"ChipStatus": "Estado del chip de {module}",
"ChipType": "Tipo de chip de {module}",
"Connected": "conectado",
"NotConnected": "no conectado",
"Configured": "configurado",
"NotConfigured": "no configurado",
"Unknown": "Desconocido"
},
"networkinfo": {
"NetworkInformation": "Información de la red"
},
"wifistationinfo": {
"WifiStationInfo": "Información de WiFi (Estación)",
"Status": "Estado",
"Enabled": "habilitado",
"Disabled": "deshabilitado",
"Ssid": "SSID",
"Bssid": "BSSID",
"Quality": "Calidad",
"Rssi": "RSSI"
},
"wifiapinfo": {
"WifiApInfo": "Información de WiFi (Punto de acceso)",
"Status": "@:wifistationinfo.Status",
"Enabled": "@:wifistationinfo.Enabled",
"Disabled": "@:wifistationinfo.Disabled",
"Ssid": "@:wifistationinfo.Ssid",
"Stations": "# Estaciones"
},
"interfacenetworkinfo": {
"NetworkInterface": "Interfaz de red ({iface})",
"Hostname": "@:firmwareinfo.Hostname",
"IpAddress": "Dirección IP",
"Netmask": "Máscara de red",
"DefaultGateway": "Puerta de enlace predeterminada",
"Dns": "DNS {num}",
"MacAddress": "Dirección MAC"
},
"interfaceapinfo": {
"NetworkInterface": "Interfaz de red (Punto de acceso)",
"IpAddress": "@:interfacenetworkinfo.IpAddress",
"MacAddress": "@:interfacenetworkinfo.MacAddress"
},
"ntpinfo": {
"NtpInformation": "Información de NTP",
"ConfigurationSummary": "Resumen de configuración",
"Server": "Servidor",
"Timezone": "Zona horaria",
"TimezoneDescription": "Descripción de la zona horaria",
"CurrentTime": "Hora actual",
"Status": "Estado",
"Synced": "sincronizado",
"NotSynced": "no sincronizado",
"LocalTime": "Hora local",
"Sunrise": "Amanecer",
"Sunset": "Atardecer",
"NotAvailable": "No disponible",
"Mode": "Modo",
"Day": "Día",
"Night": "Noche"
},
"mqttinfo": {
"MqttInformation": "Información de MQTT",
"ConfigurationSummary": "@:ntpinfo.ConfigurationSummary",
"Status": "@:ntpinfo.Status",
"Enabled": "Habilitado",
"Disabled": "Deshabilitado",
"Server": "@:ntpinfo.Server",
"Port": "Puerto",
"ClientId": "Client ID",
"Username": "Nombre de usuario",
"BaseTopic": "Tema base",
"PublishInterval": "Intervalo de publicación",
"Seconds": "{sec} segundos",
"CleanSession": "Bandera CleanSession",
"Retain": "Retener",
"Tls": "TLS",
"RootCertifcateInfo": "Información del certificado raíz de CA",
"TlsCertLogin": "Iniciar sesión con certificado TLS",
"ClientCertifcateInfo": "Información del Certificado del Cliente",
"HassSummary": "Resumen de la Configuración de Descubrimiento Automático MQTT de Home Assistant",
"Expire": "Expirar",
"IndividualPanels": "Paneles Individuales",
"RuntimeSummary": "Resumen de Tiempo de Ejecución",
"ConnectionStatus": "Estado de Conexión",
"Connected": "conectado",
"Disconnected": "desconectado"
},
"console": {
"Console": "Consola",
"VirtualDebugConsole": "Consola de Depuración Virtual",
"EnableAutoScroll": "Habilitar Desplazamiento Automático",
"ClearConsole": "Limpiar Consola",
"CopyToClipboard": "Copiar al Portapapeles"
},
"inverterchannelinfo": {
"String": "Cadena {num}",
"Phase": "Fase {num}",
"General": "General"
},
"invertertotalinfo": {
"TotalYieldTotal": "Total de Rendimiento Acumulado",
"TotalYieldDay": "Total de Rendimiento del Día",
"TotalPower": "Potencia Total"
},
"inverterchannelproperty": {
"Power": "Potencia",
"Voltage": "Voltaje",
"Current": "Corriente",
"Power DC": "Potencia DC",
"YieldDay": "Rendimiento del Día",
"YieldTotal": "Rendimiento Total",
"Frequency": "Frecuencia",
"Temperature": "Temperatura",
"PowerFactor": "Factor de Potencia",
"ReactivePower": "Potencia Reactiva",
"Efficiency": "Eficiencia",
"Irradiation": "Irradiación"
},
"maintenancereboot": {
"DeviceReboot": "Reinicio del Dispositivo",
"PerformReboot": "Realizar Reinicio",
"Reboot": "¡Reiniciar!",
"Cancel": "@:base.Cancel",
"RebootOpenDTU": "Reiniciar OpenDTU",
"RebootQuestion": "¿Realmente desea reiniciar el dispositivo?",
"RebootHint": "<b>Nota:</b> Normalmente no es necesario realizar un reinicio manual. OpenDTU realiza cualquier reinicio necesario (por ejemplo, después de una actualización de firmware) automáticamente. También se adoptan configuraciones sin reiniciar. Si necesita reiniciar debido a un error, considere informarlo en <a href=\"https://github.com/tbnobody/OpenDTU/issues\" class=\"alert-link\" target=\"_blank\">https://github.com/tbnobody/OpenDTU/issues</a>."
},
"dtuadmin": {
"DtuSettings": "Configuración de DTU",
"DtuConfiguration": "Configuración de DTU",
"Serial": "Serial",
"SerialHint": "Tanto el inversor como el DTU tienen un número de serie. El número de serie del DTU se genera aleatoriamente en el primer inicio y generalmente no es necesario cambiarlo.",
"PollInterval": "Intervalo de Sondeo",
"Seconds": "Segundos",
"NrfPaLevel": "Potencia de Transmisión NRF24",
"CmtPaLevel": "Potencia de Transmisión CMT2300A",
"NrfPaLevelHint": "Utilizado para inversores HM. Asegúrese de que su fuente de alimentación sea lo suficientemente estable antes de aumentar la potencia de transmisión.",
"CmtPaLevelHint": "Utilizado para inversores HMS/HMT. Asegúrese de que su fuente de alimentación sea lo suficientemente estable antes de aumentar la potencia de transmisión.",
"CmtCountry": "Región/País CMT2300A",
"CmtCountryHint": "Cada país tiene asignaciones de frecuencia diferentes.",
"country_0": "Europa ({min}MHz - {max}MHz)",
"country_1": "América del Norte ({min}MHz - {max}MHz)",
"country_2": "Brasil ({min}MHz - {max}MHz)",
"CmtFrequency": "Frecuencia CMT2300A",
"CmtFrequencyHint": "¡Asegúrese de utilizar solo frecuencias permitidas en el país respectivo! Después de un cambio de frecuencia, puede tardar hasta 15 minutos en establecer una conexión.",
"CmtFrequencyWarning": "La frecuencia seleccionada está fuera del rango permitido en su región/país seleccionado. Asegúrese de que esta selección no infrinja ninguna regulación local.",
"MHz": "{mhz} MHz",
"dBm": "{dbm} dBm",
"Min": "Mínimo ({db} dBm)",
"Low": "Bajo ({db} dBm)",
"High": "Alto ({db} dBm)",
"Max": "Máximo ({db} dBm)"
},
"securityadmin": {
"SecuritySettings": "Configuración de Seguridad",
"AdminPassword": "Contraseña de Administrador",
"Password": "Contraseña",
"RepeatPassword": "Repetir Contraseña",
"PasswordHint": "<b>Consejo:</b> La contraseña de administrador se utiliza para acceder a esta interfaz web (usuario 'admin'), pero también para conectarse al dispositivo cuando está en modo AP. Debe tener 8 a 64 caracteres.",
"Permissions": "Permisos",
"ReadOnly": "Permitir acceso de solo lectura a la interfaz web sin contraseña"
},
"ntpadmin": {
"NtpSettings": "Configuración de NTP",
"NtpConfiguration": "Configuración de NTP",
"TimeServer": "Servidor de Tiempo",
"TimeServerHint": "El valor predeterminado es adecuado siempre que OpenDTU tenga acceso directo a Internet.",
"Timezone": "Zona Horaria",
"TimezoneConfig": "Configuración de Zona Horaria",
"LocationConfiguration": "Configuración de Ubicación",
"Longitude": "Longitud",
"Latitude": "Latitud",
"SunSetType": "Tipo de Atardecer",
"SunSetTypeHint": "Afecta al cálculo día/noche. Puede tardar hasta un minuto en aplicarse el nuevo tipo.",
"OFFICIAL": "Amanecer estándar (90.8°)",
"NAUTICAL": "Amanecer náutico (102°)",
"CIVIL": "Amanecer civil (96°)",
"ASTONOMICAL": "Amanecer astronómico (108°)",
"ManualTimeSynchronization": "Sincronización Manual del Tiempo",
"CurrentOpenDtuTime": "Hora Actual de OpenDTU",
"CurrentLocalTime": "Hora Local Actual",
"SynchronizeTime": "Sincronizar Tiempo",
"SynchronizeTimeHint": "<b>Consejo:</b> Puede utilizar la sincronización manual del tiempo para establecer la hora actual de OpenDTU si no hay un servidor NTP disponible. Pero tenga en cuenta que en caso de un ciclo de energía, se perderá la hora. Además, tenga en cuenta que la precisión del tiempo se verá gravemente afectada, ya que no se puede resincronizar regularmente y el microcontrolador ESP32 no tiene un reloj en tiempo real."
},
"networkadmin": {
"NetworkSettings": "Configuración de Red",
"WifiConfiguration": "Configuración de WiFi",
"WifiSsid": "SSID de WiFi",
"WifiPassword": "Contraseña de WiFi",
"Hostname": "Nombre de Host",
"HostnameHint": "<b>Consejo:</b> El texto <span class=\"font-monospace\">%06X</span> se remplazará con los últimos 6 dígitos del ChipID de ESP en formato hexadecimal.",
"EnableDhcp": "Habilitar DHCP",
"StaticIpConfiguration": "Configuración de IP Estática",
"IpAddress": "Dirección IP",
"Netmask": "Máscara de Red",
"DefaultGateway": "Puerta de Enlace Predeterminada",
"Dns": "Servidor DNS {num}",
"AdminAp": "Configuración de WiFi (Punto de Acceso de Administrador)",
"ApTimeout": "Tiempo de espera del Punto de Acceso",
"ApTimeoutHint": "Tiempo que se mantiene abierto el Punto de Acceso. Un valor de 0 significa infinito.",
"Minutes": "minutos",
"EnableMdns": "Habilitar mDNS",
"MdnsSettings": "Configuración de mDNS"
},
"mqttadmin": {
"MqttSettings": "Configuración de MQTT",
"MqttConfiguration": "Configuración de MQTT",
"EnableMqtt": "Habilitar MQTT",
"EnableHass": "Habilitar Descubrimiento Automático MQTT de Home Assistant",
"MqttBrokerParameter": "Parámetros del Broker MQTT",
"Hostname": "Nombre de Host",
"HostnameHint": "Nombre de host o dirección IP",
"Port": "Puerto",
"ClientId": "Client ID",
"Username": "Nombre de Usuario",
"UsernameHint": "Nombre de usuario, dejar vacío para conexión anónima",
"Password": "Contraseña",
"PasswordHint": "Contraseña, dejar vacío para conexión anónima",
"BaseTopic": "Tema Base",
"BaseTopicHint": "Tema base, se antepondrá a todos los temas publicados (por ejemplo, inverter/)",
"PublishInterval": "Intervalo de Publicación",
"Seconds": "segundos",
"CleanSession": "Habilitar Bandera CleanSession",
"EnableRetain": "Habilitar Bandera Retain",
"EnableTls": "Habilitar TLS",
"RootCa": "Certificado Raíz CA (predeterminado Letsencrypt)",
"TlsCertLoginEnable": "Habilitar Inicio de Sesión con Certificado TLS",
"ClientCert": "Certificado del Cliente TLS",
"ClientKey": "Clave del Cliente TLS",
"LwtParameters": "Parámetros de LWT",
"LwtTopic": "Tema de LWT",
"LwtTopicHint": "Tema de LWT, se añadirá al tema base",
"LwtOnline": "Mensaje de LWT en línea",
"LwtOnlineHint": "Mensaje que se publicará en el tema de LWT cuando esté en línea",
"LwtOffline": "Mensaje de LWT fuera de línea",
"LwtOfflineHint": "Mensaje que se publicará en el tema de LWT cuando esté fuera de línea",
"LwtQos": "QoS (Calidad de Servicio)",
"QOS0": "0 (Como máximo una vez)",
"QOS1": "1 (Al menos una vez)",
"QOS2": "2 (Exactamente una vez)",
"HassParameters": "Parámetros de Descubrimiento Automático MQTT de Home Assistant",
"HassPrefixTopic": "Tema de Prefijo",
"HassPrefixTopicHint": "El prefijo para el tema de descubrimiento",
"HassRetain": "Habilitar Bandera Retain",
"HassExpire": "Habilitar Expiración",
"HassIndividual": "Paneles Individuales"
},
"inverteradmin": {
"InverterSettings": "Configuración del Inversor",
"AddInverter": "Agregar un nuevo Inversor",
"Serial": "Serial",
"Name": "Nombre",
"Add": "Agregar",
"AddHint": "<b>Consejo:</b> Puede configurar parámetros adicionales después de haber creado el inversor. Use el ícono de lápiz en la lista de inversores.",
"InverterList": "Lista de Inversores",
"Status": "Estado",
"Send": "Enviar",
"Receive": "Recibir",
"StatusHint": "<b>Consejo:</b> El inversor se alimenta con su entrada de CC. Si no hay sol, el inversor está apagado. Aún se pueden enviar solicitudes.",
"Type": "Tipo",
"Action": "Acción",
"SaveOrder": "Guardar orden",
"DeleteInverter": "Eliminar inversor",
"EditInverter": "Editar inversor",
"General": "General",
"String": "Cadena",
"Advanced": "Avanzado",
"InverterSerial": "Serial del Inversor:",
"InverterName": "Nombre del Inversor:",
"InverterNameHint": "Aquí puede especificar un nombre personalizado para su inversor.",
"InverterStatus": "Recibir / Enviar",
"PollEnable": "Sondear datos del inversor",
"PollEnableNight": "Sondear datos del inversor por la noche",
"CommandEnable": "Enviar comandos",
"CommandEnableNight": "Enviar comandos por la noche",
"StringName": "Nombre de cadena {num}:",
"StringNameHint": "Aquí puede especificar un nombre personalizado para el puerto respectivo de su inversor.",
"StringMaxPower": "Potencia máxima de cadena {num}:",
"StringMaxPowerHint": "Ingrese la potencia máxima de los paneles solares conectados.",
"StringYtOffset": "Compensación total de rendimiento de cadena {num}:",
"StringYtOffsetHint": "Esta compensación se aplica al valor total de rendimiento leído del inversor. Esto se puede usar para ajustar el rendimiento total del inversor a cero si se utiliza un inversor usado. Pero aún puede intentar sondear datos.",
"InverterHint": "*) Ingrese W<sub>p</sub> del canal para calcular la irradiación.",
"ReachableThreshold": "Umbral de Alcanzabilidad",
"ReachableThresholdHint": "Define cuántas solicitudes se permiten fallar hasta que el inversor se considere no alcanzable.",
"ZeroRuntime": "Datos de tiempo cero",
"ZeroRuntimeHint": "Datos de tiempo cero (sin datos de rendimiento) si el inversor se vuelve inalcanzable.",
"ZeroDay": "Rendimiento diario cero a medianoche",
"ZeroDayHint": "Esto solo funciona si el inversor es inalcanzable. Si se leen datos del inversor, se usarán sus valores. (El reinicio solo ocurre en el ciclo de energía)",
"ClearEventlog": "Clear Eventlog at midnight",
"Cancel": "@:base.Cancel",
"Save": "@:base.Save",
"DeleteMsg": "¿Está seguro de que desea eliminar el inversor \"{name}\" con número de serie {serial}?",
"Delete": "Eliminar",
"YieldDayCorrection": "Corrección de Rendimiento Diario",
"YieldDayCorrectionHint": "Sumar el rendimiento diario incluso si el inversor se reinicia. El valor se restablecerá a medianoche"
},
"fileadmin": {
"ConfigManagement": "Gestión de Configuración",
"BackupHeader": "Copia de seguridad: Copia de Seguridad del Archivo de Configuración",
"BackupConfig": "Copia de seguridad del archivo de configuración",
"Backup": "Copia de seguridad",
"Restore": "Restaurar",
"NoFileSelected": "Ningún archivo seleccionado",
"RestoreHeader": "Restaurar: Restaurar el Archivo de Configuración",
"Back": "Atrás",
"UploadSuccess": "Carga Exitosa",
"RestoreHint": "<b>Nota:</b> Esta operación reemplaza el archivo de configuración con la configuración restaurada y reinicia OpenDTU para aplicar todas las configuraciones.",
"ResetHeader": "Inicializar: Realizar Restablecimiento de Fábrica",
"FactoryResetButton": "Restaurar Configuraciones Predeterminadas de Fábrica",
"ResetHint": "<b>Nota:</b> Haga clic en Restaurar Configuraciones Predeterminadas de Fábrica para restaurar e inicializar las configuraciones predeterminadas de fábrica y reiniciar.",
"FactoryReset": "Restablecimiento de Fábrica",
"ResetMsg": "¿Está seguro de que desea eliminar la configuración actual y restablecer todas las configuraciones a sus valores predeterminados de fábrica?",
"ResetConfirm": "Restablecimiento de Fábrica",
"Cancel": "@:base.Cancel",
"InvalidJson": "JSON file is formatted incorrectly.",
"InvalidJsonContent": "JSON file has the wrong content."
},
"login": {
"Login": "Iniciar Sesión",
"SystemLogin": "Inicio de Sesión en el Sistema",
"Username": "Nombre de Usuario",
"UsernameRequired": "Se requiere el nombre de usuario",
"Password": "Contraseña",
"PasswordRequired": "Se requiere la contraseña",
"LoginButton": "Iniciar Sesión"
},
"firmwareupgrade": {
"FirmwareUpgrade": "Actualización de Firmware",
"Loading": "@:base.Loading",
"OtaError": "Error OTA",
"Back": "Atrás",
"Retry": "Reintentar",
"OtaStatus": "Estado OTA",
"OtaSuccess": "La carga de firmware fue exitosa. El dispositivo se reinició automáticamente. Cuando el dispositivo vuelva a ser accesible, la interfaz se recargará automáticamente.",
"FirmwareUpload": "Carga de Firmware",
"UploadProgress": "Progreso de Carga"
},
"about": {
"AboutOpendtu": "Acerca de OpenDTU",
"Documentation": "Documentation",
"DocumentationBody": "The firmware and hardware documentation can be found here: <a href=\"https://www.opendtu.solar\" target=\"_blank\">https://www.opendtu.solar</a>",
"ProjectOrigin": "Origen del Proyecto",
"ProjectOriginBody1": "Este proyecto se inició a partir de <a href=\"https://www.mikrocontroller.net/topic/525778\" target=\"_blank\">esta discusión. (Mikrocontroller.net)</a>",
"ProjectOriginBody2": "El protocolo de Hoymiles fue descifrado mediante los esfuerzos voluntarios de muchos participantes. OpenDTU, entre otros, se desarrolló basado en este trabajo. El proyecto está bajo una Licencia de Código Abierto (<a href=\"https://www.gnu.de/documents/gpl-2.0.de.html\" target=\"_blank\">Licencia Pública General de GNU versión 2</a>).",
"ProjectOriginBody3": "El software se desarrolló según nuestro mejor conocimiento y creencia. Sin embargo, no se acepta ninguna responsabilidad por un mal funcionamiento o pérdida de garantía del inversor.",
"ProjectOriginBody4": "OpenDTU está disponible de forma gratuita. Si pagaste dinero por el software, probablemente te estafaron.",
"NewsUpdates": "Noticias y Actualizaciones",
"NewsUpdatesBody": "Las nuevas actualizaciones se pueden encontrar en Github: <a href=\"https://github.com/tbnobody/OpenDTU\" target=\"_blank\">https://github.com/tbnobody/OpenDTU</a>",
"ErrorReporting": "Reporte de Errores",
"ErrorReportingBody": "Por favor, informa problemas utilizando la función proporcionada por <a href=\"https://github.com/tbnobody/OpenDTU/issues\" target=\"_blank\">Github</a>",
"Discussion": "Discusión",
"DiscussionBody": "Discute con nosotros en <a href=\"https://discord.gg/WzhxEY62mB\" target=\"_blank\">Discord</a> o <a href=\"https://github.com/tbnobody/OpenDTU/discussions\" target=\"_blank\">Github</a>"
},
"hints": {
"RadioProblem": "No se pudo conectar a un módulo de radio configurado. Por favor, verifica la conexión.",
"TimeSync": "El reloj aún no ha sido sincronizado. Sin un reloj correctamente ajustado, no se realizan solicitudes al inversor. Esto es normal poco después del inicio. Sin embargo, después de un tiempo de ejecución más largo (>1 minuto), indica que el servidor NTP no es accesible.",
"TimeSyncLink": "Por favor, verifica la configuración de tu hora.",
"DefaultPassword": "Estás utilizando la contraseña predeterminada para la interfaz web y el punto de acceso de emergencia. Esto potencialmente es inseguro.",
"DefaultPasswordLink": "Por favor, cambia la contraseña.",
"PinMappingIssue": "You are using a generic firmware image, but have not yet uploaded a file with device profiles (<code>pin_mapping.json</code>) or have not selected a profile defined there. Please refer to the <a href=\"https://opendtu.solar/firmware/device_profiles/\" target=\"_blank\" class=\"alert-link\">documentation</a> for details."
},
"deviceadmin": {
"DeviceManager": "Administrador de Dispositivos",
"ParseError": "Error de análisis en 'pin_mapping.json': {error}",
"PinAssignment": "Configuración de Conexión",
"SelectedProfile": "Perfil Seleccionado",
"DefaultProfile": "(Configuraciones predeterminadas)",
"ProfileHint": "Tu dispositivo puede dejar de responder si seleccionas un perfil incompatible. En este caso, debes realizar una eliminación a través de la interfaz serial.",
"Display": "Pantalla",
"PowerSafe": "Habilitar Ahorro de Energía",
"PowerSafeHint": "Apaga la pantalla si no hay un inversor produciendo.",
"Screensaver": "Habilitar Protector de Pantalla",
"ScreensaverHint": "Mueve la pantalla un poco en cada actualización para evitar el quemado. (Útil especialmente para pantallas OLED)",
"DiagramMode": "Modo de Diagrama",
"off": "Apagar",
"small": "Pequeño",
"fullscreen": "Pantalla Completa",
"DiagramDuration": "Duración del Diagrama",
"DiagramDurationHint": "El período de tiempo que se muestra en el diagrama.",
"Seconds": "Segundos",
"Contrast": "Contraste ({contrast})",
"Rotation": "Rotación",
"rot0": "Sin rotación",
"rot90": "Rotación de 90 grados",
"rot180": "Rotación de 180 grados",
"rot270": "Rotación de 270 grados",
"DisplayLanguage": "Idioma de la Pantalla",
"en": "Inglés",
"de": "Alemán",
"fr": "Francés",
"Leds": "LEDs",
"EqualBrightness": "Brillo Equitativo",
"LedBrightness": "Brillo del LED {led} ({brightness})"
},
"pininfo": {
"Category": "Categoría",
"Name": "Nombre",
"Number": "Número",
"ValueSelected": "Seleccionado",
"ValueActive": "Activo"
},
"inputserial": {
"format_hoymiles": "Hoymiles serial number format",
"format_converted": "Already converted serial number",
"format_herf_valid": "E-Star HERF format (will be saved converted): {serial}",
"format_herf_invalid": "E-Star HERF format: Invalid checksum",
"format_unknown": "Unknown format"
}
}
}

696
lang/it.lang.json Normal file
View File

@ -0,0 +1,696 @@
{
"meta": {
"name": "Italiano",
"code": "it"
},
"display": {
"date_format": "%d/%m/%Y %H:%M",
"offline": "Offline",
"power_w": "%.0f W",
"power_kw": "%.1f kW",
"yield_today_wh": "oggi: %4.0f Wh",
"yield_today_kwh": "oggi: %.1f kWh",
"yield_total_kwh": "totale: %.1f kWh",
"yield_total_mwh": "totale: %.0f kWh"
},
"webapp": {
"menu": {
"LiveView": "Dati in tempo reale",
"Settings": "Impostazioni",
"NetworkSettings": "Impostazioni di rete",
"NTPSettings": "Impostazioni NTP",
"MQTTSettings": "Impostazioni MQTT",
"InverterSettings": "Impostazioni Inverter",
"SecuritySettings": "Impostazioni di Sicurezza",
"DTUSettings": "Impostazioni DTU",
"DeviceManager": "Gestione Dispositivi",
"ConfigManagement": "Gestione Configurazione",
"FirmwareUpgrade": "Aggiornamento Firmware",
"DeviceReboot": "Riavvio DTU",
"Info": "Info",
"System": "Sistema",
"Network": "Rete",
"NTP": "NTP",
"MQTT": "MQTT",
"Console": "Console",
"About": "Informazioni DTU",
"Logout": "Esci",
"Login": "Login"
},
"base": {
"Loading": "Caricamento...",
"Reload": "Ricarica",
"Cancel": "Cancella",
"Save": "Salva",
"Refreshing": "Aggiorna",
"Pull": "Trascina in basso per aggiornare",
"Release": "Rilascia per aggiornare",
"Close": "Chiudi",
"Yes": "Yes",
"No": "No"
},
"wait": {
"NotReady": "OpenDTU is not yet ready",
"PleaseWait": "Please wait. You will be automatically redirected to the home page."
},
"Error": {
"Oops": "Oops!"
},
"localeswitcher": {
"Dark": "Scuro",
"Light": "Chiaro",
"Auto": "Automatico"
},
"apiresponse": {
"1001": "Settings saved!",
"1002": "No values found!",
"1003": "Data too large!",
"1004": "Failed to parse data!",
"1005": "Values are missing!",
"1006": "Write failed!",
"2001": "Serial cannot be zero!",
"2002": "Poll interval must be greater zero!",
"2003": "Invalid power level setting!",
"2004": "The frequency must be set between {min} and {max} kHz and must be a multiple of 250kHz!",
"2005": "Invalid country selection!",
"3001": "Not deleted anything!",
"3002": "Configuration resettet. Rebooting now...",
"4001": "@:apiresponse.2001",
"4002": "Name must between 1 and {max} characters long!",
"4003": "Only {max} inverters are supported!",
"4004": "Inverter created!",
"4005": "Invalid ID specified!",
"4006": "Invalid amount of max channel setting given!",
"4007": "Inverter changed!",
"4008": "Inverter deleted!",
"4009": "Inverter order saved!",
"5001": "@:apiresponse.2001",
"5002": "Limit must between 1 and {max}!",
"5003": "Invalid type specified!",
"5004": "Invalid inverter specified!",
"6001": "Reboot triggered!",
"6002": "Reboot cancled!",
"7001": "MQTT Server must between 1 and {max} characters long!",
"7002": "Username must not longer then {max} characters!",
"7003": "Password must not longer then {max} characters!",
"7004": "Topic must not longer then {max} characters!",
"7005": "Topic must not contain space characters!",
"7006": "Topic must end with slash (/)!",
"7007": "Port must be a number between 1 and 65535!",
"7008": "Certificate must not longer then {max} characters!",
"7009": "LWT topic must not longer then {max} characters!",
"7010": "LWT topic must not contain space characters!",
"7011": "LWT online value must not longer then {max} characters!",
"7012": "LWT offline value must not longer then {max} characters!",
"7013": "Publish interval must be a number between {min} and {max}!",
"7014": "Hass topic must not longer then {max} characters!",
"7015": "Hass topic must not contain space characters!",
"7016": "LWT QOS must not greater then {max}!",
"7017": "Client ID must not longer then {max} characters!",
"8001": "IP address is invalid!",
"8002": "Netmask is invalid!",
"8003": "Gateway is invalid!",
"8004": "DNS Server IP 1 is invalid!",
"8005": "DNS Server IP 2 is invalid!",
"8006": "Administrative AccessPoint Timeout value is invalid",
"9001": "NTP Server must between 1 and {max} characters long!",
"9002": "Timezone must between 1 and {max} characters long!",
"9003": "Timezone description must between 1 and {max} characters long!",
"9004": "Year must be a number between {min} and {max}!",
"9005": "Month must be a number between {min} and {max}!",
"9006": "Day must be a number between {min} and {max}!",
"9007": "Hour must be a number between {min} and {max}!",
"9008": "Minute must be a number between {min} and {max}!",
"9009": "Second must be a number between {min} and {max}!",
"9010": "Time updated!",
"10001": "Password must between 8 and {max} characters long!",
"10002": "Authentication successful!",
"11001": "@:apiresponse.2001",
"11002": "@:apiresponse:5004",
"12001": "Profil must between 1 and {max} characters long!"
},
"home": {
"LiveData": "Dati in tempo reale",
"SerialNumber": "Numero seriale: ",
"CurrentLimit": "Limite attuale: ",
"DataAge": "Aggiornamento Dati: ",
"Seconds": "{val} secondi",
"ShowSetInverterLimit": "Mostra / Imposta Limite di Potenza",
"TurnOnOff": "Accendi/Spegni Inverter",
"ShowInverterInfo": "Mostra info Inverter",
"ShowEventlog": "Mostra Log Eventi",
"UnreadMessages": "msg non letti",
"Loading": "@:base.Loading",
"EventLog": "Log Eventi",
"InverterInfo": "Info Inverter",
"LimitSettings": "Impostazioni Limite Potenza",
"LastLimitSetStatus": "Stato ultimo limite impostato:",
"SetLimit": "Imposta Limite a:",
"Relative": "Percentuale (%)",
"Absolute": "Assoluto (W)",
"LimitHint": "<b>Nota:</b> Se imposti il limite assoluto, il valore sul display sarà aggiornato dopo circa 4 minuti.",
"SetPersistent": "Imposta Limite in Modo Persistente",
"SetNonPersistent": "Imposta Limite Temporaneamente",
"PowerSettings": "Impostazioni Potenza",
"LastPowerSetStatus": "Ultimo Stato dell'Inverter:",
"TurnOn": "Accendi Inverter",
"TurnOff": "Spegni Inverter",
"Restart": "Riavvia Inverter",
"Failure": "Fallito",
"Pending": "In Attesa",
"Ok": "Ok",
"Unknown": "Sconosciuto",
"ShowGridProfile": "Mostra Settaggi Inverter",
"GridProfile": "Settaggi Inverter",
"LoadingInverter": "In attesa dei dati... (puo' richiedere fino a 10 secondi)",
"RadioStats": "Radio Statistics",
"TxRequest": "TX Request Count",
"RxSuccess": "RX Success",
"RxFailNothing": "RX Fail: Receive Nothing",
"RxFailPartial": "RX Fail: Receive Partial",
"RxFailCorrupt": "RX Fail: Receive Corrupt",
"TxReRequest": "TX Re-Request Fragment",
"StatsReset": "Reset Statistics",
"StatsResetting": "Resetting...",
"Rssi": "RSSI of last received packet",
"RssiHint": "HM inverters only support RSSI values < -64 dBm and > -64 dBm. In this case, -80 dbm and -30 dbm is shown.",
"dBm": "{dbm} dBm"
},
"eventlog": {
"Start": "Inizio",
"Stop": "Fine",
"Id": "ID",
"Message": "Messaggio"
},
"devinfo": {
"NoInfo": "Informazioni non disponibili",
"NoInfoLong": "Ancora nessuna informazione dall'inverter. Sto riprovando...",
"UnknownModel": "Modello sconosciuto! Per favore fornisci \"Hardware Part Number\" ed il modello (esempio HM-350) in una Issue su <a href=\"https://github.com/tbnobody/OpenDTU/issues\" target=\"_blank\">GitHub</a>.",
"Serial": "Seriale",
"ProdYear": "Produzione Annua",
"ProdWeek": "Produzione Settimanale",
"Model": "Modello",
"DetectedMaxPower": "Rilevata potenza massima",
"BootloaderVersion": "Versione Bootloader",
"FirmwareVersion": "Versione Firmware",
"FirmwareBuildDate": "Data Firmware",
"HardwarePartNumber": "Hardware Part Number",
"HardwareVersion": "Hardware Version",
"SupportsPowerDistributionLogic": "'Power Distribution Logic' supported",
"Yes": "@:base.Yes",
"No": "@:base.No"
},
"gridprofile": {
"NoInfo": "@:devinfo.NoInfo",
"NoInfoLong": "@:devinfo.NoInfoLong",
"Name": "Nome",
"Version": "Versione",
"Enabled": "@:wifistationinfo.Enabled",
"Disabled": "@:wifistationinfo.Disabled",
"GridprofileSupport": "Supporto sviluppatori",
"GridprofileSupportLong": "Clicca <a href=\"https://github.com/tbnobody/OpenDTU/wiki/Grid-Profile-Parser\" target=\"_blank\">qui</a> per ulteriori informazioni."
},
"systeminfo": {
"SystemInfo": "Info Sistema",
"VersionError": "Errore ricezione della versione",
"VersionNew": "Nuova versione disponibile! Mostra aggiornamenti!",
"VersionOk": "Già aggiornato!"
},
"firmwareinfo": {
"FirmwareInformation": "Info Firmware",
"Hostname": "Hostname",
"SdkVersion": "SDK Version",
"ConfigVersion": "Config Version",
"FirmwareVersion": "Firmware Version / Git Hash",
"PioEnv": "PIO Environment",
"FirmwareVersionHint": "Click here to show information about your current version",
"FirmwareUpdate": "Firmware Update",
"FirmwareUpdateHint": "Click here to view the changes between your version and the latest version",
"FrmwareUpdateAllow": "By activating the update check, a request is sent to GitHub.com each time the page is called up to retrieve the currently available version. If you do not agree with this, leave this function deactivated.",
"ResetReason0": "Reset Reason CPU 0",
"ResetReason1": "Reset Reason CPU 1",
"ConfigSaveCount": "Config save count",
"Uptime": "Uptime",
"UptimeValue": "0 days {time} | 1 day {time} | {count} days {time}"
},
"hardwareinfo": {
"HardwareInformation": "Info Hardware",
"ChipModel": "Chip Model",
"ChipRevision": "Chip Revision",
"ChipCores": "Chip Cores",
"CpuFrequency": "CPU Frequency",
"Mhz": "MHz",
"CpuTemperature": "CPU Temperature",
"FlashSize": "Flash Memory Size"
},
"memoryinfo": {
"MemoryInformation": "Info Memoria",
"Type": "Tipo",
"Usage": "Uso",
"Free": "Libera",
"Used": "Usata",
"Size": "Dimensione",
"Heap": "Heap",
"PsRam": "PSRAM",
"LittleFs": "LittleFs",
"Sketch": "Sketch"
},
"heapdetails": {
"HeapDetails": "Dettagli memoria Heap",
"TotalFree": "Libera totale",
"LargestFreeBlock": "Blocco contiguo libero più grande",
"MaxUsage": "Massima utilizzata dall'avvio",
"Fragmentation": "Livello frammentazione"
},
"taskdetails": {
"TaskDetails": "Task Details",
"Name": "Name",
"StackFree": "Stack Free",
"Priority": "Priority",
"Task_idle0": "Idle (CPU Core 0)",
"Task_idle1": "Idle (CPU Core 1)",
"Task_wifi": "Wi-Fi",
"Task_tit": "TCP/IP",
"Task_looptask": "Arduino Main Loop",
"Task_asynctcp": "Async TCP",
"Task_mqttclient": "MQTT Client",
"Task_huaweican0": "AC Charger CAN",
"Task_pmsdm": "PowerMeter (SDM)",
"Task_pmhttpjson": "PowerMeter (HTTP+JSON)",
"Task_pmsml": "PowerMeter (Serial SML)",
"Task_pmhttpsml": "PowerMeter (HTTP+SML)"
},
"radioinfo": {
"RadioInformation": "Info Transceiver Radio",
"Status": "{module} Stato",
"ChipStatus": "{module} Chip Stato",
"ChipType": "{module} Chip Tipo",
"Connected": "connesso",
"NotConnected": "non connesso",
"Configured": "configurato",
"NotConfigured": "no configurato",
"Unknown": "Sconosciuto"
},
"networkinfo": {
"NetworkInformation": "Informazioni Rete"
},
"wifistationinfo": {
"WifiStationInfo": "Info WiFi (Station)",
"Status": "Stato",
"Enabled": "abilitato",
"Disabled": "disabilitato",
"Ssid": "SSID",
"Bssid": "BSSID",
"Quality": "Qualità",
"Rssi": "RSSI"
},
"wifiapinfo": {
"WifiApInfo": "Info WiFi (Access Point)",
"Status": "@:wifistationinfo.Status",
"Enabled": "@:wifistationinfo.Enabled",
"Disabled": "@:wifistationinfo.Disabled",
"Ssid": "@:wifistationinfo.Ssid",
"Stations": "Numero Stazioni"
},
"interfacenetworkinfo": {
"NetworkInterface": "Interfaccia di Rete ({iface})",
"Hostname": "@:firmwareinfo.Hostname",
"IpAddress": "Indirizzo IP",
"Netmask": "Netmask",
"DefaultGateway": "Gateway",
"Dns": "DNS {num}",
"MacAddress": "Indirizzo MAC"
},
"interfaceapinfo": {
"NetworkInterface": "Interfaccia di Rete (Access Point)",
"IpAddress": "@:interfacenetworkinfo.IpAddress",
"MacAddress": "@:interfacenetworkinfo.MacAddress"
},
"ntpinfo": {
"NtpInformation": "Informazioni NTP",
"ConfigurationSummary": "Riepilogo Configurazione",
"Server": "Server",
"Timezone": "Timezone",
"TimezoneDescription": "Descrizione Timezone",
"CurrentTime": "Data/Ora attuale",
"Status": "Stato",
"Synced": "sincronizzata",
"NotSynced": "non sincronizzata",
"LocalTime": "Ora Locale",
"Sunrise": "Alba",
"Sunset": "Tramonto",
"NotAvailable": "Non Disponibile",
"Mode": "Modalità",
"Day": "Giorno",
"Night": "Notte"
},
"mqttinfo": {
"MqttInformation": "Informazioni MQTT",
"ConfigurationSummary": "@:ntpinfo.ConfigurationSummary",
"Status": "@:ntpinfo.Status",
"Enabled": "Abilitato",
"Disabled": "Disabilitato",
"Server": "@:ntpinfo.Server",
"Port": "Porta",
"ClientId": "Client ID",
"Username": "Username",
"BaseTopic": "Topic Base",
"PublishInterval": "Intervallo Publish",
"Seconds": "{sec} secondi",
"CleanSession": "CleanSession",
"Retain": "Retain",
"Tls": "TLS",
"RootCertifcateInfo": "Info Certificato Root CA",
"TlsCertLogin": "Entra con Certificato TLS",
"ClientCertifcateInfo": "Info Certificato Client",
"HassSummary": "Riepilogo Configurazione Home Assistant MQTT Auto Discovery",
"Expire": "Scade",
"IndividualPanels": "Pannello Individuale",
"RuntimeSummary": "Riepilogo Runtime",
"ConnectionStatus": "Stato Connessione",
"Connected": "connesso",
"Disconnected": "disconnesso"
},
"console": {
"Console": "Console",
"VirtualDebugConsole": "Virtual Debug Console",
"EnableAutoScroll": "Abilita AutoScroll",
"ClearConsole": "Pulisci Console",
"CopyToClipboard": "Copia nella clipboard"
},
"inverterchannelinfo": {
"String": "Stringa {num}",
"Phase": "Fase {num}",
"General": "Generale"
},
"invertertotalinfo": {
"TotalYieldTotal": "Totale Energia",
"TotalYieldDay": "Energia Giornaliera",
"TotalPower": "Potenza Totale"
},
"inverterchannelproperty": {
"Power": "Potenza",
"Voltage": "Tensione",
"Current": "Corrente",
"Power DC": "PotenzaDC",
"YieldDay": "EnergiaOggi",
"YieldTotal": "EnergiaTotale",
"Frequency": "Frequenza",
"Temperature": "Temperatura",
"PowerFactor": "FattorePotenza",
"ReactivePower": "PotenzaReattiva",
"Efficiency": "Efficienza",
"Irradiation": "Irragiamento"
},
"maintenancereboot": {
"DeviceReboot": "Riavvio DTU",
"PerformReboot": "Fai il riavvio",
"Reboot": "Riavvio!",
"Cancel": "@:base.Cancel",
"RebootOpenDTU": "Riavvio OpenDTU",
"RebootQuestion": "Vuoi veramente riavvia il DTU?",
"RebootHint": "<b>Nota:</b> Normalmente non serve riavviare OpenDTU, in quanto esegue automaticamente il ravvio quando necessario (ad esempio dopo aggiornamento firmware). Modifiche alla configurazione vengono apprese subito, senza richiedere riavvio. Se devi riavviare a causa di un errore, ti preghiamo di segnalarcelo cliccando su <a href=\"https://github.com/tbnobody/OpenDTU/issues\" class=\"alert-link\" target=\"_blank\">https://github.com/tbnobody/OpenDTU/issues</a>."
},
"dtuadmin": {
"DtuSettings": "Impostazioni DTU",
"DtuConfiguration": "Configurazione DTU",
"Serial": "Seriale",
"SerialHint": "Sia il DTU che l'inverter hanno un numero seriale. Il numero seriale del DTU è generato casualmente al primo avvio e normalmente non serve modificarlo.",
"PollInterval": "Intervallo Interrogazione",
"Seconds": "Secondi",
"NrfPaLevel": "Potenza Trasmettitore NRF24",
"CmtPaLevel": "Potenza Trasmettitore CMT2300A",
"NrfPaLevelHint": "Usato per inverter HM. Considera che aumentando la potenza aumentano il consumo di corrente.",
"CmtPaLevelHint": "Usato per inverter HMS/HMT. Considera che aumentando la potenza aumentano il consumo di corrente.",
"CmtCountry": "CMT2300A Zona/Paese",
"CmtCountryHint": "Ogni zona ha una differente allocazione di frequenze utilizzabili.",
"country_0": "Europa ({min}MHz - {max}MHz)",
"country_1": "Nord America ({min}MHz - {max}MHz)",
"country_2": "Brasile ({min}MHz - {max}MHz)",
"CmtFrequency": "Frequenza CMT2300A",
"CmtFrequencyHint": "Fai attenzione ad usare solo frequenze ammesse nel tuo Paese! Dopo la modifica frequenza, servono fino a 15 minuti affinché la connessione si ristabilisca.",
"CmtFrequencyWarning": "La frequenza selezionata è fuori dal range selezionato dal tuo Paese. Verifica che la frequenza selezionata non violi le normative del tuo Paese.",
"MHz": "{mhz} MHz",
"dBm": "{dbm} dBm",
"Min": "Minima ({db} dBm)",
"Low": "Bassa ({db} dBm)",
"High": "Alta ({db} dBm)",
"Max": "Massima ({db} dBm)"
},
"securityadmin": {
"SecuritySettings": "Impostazioni di Sicurezza",
"AdminPassword": "Password Admin",
"Password": "Password",
"RepeatPassword": "Ripeti Password",
"PasswordHint": "<b>Nota:</b> La password di amministrazione viene utilizzata non solo per accedere a questa interfaccia web (con user 'admin'), ma anche per connettersi al dispositivo in modalità AP. Deve avere da 8 a 64 caratteri.",
"Permissions": "Permessi",
"ReadOnly": "Permetti accessi web in sola lettura senza richiedere la password"
},
"ntpadmin": {
"NtpSettings": "Impostazioni NTP (Data / Ora)",
"NtpConfiguration": "Configurazione NTP",
"TimeServer": "Server NTP",
"TimeServerHint": "Puoi lasciare il valore di default, nel caso in cui OpenDTU abbia accesso ad internet.",
"Timezone": "Timezone",
"TimezoneConfig": "Timezone Config",
"LocationConfiguration": "Configurazione Posizione",
"Longitude": "Longitudine",
"Latitude": "Latitudine",
"SunSetType": "Tipo di Alba",
"SunSetTypeHint": "Influenza il calcolo dell'ora di Alba/Tramonto. Dopo la conferma, è richiesto fino ad un minuto perché la modifica venga applicata.",
"OFFICIAL": "Standard dawn (90.8°)",
"NAUTICAL": "Nautical dawn (102°)",
"CIVIL": "Civil dawn (96°)",
"ASTONOMICAL": "Astronomical dawn (108°)",
"ManualTimeSynchronization": "Sincronizzazione Manuale Data/Ora",
"CurrentOpenDtuTime": "Ora OpenDTU attuale",
"CurrentLocalTime": "Ora Locale attuale",
"SynchronizeTime": "Sincronizza Data/Ora",
"SynchronizeTimeHint": "<b>Nota:</b> Puoi usare la sincronizzazione manuale per impostare Data/Ora nel caso che non sia disponibile un server NTP. In questo caso la data/ora viene persa in caso di mancata alimentazione. Inoltre, con la sincronizzazione manuale ci sarà una progressiva deriva della Data/Ora in quanto l'ESP32 non ha un Real Time Clock interno."
},
"networkadmin": {
"NetworkSettings": "Impostazioni di Rete",
"WifiConfiguration": "Configurazione WiFi",
"WifiSsid": "WiFi SSID",
"WifiPassword": "WiFi Password",
"Hostname": "Hostname",
"HostnameHint": "<b>Nota:</b> Il testo <span class=\"font-monospace\">%06X</span> sarà rimpiazzato con le ultime 6 cifre del ChipID dell'ESP32 in formato esadecimale.",
"EnableDhcp": "Abilita DHCP",
"StaticIpConfiguration": "Configurazione IP Statico",
"IpAddress": "Indirizzo IP",
"Netmask": "Netmask",
"DefaultGateway": "Default Gateway",
"Dns": "DNS Server {num}",
"AdminAp": "Configurazione WiFi (Admin AccessPoint)",
"ApTimeout": "Timeout AccessPoint",
"ApTimeoutHint": "Tempo in cui la modalità AccessPoint rimarrà attiva. 0=per sempre.",
"Minutes": "minuti",
"EnableMdns": "Abilita mDNS",
"MdnsSettings": "Configurazione mDNS"
},
"mqttadmin": {
"MqttSettings": "Impostazioni MQTT",
"MqttConfiguration": "Configurazione MQTT",
"EnableMqtt": "Abilita MQTT",
"EnableHass": "Abilita Home Assistant MQTT Auto Discovery",
"MqttBrokerParameter": "Parametri Broker MQTT",
"Hostname": "Hostname",
"HostnameHint": "Hostname o Indirizzo IP",
"Port": "Porta",
"ClientId": "Client ID",
"Username": "Username",
"UsernameHint": "Username, lascia vuoto per connessione anonima",
"Password": "Password",
"PasswordHint": "Password, lascia vuota per connessione anonima",
"BaseTopic": "Topic Base",
"BaseTopicHint": "Topic Base, prefisso da aggiungere (ad esempio inverter/)",
"PublishInterval": "Intervallo pubblicazione",
"Seconds": "secondi",
"CleanSession": "Abilita CleanSession",
"EnableRetain": "Abilita Retain",
"EnableTls": "Abilita TLS",
"RootCa": "CA-Root-Certificate (default Letsencrypt)",
"TlsCertLoginEnable": "Abilita Login con certificato TLS",
"ClientCert": "TLS Client-Certificate",
"ClientKey": "TLS Client-Key",
"LwtParameters": "Parametri LWT",
"LwtTopic": "Topic LWT",
"LwtTopicHint": "Topic LWT, da aggiungere al Topic Base",
"LwtOnline": "Messaggio 'Online0 LWT",
"LwtOnlineHint": "Messaggio pubblicato quando online",
"LwtOffline": "Messaggio 'Offline' LWT",
"LwtOfflineHint": "Messaggio che sarà pubblicato quando offline",
"LwtQos": "QoS (Quality of Service)",
"QOS0": "0 (Al massimo una volta)",
"QOS1": "1 (Almeno una volta)",
"QOS2": "2 (Esattamente una volta)",
"HassParameters": "Parametri Home Assistant MQTT Auto Discovery",
"HassPrefixTopic": "Prefisso Topic",
"HassPrefixTopicHint": "Prefisso per Topic autodiscovery",
"HassRetain": "Abilita Retain",
"HassExpire": "Abilita Scadenza",
"HassIndividual": "Pannelli Individuale"
},
"inverteradmin": {
"InverterSettings": "Impostazioni Inverter",
"AddInverter": "Aggiungi nuovo Inverter",
"Serial": "Seriale",
"Name": "Nome",
"Add": "Aggiungi",
"AddHint": "<b>Nota:</b> Potrai aggiungere ulteriori parametri dopo aver creato l'inverter, cliccando sull'icona 'Matita' nella lista inverter.",
"InverterList": "Lista Inverter",
"Status": "Stato",
"Send": "Invia",
"Receive": "Riceve",
"StatusHint": "<b>Nota:</b> L'inverter viene alimentato dal fotovoltaico. Durante la notte, l'inverter risulterà spento. Le richieste potranno comunque essere trasmesse.",
"Type": "Tipo",
"Action": "Azione",
"SaveOrder": "Salva ordine",
"DeleteInverter": "Rimuovi inverter",
"EditInverter": "Modifica inverter",
"General": "Generale",
"String": "Stringa",
"Advanced": "Avanzate",
"InverterSerial": "Seriale Inverter:",
"InverterName": "Nome Inverter:",
"InverterNameHint": "Puoi specificare un nome qualsiasi da assegnare all'inverter.",
"InverterStatus": "Riceve / Invia",
"PollEnable": "Interroga inverter",
"PollEnableNight": "Interroga inverter di notte",
"CommandEnable": "Invia comandi",
"CommandEnableNight": "Invia comandi di notte",
"StringName": "Nome stringa {num}:",
"StringNameHint": "Qui puoi specificare un nome qualsiasi per la porta dell'inverter o per il pannello fotovoltaico collegato.",
"StringMaxPower": "Massima potenza stringa {num}:",
"StringMaxPowerHint": "Inserisci la potenza massima associata ai panelli fotovoltaici collegati a questa stringa.",
"StringYtOffset": "Offset Energia totale per la stringa {num}:",
"StringYtOffsetHint": "Questo offset viene utilizzato per azzerare il contatore qualora venga usato un inverter usato.",
"InverterHint": "*) Inserisci la potenza W<sub>p</sub> dei pannelli fotovoltaici collegati alla stringa: servirà per calcolare l'irragiamento.",
"ReachableThreshold": "Reachable Threshold",
"ReachableThresholdHint": "Definisce il numero di richieste fallite prima che l'inverter sia considerato irraggiungibile.",
"ZeroRuntime": "Azzera dati in tempo reale",
"ZeroRuntimeHint": "Azzera i dati in tempo reale (tranne l'Energia) se l'inverter diventa irraggiunbile.",
"ZeroDay": "Azzera dati energia alla mezzanotte",
"ZeroDayHint": "Questo vale se l'inverter risulta irraggiungibile. Se l'inverter risponde anche di notte, verranno mostrati i suoi valori. (Il Reset si verifica al riavvio)",
"ClearEventlog": "Clear Eventlog at midnight",
"Cancel": "@:base.Cancel",
"Save": "@:base.Save",
"DeleteMsg": "Sicuro di voler rimuovere l'inverter \"{name}\" con numero seriale {serial}?",
"Delete": "Rimuovi",
"YieldDayCorrection": "Correzione energia giornaliera",
"YieldDayCorrectionHint": "Aggiungi questo valore all'energia giornaliera se l'inverter è stato riavviato. Questo valore sarò resettato a mezzanotte"
},
"fileadmin": {
"ConfigManagement": "Configurazione Gestione",
"BackupHeader": "Backup: Configurazione File Backup",
"BackupConfig": "Esegui il backup del file",
"Backup": "Backup",
"Restore": "Ripristina",
"NoFileSelected": "Nessun file selezionato",
"RestoreHeader": "Ripristina: Ripristina File Configurazione",
"Back": "Indietro",
"UploadSuccess": "Invio File con successo",
"RestoreHint": "<b>Nota:</b> questa operazione rimpiazza la configurazione con quella contenuta nel file, e poi riavvia automaticamente OpenDTU per applicare la nuova configurazione.",
"ResetHeader": "Inizializza: Esegui il Factory Reset",
"FactoryResetButton": "Ripristina Configurazione Factory-Default",
"ResetHint": "<b>Nota:</b> Clicca 'Ripristina Configurazione Factory-Default' per stabilire le impostazioni di fabbrica e riavviare automaticamente OpenDTU.",
"FactoryReset": "Factory Reset",
"ResetMsg": "Sei sicuro di voler cancellare la configurazione attuale e applicare la configurazione di fabbrica?",
"ResetConfirm": "Factory Reset!",
"Cancel": "@:base.Cancel",
"InvalidJson": "JSON file is formatted incorrectly.",
"InvalidJsonContent": "JSON file has the wrong content."
},
"login": {
"Login": "Login",
"SystemLogin": "System Login",
"Username": "Username",
"UsernameRequired": "Inserisci Username",
"Password": "Password",
"PasswordRequired": "Inserisci Password",
"LoginButton": "Login"
},
"firmwareupgrade": {
"FirmwareUpgrade": "Aggiornamento Firmware",
"Loading": "@:base.Loading",
"OtaError": "Errore aggiornamento OTA",
"Back": "Indietro",
"Retry": "Riprova",
"OtaStatus": "Stato OTA",
"OtaSuccess": "Aggiornamento firmware eseguito con successo. Il dispositivo si riavvierà automaticamente. Quando sarà nuovamente disponibile, l'interfacca sarà ricaricata automaticamente.",
"FirmwareUpload": "Invia Firmware",
"UploadProgress": "Upload in corso"
},
"about": {
"AboutOpendtu": "About OpenDTU",
"Documentation": "Documentazione",
"DocumentationBody": "La documentazione firmware e hardware sono disponibili qui: <a href=\"https://www.opendtu.solar\" target=\"_blank\">https://www.opendtu.solar</a>",
"ProjectOrigin": "Origine Progetto",
"ProjectOriginBody1": "Questo progetto è partito da <a href=\"https://www.mikrocontroller.net/topic/525778\" target=\"_blank\">questa discussione. (Mikrocontroller.net)</a>",
"ProjectOriginBody2": "Il protocollo Hoymiles è stato decriptato grazie al contributo volontario di molti programmatori. OpenDTU, fra gli altri, è stato sviluppato grazie a questo lavoro. Il progetto è distribuito con Licenza Open Source (<a href=\"https://www.gnu.de/documents/gpl-2.0.de.html\" target=\"_blank\">GNU General Public License version 2</a>).",
"ProjectOriginBody3": "Il software è stato sviluppato con le nostre migliori conoscenze e convinzioni. Tuttavia, non si assume alcuna responsabilità per malfunzionamenti o perdita di garanzia dell'inverter.",
"ProjectOriginBody4": "OpenDTU è disponibile gratuitamente. Se hai pagato per questo software, probabilmente sei stato truffato.",
"NewsUpdates": "Novità e Aggiornamenti",
"NewsUpdatesBody": "Nuovi aggiornamenti sono disponibili su Github: <a href=\"https://github.com/tbnobody/OpenDTU\" target=\"_blank\">https://github.com/tbnobody/OpenDTU</a>",
"ErrorReporting": "Segnalazione Errori",
"ErrorReportingBody": "Per favore segnala eventuali problemi utilizzando le funzionalità della piattaforma <a href=\"https://github.com/tbnobody/OpenDTU/issues\" target=\"_blank\">Github</a>",
"Discussion": "Discussioni",
"DiscussionBody": "Puoi avviare una discussione con noi su <a href=\"https://discord.gg/WzhxEY62mB\" target=\"_blank\">Discord</a> o <a href=\"https://github.com/tbnobody/OpenDTU/discussions\" target=\"_blank\">Github</a>"
},
"hints": {
"RadioProblem": "Non è possibile dialogare con il modulo radio selezionato. Controlla i collegamenti alla radio.",
"TimeSync": "La Data/Ora non sono state sincronizzate, ed in tal caso non è possibile eseguire richieste all'inverter. Questa condizione è normale appena avviato, tuttavia dopo un po' (>1 minuto), questa situazione potrebbe indicare un problema di accesso al server NTP.",
"TimeSyncLink": "Controlla le impostazioni Data/Ora.",
"DefaultPassword": "Stai usando la password di default per accedere all'interfaccia web e per la modalità Access Point di emergenza. Questo può portare ad un rischio di sicurezza.",
"DefaultPasswordLink": "Per favore cambia la password.",
"PinMappingIssue": "You are using a generic firmware image, but have not yet uploaded a file with device profiles (<code>pin_mapping.json</code>) or have not selected a profile defined there. Please refer to the <a href=\"https://opendtu.solar/firmware/device_profiles/\" target=\"_blank\" class=\"alert-link\">documentation</a> for details."
},
"deviceadmin": {
"DeviceManager": "Device-Manager",
"ParseError": "Parse error in 'pin_mapping.json': {error}",
"PinAssignment": "Impostazioni Connessione",
"SelectedProfile": "Profilo selezionato",
"DefaultProfile": "(Impostazioni di Default)",
"ProfileHint": "Il tuo dispositivo potrebbe smettere di rispondere selezionando un profilo incompatibile. In questo caso, dovrai eseguire una cancellazione collegandoti all'interfaccia seriale.",
"Display": "Display",
"PowerSafe": "Abilita Risparmio Energetico",
"PowerSafeHint": "Spegni il display se l'inverter non produce.",
"Screensaver": "Abilita Screensaver",
"ScreensaverHint": "Muove il testo nel display per prevenire danneggiamento pixel. (Utile in caso di display OLED)",
"DiagramMode": "Modalità grafica",
"off": "Off",
"small": "Small",
"fullscreen": "Fullscreen",
"DiagramDuration": "Durata grafico",
"DiagramDurationHint": "Periodo che viene mostrato nel grafico.",
"Seconds": "Secondi",
"Contrast": "Contrasto ({contrast})",
"Rotation": "Rotazione",
"rot0": "Nessuna rotazione",
"rot90": "Rotazione 90 gradi",
"rot180": "Rotazione 180 gradi",
"rot270": "Rotazione 270 gradi",
"DisplayLanguage": "Linuga Display",
"en": "English",
"de": "German",
"fr": "French",
"Leds": "LEDs",
"EqualBrightness": "Equalizza luminosità",
"LedBrightness": "LED {led}, Luminosità ({brightness})"
},
"pininfo": {
"Category": "Categoria",
"Name": "Nome",
"Number": "Numero",
"ValueSelected": "Selezionato",
"ValueActive": "Attivo"
},
"inputserial": {
"format_hoymiles": "Hoymiles serial number format",
"format_converted": "Already converted serial number",
"format_herf_valid": "E-Star HERF format (will be saved converted): {serial}",
"format_herf_invalid": "E-Star HERF format: Invalid checksum",
"format_unknown": "Unknown format"
}
}
}

View File

@ -1,142 +0,0 @@
#include "cmt_spi3.h"
#include <Arduino.h>
#include <driver/spi_master.h>
#include <esp_rom_gpio.h> // for esp_rom_gpio_connect_out_signal
SemaphoreHandle_t paramLock = NULL;
#define SPI_PARAM_LOCK() \
do { \
} while (xSemaphoreTake(paramLock, portMAX_DELAY) != pdPASS)
#define SPI_PARAM_UNLOCK() xSemaphoreGive(paramLock)
// for ESP32 this is the so-called HSPI
// for ESP32-S2/S3/C3 this nomenclature does not really exist anymore,
// it is simply the first externally usable hardware SPI master controller
#define SPI_CMT SPI2_HOST
spi_device_handle_t spi_reg, spi_fifo;
void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed)
{
paramLock = xSemaphoreCreateMutex();
spi_bus_config_t buscfg = {
.mosi_io_num = pin_sdio,
.miso_io_num = -1, // single wire MOSI/MISO
.sclk_io_num = pin_clk,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 32,
};
spi_device_interface_config_t devcfg = {
.command_bits = 1,
.address_bits = 7,
.dummy_bits = 0,
.mode = 0, // SPI mode 0
.cs_ena_pretrans = 1,
.cs_ena_posttrans = 1,
.clock_speed_hz = spi_speed,
.spics_io_num = pin_cs,
.flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE,
.queue_size = 1,
.pre_cb = NULL,
.post_cb = NULL,
};
ESP_ERROR_CHECK(spi_bus_initialize(SPI_CMT, &buscfg, SPI_DMA_DISABLED));
ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg, &spi_reg));
// FiFo
spi_device_interface_config_t devcfg2 = {
.command_bits = 0,
.address_bits = 0,
.dummy_bits = 0,
.mode = 0, // SPI mode 0
.cs_ena_pretrans = 2,
.cs_ena_posttrans = (uint8_t)(1 / (spi_speed * 10e6 * 2) + 2), // >2 us
.clock_speed_hz = spi_speed,
.spics_io_num = pin_fcs,
.flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE,
.queue_size = 1,
.pre_cb = NULL,
.post_cb = NULL,
};
ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg2, &spi_fifo));
esp_rom_gpio_connect_out_signal(pin_sdio, spi_periph_signal[SPI_CMT].spid_out, true, false);
delay(100);
}
void cmt_spi3_write(const uint8_t addr, const uint8_t dat)
{
uint8_t tx_data;
tx_data = ~dat;
spi_transaction_t t = {
.cmd = 1,
.addr = ~addr,
.length = 8,
.tx_buffer = &tx_data,
.rx_buffer = NULL
};
SPI_PARAM_LOCK();
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t));
SPI_PARAM_UNLOCK();
delayMicroseconds(100);
}
uint8_t cmt_spi3_read(const uint8_t addr)
{
uint8_t rx_data;
spi_transaction_t t = {
.cmd = 0,
.addr = ~addr,
.length = 8,
.rxlength = 8,
.tx_buffer = NULL,
.rx_buffer = &rx_data
};
SPI_PARAM_LOCK();
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t));
SPI_PARAM_UNLOCK();
delayMicroseconds(100);
return rx_data;
}
void cmt_spi3_write_fifo(const uint8_t* buf, const uint16_t len)
{
uint8_t tx_data;
spi_transaction_t t = {
.length = 8,
.tx_buffer = &tx_data, // reference to write data
.rx_buffer = NULL
};
SPI_PARAM_LOCK();
for (uint8_t i = 0; i < len; i++) {
tx_data = ~buf[i]; // negate buffer contents
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t));
delayMicroseconds(4); // > 4 us
}
SPI_PARAM_UNLOCK();
}
void cmt_spi3_read_fifo(uint8_t* buf, const uint16_t len)
{
uint8_t rx_data;
spi_transaction_t t = {
.length = 8,
.rxlength = 8,
.tx_buffer = NULL,
.rx_buffer = &rx_data
};
SPI_PARAM_LOCK();
for (uint8_t i = 0; i < len; i++) {
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t));
delayMicroseconds(4); // > 4 us
buf[i] = rx_data;
}
SPI_PARAM_UNLOCK();
}

155
lib/CMT2300a/cmt_spi3.cpp Normal file
View File

@ -0,0 +1,155 @@
#include "cmt_spi3.h"
#include <Arduino.h>
#include <driver/spi_master.h>
#include <SpiManager.h>
SemaphoreHandle_t paramLock = NULL;
#define SPI_PARAM_LOCK() \
do { \
} while (xSemaphoreTake(paramLock, portMAX_DELAY) != pdPASS)
#define SPI_PARAM_UNLOCK() xSemaphoreGive(paramLock)
static void IRAM_ATTR pre_cb(spi_transaction_t *trans) {
gpio_set_level(*reinterpret_cast<gpio_num_t*>(trans->user), 0);
}
static void IRAM_ATTR post_cb(spi_transaction_t *trans) {
gpio_set_level(*reinterpret_cast<gpio_num_t*>(trans->user), 1);
}
spi_device_handle_t spi;
gpio_num_t cs_reg, cs_fifo;
void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int32_t spi_speed)
{
paramLock = xSemaphoreCreateMutex();
auto bus_config = std::make_shared<SpiBusConfig>(
static_cast<gpio_num_t>(pin_sdio),
GPIO_NUM_NC,
static_cast<gpio_num_t>(pin_clk)
);
spi_device_interface_config_t device_config {
.command_bits = 0, // set by transactions individually
.address_bits = 0, // set by transactions individually
.dummy_bits = 0,
.mode = 0, // SPI mode 0
.duty_cycle_pos = 0,
.cs_ena_pretrans = 2, // only 1 pre and post cycle would be required for register access
.cs_ena_posttrans = static_cast<uint8_t>(2 * spi_speed / 1000000), // >2 us
.clock_speed_hz = spi_speed,
.input_delay_ns = 0,
.spics_io_num = -1, // CS handled by callbacks
.flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE,
.queue_size = 1,
.pre_cb = pre_cb,
.post_cb = post_cb,
};
spi = SpiManagerInst.alloc_device("", bus_config, device_config);
if (!spi)
ESP_ERROR_CHECK(ESP_FAIL);
cs_reg = static_cast<gpio_num_t>(pin_cs);
ESP_ERROR_CHECK(gpio_reset_pin(cs_reg));
ESP_ERROR_CHECK(gpio_set_level(cs_reg, 1));
ESP_ERROR_CHECK(gpio_set_direction(cs_reg, GPIO_MODE_OUTPUT));
cs_fifo = static_cast<gpio_num_t>(pin_fcs);
ESP_ERROR_CHECK(gpio_reset_pin(cs_fifo));
ESP_ERROR_CHECK(gpio_set_level(cs_fifo, 1));
ESP_ERROR_CHECK(gpio_set_direction(cs_fifo, GPIO_MODE_OUTPUT));
}
void cmt_spi3_write(const uint8_t addr, const uint8_t data)
{
spi_transaction_ext_t trans {
.base {
.flags = SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR,
.cmd = 0,
.addr = addr,
.length = 8,
.rxlength = 0,
.user = &cs_reg, // CS for register access
.tx_buffer = &data,
.rx_buffer = nullptr,
},
.command_bits = 1,
.address_bits = 7,
.dummy_bits = 0,
};
SPI_PARAM_LOCK();
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, reinterpret_cast<spi_transaction_t*>(&trans)));
SPI_PARAM_UNLOCK();
}
uint8_t cmt_spi3_read(const uint8_t addr)
{
uint8_t data;
spi_transaction_ext_t trans {
.base {
.flags = SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR,
.cmd = 1,
.addr = addr,
.length = 0,
.rxlength = 8,
.user = &cs_reg, // CS for register access
.tx_buffer = nullptr,
.rx_buffer = &data,
},
.command_bits = 1,
.address_bits = 7,
.dummy_bits = 0,
};
SPI_PARAM_LOCK();
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, reinterpret_cast<spi_transaction_t*>(&trans)));
SPI_PARAM_UNLOCK();
return data;
}
void cmt_spi3_write_fifo(const uint8_t* buf, const uint16_t len)
{
spi_transaction_t trans {
.flags = 0,
.cmd = 0,
.addr = 0,
.length = 8,
.rxlength = 0,
.user = &cs_fifo, // CS for FIFO access
.tx_buffer = nullptr,
.rx_buffer = nullptr,
};
SPI_PARAM_LOCK();
spi_device_acquire_bus(spi, portMAX_DELAY);
for (uint8_t i = 0; i < len; i++) {
trans.tx_buffer = buf + i;
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &trans));
}
spi_device_release_bus(spi);
SPI_PARAM_UNLOCK();
}
void cmt_spi3_read_fifo(uint8_t* buf, const uint16_t len)
{
spi_transaction_t trans {
.flags = 0,
.cmd = 0,
.addr = 0,
.length = 0,
.rxlength = 8,
.user = &cs_fifo, // CS for FIFO access
.tx_buffer = nullptr,
.rx_buffer = nullptr,
};
SPI_PARAM_LOCK();
spi_device_acquire_bus(spi, portMAX_DELAY);
for (uint8_t i = 0; i < len; i++) {
trans.rx_buffer = buf + i;
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &trans));
}
spi_device_release_bus(spi);
SPI_PARAM_UNLOCK();
}

View File

@ -3,7 +3,11 @@
#include <stdint.h> #include <stdint.h>
void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed); #ifdef __cplusplus
extern "C" {
#endif
void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int32_t spi_speed);
void cmt_spi3_write(const uint8_t addr, const uint8_t dat); void cmt_spi3_write(const uint8_t addr, const uint8_t dat);
uint8_t cmt_spi3_read(const uint8_t addr); uint8_t cmt_spi3_read(const uint8_t addr);
@ -11,4 +15,8 @@ uint8_t cmt_spi3_read(const uint8_t addr);
void cmt_spi3_write_fifo(const uint8_t* p_buf, const uint16_t len); void cmt_spi3_write_fifo(const uint8_t* p_buf, const uint16_t len);
void cmt_spi3_read_fifo(uint8_t* p_buf, const uint16_t len); void cmt_spi3_read_fifo(uint8_t* p_buf, const uint16_t len);
#ifdef __cplusplus
}
#endif
#endif #endif

View File

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

View File

@ -57,7 +57,7 @@ void HoymilesClass::loop()
} }
} }
if (iv != nullptr && iv->getRadio()->isInitialized() && iv->getRadio()->isQueueEmpty()) { if (iv != nullptr && iv->getRadio()->isInitialized()) {
if (iv->getZeroValuesIfUnreachable() && !iv->isReachable()) { if (iv->getZeroValuesIfUnreachable() && !iv->isReachable()) {
iv->Statistics()->zeroRuntimeData(); iv->Statistics()->zeroRuntimeData();
@ -119,6 +119,7 @@ void HoymilesClass::loop()
iv->sendGridOnProFileParaRequest(); iv->sendGridOnProFileParaRequest();
} }
_messageOutput->printf("Queue size - NRF: %" PRId32 " CMT: %" PRId32 "\r\n", _radioNrf->getQueueSize(), _radioCmt->getQueueSize());
_lastPoll = millis(); _lastPoll = millis();
} }
@ -136,15 +137,7 @@ void HoymilesClass::loop()
if (currentWeekDay != lastWeekDay) { if (currentWeekDay != lastWeekDay) {
for (auto& inv : _inverters) { for (auto& inv : _inverters) {
// Have to reset the offets first, otherwise it will inv->performDailyTask();
// Substract the offset from zero which leads to a high value
inv->Statistics()->resetYieldDayCorrection();
if (inv->getZeroYieldDayOnMidnight()) {
inv->Statistics()->zeroDailyData();
}
if (inv->getClearEventlogOnMidnight()) {
inv->EventLog()->clearBuffer();
}
} }
lastWeekDay = currentWeekDay; lastWeekDay = currentWeekDay;
@ -203,9 +196,9 @@ std::shared_ptr<InverterAbstract> HoymilesClass::getInverterByPos(const uint8_t
std::shared_ptr<InverterAbstract> HoymilesClass::getInverterBySerial(const uint64_t serial) std::shared_ptr<InverterAbstract> HoymilesClass::getInverterBySerial(const uint64_t serial)
{ {
for (uint8_t i = 0; i < _inverters.size(); i++) { for (auto& inv : _inverters) {
if (_inverters[i]->serial() == serial) { if (inv->serial() == serial) {
return _inverters[i]; return inv;
} }
} }
return nullptr; return nullptr;
@ -217,9 +210,7 @@ std::shared_ptr<InverterAbstract> HoymilesClass::getInverterByFragment(const fra
return nullptr; return nullptr;
} }
std::shared_ptr<InverterAbstract> inv; for (auto& inv : _inverters) {
for (uint8_t i = 0; i < _inverters.size(); i++) {
inv = _inverters[i];
serial_u p; serial_u p;
p.u64 = inv->serial(); p.u64 = inv->serial();
@ -239,6 +230,7 @@ void HoymilesClass::removeInverterBySerial(const uint64_t serial)
for (uint8_t i = 0; i < _inverters.size(); i++) { for (uint8_t i = 0; i < _inverters.size(); i++) {
if (_inverters[i]->serial() == serial) { if (_inverters[i]->serial() == serial) {
std::lock_guard<std::mutex> lock(_mutex); std::lock_guard<std::mutex> lock(_mutex);
_inverters[i]->getRadio()->removeCommands(_inverters[i].get());
_inverters.erase(_inverters.begin() + i); _inverters.erase(_inverters.begin() + i);
return; return;
} }

View File

@ -66,16 +66,31 @@ void HoymilesRadio::handleReceivedPackage()
} else if (verifyResult == FRAGMENT_ALL_MISSING_TIMEOUT) { } else if (verifyResult == FRAGMENT_ALL_MISSING_TIMEOUT) {
Hoymiles.getMessageOutput()->println("Nothing received, resend count exeeded"); Hoymiles.getMessageOutput()->println("Nothing received, resend count exeeded");
// Statistics: Count RX Fail No Answer
if (inv->RadioStats.TxRequestData > 0) {
inv->RadioStats.RxFailNoAnswer++;
}
_commandQueue.pop(); _commandQueue.pop();
_busyFlag = false; _busyFlag = false;
} else if (verifyResult == FRAGMENT_RETRANSMIT_TIMEOUT) { } else if (verifyResult == FRAGMENT_RETRANSMIT_TIMEOUT) {
Hoymiles.getMessageOutput()->println("Retransmit timeout"); Hoymiles.getMessageOutput()->println("Retransmit timeout");
// Statistics: Count RX Fail Partial Answer
if (inv->RadioStats.TxRequestData > 0) {
inv->RadioStats.RxFailPartialAnswer++;
}
_commandQueue.pop(); _commandQueue.pop();
_busyFlag = false; _busyFlag = false;
} else if (verifyResult == FRAGMENT_HANDLE_ERROR) { } else if (verifyResult == FRAGMENT_HANDLE_ERROR) {
Hoymiles.getMessageOutput()->println("Packet handling error"); Hoymiles.getMessageOutput()->println("Packet handling error");
// Statistics: Count RX Fail Corrupt Data
if (inv->RadioStats.TxRequestData > 0) {
inv->RadioStats.RxFailCorruptData++;
}
_commandQueue.pop(); _commandQueue.pop();
_busyFlag = false; _busyFlag = false;
@ -83,17 +98,26 @@ void HoymilesRadio::handleReceivedPackage()
// Perform Retransmit // Perform Retransmit
Hoymiles.getMessageOutput()->print("Request retransmit: "); Hoymiles.getMessageOutput()->print("Request retransmit: ");
Hoymiles.getMessageOutput()->println(verifyResult); Hoymiles.getMessageOutput()->println(verifyResult);
// Statistics: Count TX Re-Request Fragment
inv->RadioStats.TxReRequestFragment++;
sendRetransmitPacket(verifyResult); sendRetransmitPacket(verifyResult);
} else { } else {
// Successful received all packages // Successful received all packages
Hoymiles.getMessageOutput()->println("Success"); Hoymiles.getMessageOutput()->println("Success");
// Statistics: Count RX Success
if (inv->RadioStats.TxRequestData > 0) {
inv->RadioStats.RxSuccess++;
}
_commandQueue.pop(); _commandQueue.pop();
_busyFlag = false; _busyFlag = false;
} }
} else { } else {
// If inverter was not found, assume the command is invalid // If inverter was not found, assume the command is invalid
Hoymiles.getMessageOutput()->println("RX: Invalid inverter found"); Hoymiles.getMessageOutput()->println("RX: Invalid inverter found");
// Statistics: Count RX Fail Unknown Data
_commandQueue.pop(); _commandQueue.pop();
_busyFlag = false; _busyFlag = false;
} }
@ -105,6 +129,9 @@ void HoymilesRadio::handleReceivedPackage()
auto inv = Hoymiles.getInverterBySerial(cmd->getTargetAddress()); auto inv = Hoymiles.getInverterBySerial(cmd->getTargetAddress());
if (nullptr != inv) { if (nullptr != inv) {
inv->clearRxFragmentBuffer(); inv->clearRxFragmentBuffer();
// Statistics: TX Requests
inv->RadioStats.TxRequestData++;
sendEsbPacket(*cmd); sendEsbPacket(*cmd);
} else { } else {
Hoymiles.getMessageOutput()->println("TX: Invalid inverter found"); Hoymiles.getMessageOutput()->println("TX: Invalid inverter found");
@ -129,6 +156,16 @@ bool HoymilesRadio::isInitialized() const
return _isInitialized; return _isInitialized;
} }
void HoymilesRadio::removeCommands(InverterAbstract* inv)
{
_commandQueue.removeAllEntriesForInverter(inv);
}
uint8_t HoymilesRadio::countSimilarCommands(std::shared_ptr<CommandAbstract> cmd)
{
return _commandQueue.countSimilarCommands(cmd);
}
bool HoymilesRadio::isIdle() const bool HoymilesRadio::isIdle() const
{ {
return !_busyFlag; return !_busyFlag;
@ -138,3 +175,8 @@ bool HoymilesRadio::isQueueEmpty() const
{ {
return _commandQueue.size() == 0; return _commandQueue.size() == 0;
} }
uint32_t HoymilesRadio::getQueueSize() const
{
return _commandQueue.size();
}

View File

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

View File

@ -34,7 +34,7 @@ uint32_t HoymilesRadio_CMT::getFrequencyFromChannel(const uint8_t channel) const
uint8_t HoymilesRadio_CMT::getChannelFromFrequency(const uint32_t frequency) const uint8_t HoymilesRadio_CMT::getChannelFromFrequency(const uint32_t frequency) const
{ {
if ((frequency % getChannelWidth()) != 0) { if ((frequency % getChannelWidth()) != 0) {
Hoymiles.getMessageOutput()->printf("%.3f MHz is not divisible by %d kHz!\r\n", frequency / 1000000.0, getChannelWidth()); Hoymiles.getMessageOutput()->printf("%.3f MHz is not divisible by %" PRId32 " kHz!\r\n", frequency / 1000000.0, getChannelWidth());
return 0xFF; // ERROR return 0xFF; // ERROR
} }
if (frequency < getMinFrequency() || frequency > getMaxFrequency()) { if (frequency < getMinFrequency() || frequency > getMaxFrequency()) {
@ -43,7 +43,7 @@ uint8_t HoymilesRadio_CMT::getChannelFromFrequency(const uint32_t frequency) con
return 0xFF; // ERROR return 0xFF; // ERROR
} }
if (frequency < countryDefinition.at(_countryMode).Freq_Legal_Min || frequency > countryDefinition.at(_countryMode).Freq_Legal_Max) { if (frequency < countryDefinition.at(_countryMode).Freq_Legal_Min || frequency > countryDefinition.at(_countryMode).Freq_Legal_Max) {
Hoymiles.getMessageOutput()->printf("!!! caution: %.2f MHz is out of region legal range! (%d - %d MHz)\r\n", Hoymiles.getMessageOutput()->printf("!!! caution: %.2f MHz is out of region legal range! (%" PRId32 " - %" PRId32 " MHz)\r\n",
frequency / 1000000.0, frequency / 1000000.0,
static_cast<uint32_t>(countryDefinition.at(_countryMode).Freq_Legal_Min / 1e6), static_cast<uint32_t>(countryDefinition.at(_countryMode).Freq_Legal_Min / 1e6),
static_cast<uint32_t>(countryDefinition.at(_countryMode).Freq_Legal_Max / 1e6)); static_cast<uint32_t>(countryDefinition.at(_countryMode).Freq_Legal_Max / 1e6));
@ -167,9 +167,9 @@ void HoymilesRadio_CMT::loop()
// Save packet in inverter rx buffer // Save packet in inverter rx buffer
Hoymiles.getMessageOutput()->printf("RX %.2f MHz --> ", getFrequencyFromChannel(f.channel) / 1000000.0); Hoymiles.getMessageOutput()->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.getMessageOutput()->printf("| %" PRId8 " dBm\r\n", f.rssi);
inv->addRxFragment(f.fragment, f.len); inv->addRxFragment(f.fragment, f.len, f.rssi);
} else { } else {
Hoymiles.getMessageOutput()->println("Inverter Not found!"); Hoymiles.getMessageOutput()->println("Inverter Not found!");
} }
@ -194,9 +194,9 @@ void HoymilesRadio_CMT::setPALevel(const int8_t paLevel)
} }
if (_radio->setPALevel(paLevel)) { if (_radio->setPALevel(paLevel)) {
Hoymiles.getMessageOutput()->printf("CMT TX power set to %d dBm\r\n", paLevel); Hoymiles.getMessageOutput()->printf("CMT TX power set to %" PRId8 " dBm\r\n", paLevel);
} else { } else {
Hoymiles.getMessageOutput()->printf("CMT TX power %d dBm is not defined! (min: -10 dBm, max: 20 dBm)\r\n", paLevel); Hoymiles.getMessageOutput()->printf("CMT TX power %" PRId8 " dBm is not defined! (min: -10 dBm, max: 20 dBm)\r\n", paLevel);
} }
} }

View File

@ -76,11 +76,11 @@ 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.getMessageOutput()->printf("RX Channel: %" PRId8 " --> ", f.channel);
dumpBuf(f.fragment, f.len, false); dumpBuf(f.fragment, f.len, false);
Hoymiles.getMessageOutput()->printf("| %d dBm\r\n", f.rssi); Hoymiles.getMessageOutput()->printf("| %" PRId8 " dBm\r\n", f.rssi);
inv->addRxFragment(f.fragment, f.len); inv->addRxFragment(f.fragment, f.len, f.rssi);
} else { } else {
Hoymiles.getMessageOutput()->println("Inverter Not found!"); Hoymiles.getMessageOutput()->println("Inverter Not found!");
} }
@ -183,7 +183,7 @@ 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.getMessageOutput()->printf("TX %s Channel: %" PRId8 " --> ",
cmd.getCommandName().c_str(), _radio->getChannel()); cmd.getCommandName().c_str(), _radio->getChannel());
cmd.dumpDataPayload(Hoymiles.getMessageOutput()); cmd.dumpDataPayload(Hoymiles.getMessageOutput());
_radio->write(cmd.getDataPayload(), cmd.getDataSize()); _radio->write(cmd.getDataPayload(), cmd.getDataSize());

View File

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

View File

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

View File

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

View File

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

View File

@ -35,8 +35,8 @@ DevControlCommand::DevControlCommand(InverterAbstract* inv, const uint64_t route
void DevControlCommand::udpateCRC(const uint8_t len) void DevControlCommand::udpateCRC(const uint8_t len)
{ {
const uint16_t crc = crc16(&_payload[10], len); const uint16_t crc = crc16(&_payload[10], len);
_payload[10 + len] = (uint8_t)(crc >> 8); _payload[10 + len] = static_cast<uint8_t>(crc >> 8);
_payload[10 + len + 1] = (uint8_t)(crc); _payload[10 + len + 1] = static_cast<uint8_t>(crc);
} }
bool DevControlCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) bool DevControlCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id)

View File

@ -63,10 +63,10 @@ uint8_t MultiDataCommand::getDataType() const
void MultiDataCommand::setTime(const time_t time) void MultiDataCommand::setTime(const time_t time)
{ {
_payload[12] = (uint8_t)(time >> 24); _payload[12] = static_cast<uint8_t>(time >> 24);
_payload[13] = (uint8_t)(time >> 16); _payload[13] = static_cast<uint8_t>(time >> 16);
_payload[14] = (uint8_t)(time >> 8); _payload[14] = static_cast<uint8_t>(time >> 8);
_payload[15] = (uint8_t)(time); _payload[15] = static_cast<uint8_t>(time);
udpateCRC(); udpateCRC();
} }
@ -112,8 +112,8 @@ bool MultiDataCommand::handleResponse(const fragment_t fragment[], const uint8_t
void MultiDataCommand::udpateCRC() void MultiDataCommand::udpateCRC()
{ {
const uint16_t crc = crc16(&_payload[10], 14); // From data_type till password const uint16_t crc = crc16(&_payload[10], 14); // From data_type till password
_payload[24] = (uint8_t)(crc >> 8); _payload[24] = static_cast<uint8_t>(crc >> 8);
_payload[25] = (uint8_t)(crc); _payload[25] = static_cast<uint8_t>(crc);
} }
uint8_t MultiDataCommand::getTotalFragmentSize(const fragment_t fragment[], const uint8_t max_fragment_id) uint8_t MultiDataCommand::getTotalFragmentSize(const fragment_t fragment[], const uint8_t max_fragment_id)

View File

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

View File

@ -48,7 +48,7 @@ bool RealTimeRunDataCommand::handleResponse(const fragment_t fragment[], const u
const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id); const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id);
const uint8_t expectedSize = _inv->Statistics()->getExpectedByteCount(); const uint8_t expectedSize = _inv->Statistics()->getExpectedByteCount();
if (fragmentsSize < expectedSize) { if (fragmentsSize < expectedSize) {
Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %d, min expected size: %d\r\n", Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %" PRId8 ", min expected size: %" PRId8 "\r\n",
getCommandName().c_str(), fragmentsSize, expectedSize); getCommandName().c_str(), fragmentsSize, expectedSize);
return false; return false;

View File

@ -48,7 +48,7 @@ bool SystemConfigParaCommand::handleResponse(const fragment_t fragment[], const
const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id); const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id);
const uint8_t expectedSize = _inv->SystemConfigPara()->getExpectedByteCount(); const uint8_t expectedSize = _inv->SystemConfigPara()->getExpectedByteCount();
if (fragmentsSize < expectedSize) { if (fragmentsSize < expectedSize) {
Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %d, min expected size: %d\r\n", Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %" PRId8 ", min expected size: %" PRId8 "\r\n",
getCommandName().c_str(), fragmentsSize, expectedSize); getCommandName().c_str(), fragmentsSize, expectedSize);
return false; return false;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,17 +29,19 @@ static const byteAssign_t byteAssignment[] = {
}; };
HM_1CH::HM_1CH(HoymilesRadio* radio, const uint64_t serial) HM_1CH::HM_1CH(HoymilesRadio* radio, const uint64_t serial)
: HM_Abstract(radio, serial) {}; : HM_Abstract(radio, serial)
{
}
bool HM_1CH::isValidSerial(const uint64_t serial) bool HM_1CH::isValidSerial(const uint64_t serial)
{ {
// serial >= 0x112100000000 && serial <= 0x1121ffffffff // serial >= 0x112100000000 && serial <= 0x1121ffffffff
uint8_t preId[2]; uint8_t preId[2];
preId[0] = (uint8_t)(serial >> 40); preId[0] = static_cast<uint8_t>(serial >> 40);
preId[1] = (uint8_t)(serial >> 32); preId[1] = static_cast<uint8_t>(serial >> 32);
if ((uint8_t)(((((uint16_t)preId[0] << 8) | preId[1]) >> 4) & 0xff) == 0x12) { if (static_cast<uint8_t>((((static_cast<uint16_t>(preId[0]) << 8) | preId[1]) >> 4) & 0xff) == 0x12) {
return true; return true;
} }

View File

@ -37,17 +37,19 @@ static const byteAssign_t byteAssignment[] = {
}; };
HM_2CH::HM_2CH(HoymilesRadio* radio, const uint64_t serial) HM_2CH::HM_2CH(HoymilesRadio* radio, const uint64_t serial)
: HM_Abstract(radio, serial) {}; : HM_Abstract(radio, serial)
{
}
bool HM_2CH::isValidSerial(const uint64_t serial) bool HM_2CH::isValidSerial(const uint64_t serial)
{ {
// serial >= 0x114100000000 && serial <= 0x1141ffffffff // serial >= 0x114100000000 && serial <= 0x1141ffffffff
uint8_t preId[2]; uint8_t preId[2];
preId[0] = (uint8_t)(serial >> 40); preId[0] = static_cast<uint8_t>(serial >> 40);
preId[1] = (uint8_t)(serial >> 32); preId[1] = static_cast<uint8_t>(serial >> 32);
if ((uint8_t)(((((uint16_t)preId[0] << 8) | preId[1]) >> 4) & 0xff) == 0x14) { if (static_cast<uint8_t>((((static_cast<uint16_t>(preId[0]) << 8) | preId[1]) >> 4) & 0xff) == 0x14) {
return true; return true;
} }

View File

@ -50,17 +50,19 @@ static const byteAssign_t byteAssignment[] = {
}; };
HM_4CH::HM_4CH(HoymilesRadio* radio, const uint64_t serial) HM_4CH::HM_4CH(HoymilesRadio* radio, const uint64_t serial)
: HM_Abstract(radio, serial) {}; : HM_Abstract(radio, serial)
{
}
bool HM_4CH::isValidSerial(const uint64_t serial) bool HM_4CH::isValidSerial(const uint64_t serial)
{ {
// serial >= 0x116100000000 && serial <= 0x1161ffffffff // serial >= 0x116100000000 && serial <= 0x1161ffffffff
uint8_t preId[2]; uint8_t preId[2];
preId[0] = (uint8_t)(serial >> 40); preId[0] = static_cast<uint8_t>(serial >> 40);
preId[1] = (uint8_t)(serial >> 32); preId[1] = static_cast<uint8_t>(serial >> 32);
if ((uint8_t)(((((uint16_t)preId[0] << 8) | preId[1]) >> 4) & 0xff) == 0x16) { if (static_cast<uint8_t>((((static_cast<uint16_t>(preId[0]) << 8) | preId[1]) >> 4) & 0xff) == 0x16) {
return true; return true;
} }

View File

@ -14,7 +14,9 @@
#include "commands/SystemConfigParaCommand.h" #include "commands/SystemConfigParaCommand.h"
HM_Abstract::HM_Abstract(HoymilesRadio* radio, const uint64_t serial) HM_Abstract::HM_Abstract(HoymilesRadio* radio, const uint64_t serial)
: InverterAbstract(radio, serial) {}; : InverterAbstract(radio, serial)
{
}
bool HM_Abstract::sendStatsRequest() bool HM_Abstract::sendStatsRequest()
{ {
@ -50,13 +52,13 @@ bool HM_Abstract::sendAlarmLogRequest(const bool force)
if (!force) { if (!force) {
if (Statistics()->hasChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG)) { if (Statistics()->hasChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG)) {
if ((uint8_t)Statistics()->getChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG) == _lastAlarmLogCnt) { if (static_cast<uint8_t>(Statistics()->getChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG) == _lastAlarmLogCnt)) {
return false; return false;
} }
} }
} }
_lastAlarmLogCnt = (uint8_t)Statistics()->getChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG); _lastAlarmLogCnt = static_cast<uint8_t>(Statistics()->getChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG));
time_t now; time_t now;
time(&now); time(&now);
@ -217,3 +219,8 @@ bool HM_Abstract::sendGridOnProFileParaRequest()
return true; return true;
} }
bool HM_Abstract::supportsPowerDistributionLogic()
{
return false;
}

View File

@ -16,6 +16,7 @@ public:
bool sendRestartControlRequest(); bool sendRestartControlRequest();
bool resendPowerControlRequest(); bool resendPowerControlRequest();
bool sendGridOnProFileParaRequest(); bool sendGridOnProFileParaRequest();
bool supportsPowerDistributionLogic() override;
private: private:
uint8_t _lastAlarmLogCnt = 0; uint8_t _lastAlarmLogCnt = 0;
@ -23,4 +24,4 @@ private:
PowerLimitControlType _activePowerControlType = PowerLimitControlType::AbsolutNonPersistent; PowerLimitControlType _activePowerControlType = PowerLimitControlType::AbsolutNonPersistent;
uint8_t _powerState = 1; uint8_t _powerState = 1;
}; };

View File

@ -14,8 +14,8 @@ InverterAbstract::InverterAbstract(HoymilesRadio* radio, const uint64_t serial)
char serial_buff[sizeof(uint64_t) * 8 + 1]; char serial_buff[sizeof(uint64_t) * 8 + 1];
snprintf(serial_buff, sizeof(serial_buff), "%0x%08x", snprintf(serial_buff, sizeof(serial_buff), "%0x%08x",
((uint32_t)((serial >> 32) & 0xFFFFFFFF)), static_cast<uint32_t>((serial >> 32) & 0xFFFFFFFF),
((uint32_t)(serial & 0xFFFFFFFF))); static_cast<uint32_t>(serial & 0xFFFFFFFF));
_serialString = serial_buff; _serialString = serial_buff;
_alarmLogParser.reset(new AlarmLogParser()); _alarmLogParser.reset(new AlarmLogParser());
@ -137,6 +137,11 @@ bool InverterAbstract::getClearEventlogOnMidnight() const
return _clearEventlogOnMidnight; return _clearEventlogOnMidnight;
} }
int8_t InverterAbstract::getLastRssi() const
{
return _lastRssi;
}
bool InverterAbstract::sendChangeChannelRequest() bool InverterAbstract::sendChangeChannelRequest()
{ {
return false; return false;
@ -185,8 +190,10 @@ void InverterAbstract::clearRxFragmentBuffer()
_rxFragmentRetransmitCnt = 0; _rxFragmentRetransmitCnt = 0;
} }
void InverterAbstract::addRxFragment(const uint8_t fragment[], const uint8_t len) void InverterAbstract::addRxFragment(const uint8_t fragment[], const uint8_t len, const int8_t rssi)
{ {
_lastRssi = rssi;
if (len < 11) { if (len < 11) {
Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) fragment too short\r\n", __FILE__, __LINE__); Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) fragment too short\r\n", __FILE__, __LINE__);
return; return;
@ -208,7 +215,7 @@ void InverterAbstract::addRxFragment(const uint8_t fragment[], const uint8_t len
} }
if (fragmentId >= MAX_RF_FRAGMENT_COUNT) { if (fragmentId >= MAX_RF_FRAGMENT_COUNT) {
Hoymiles.getMessageOutput()->printf("ERROR: fragment id %d is too large for buffer and ignored\r\n", fragmentId); Hoymiles.getMessageOutput()->printf("ERROR: fragment id %" PRId8 " is too large for buffer and ignored\r\n", fragmentId);
return; return;
} }
@ -272,3 +279,22 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract& cmd)
return FRAGMENT_OK; return FRAGMENT_OK;
} }
void InverterAbstract::performDailyTask()
{
// Have to reset the offets first, otherwise it will
// Substract the offset from zero which leads to a high value
Statistics()->resetYieldDayCorrection();
if (getZeroYieldDayOnMidnight()) {
Statistics()->zeroDailyData();
}
if (getClearEventlogOnMidnight()) {
EventLog()->clearBuffer();
}
resetRadioStats();
}
void InverterAbstract::resetRadioStats()
{
RadioStats = {};
}

View File

@ -61,10 +61,36 @@ public:
void setClearEventlogOnMidnight(const bool enabled); void setClearEventlogOnMidnight(const bool enabled);
bool getClearEventlogOnMidnight() const; bool getClearEventlogOnMidnight() const;
int8_t getLastRssi() const;
void clearRxFragmentBuffer(); void clearRxFragmentBuffer();
void addRxFragment(const uint8_t fragment[], const uint8_t len); void addRxFragment(const uint8_t fragment[], const uint8_t len, const int8_t rssi);
uint8_t verifyAllFragments(CommandAbstract& cmd); uint8_t verifyAllFragments(CommandAbstract& cmd);
void performDailyTask();
void resetRadioStats();
struct {
// TX Request Data
uint32_t TxRequestData;
// TX Re-Request Fragment
uint32_t TxReRequestFragment;
// RX Success
uint32_t RxSuccess;
// RX Fail Partial Answer
uint32_t RxFailPartialAnswer;
// RX Fail No Answer
uint32_t RxFailNoAnswer;
// RX Fail Corrupt Data
uint32_t RxFailCorruptData;
} RadioStats = {};
virtual bool sendStatsRequest() = 0; virtual bool sendStatsRequest() = 0;
virtual bool sendAlarmLogRequest(const bool force = false) = 0; virtual bool sendAlarmLogRequest(const bool force = false) = 0;
virtual bool sendDevInfoRequest() = 0; virtual bool sendDevInfoRequest() = 0;
@ -77,6 +103,9 @@ public:
virtual bool sendChangeChannelRequest(); virtual bool sendChangeChannelRequest();
virtual bool sendGridOnProFileParaRequest() = 0; virtual bool sendGridOnProFileParaRequest() = 0;
// This feature will limit the AC output instead of limiting the DC inputs.
virtual bool supportsPowerDistributionLogic() = 0;
HoymilesRadio* getRadio(); HoymilesRadio* getRadio();
AlarmLogParser* EventLog(); AlarmLogParser* EventLog();
@ -107,6 +136,8 @@ private:
bool _zeroYieldDayOnMidnight = false; bool _zeroYieldDayOnMidnight = false;
bool _clearEventlogOnMidnight = false; bool _clearEventlogOnMidnight = false;
int8_t _lastRssi = -127;
std::unique_ptr<AlarmLogParser> _alarmLogParser; std::unique_ptr<AlarmLogParser> _alarmLogParser;
std::unique_ptr<DevInfoParser> _devInfoParser; std::unique_ptr<DevInfoParser> _devInfoParser;
std::unique_ptr<GridProfileParser> _gridProfileParser; std::unique_ptr<GridProfileParser> _gridProfileParser;

View File

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

View File

@ -243,7 +243,7 @@ void AlarmLogParser::getLogEntry(const uint8_t entryId, AlarmLogEntry_t& entry,
HOY_SEMAPHORE_TAKE(); HOY_SEMAPHORE_TAKE();
const uint32_t wcode = (uint16_t)_payloadAlarmLog[entryStartOffset] << 8 | _payloadAlarmLog[entryStartOffset + 1]; const uint32_t wcode = static_cast<uint16_t>(_payloadAlarmLog[entryStartOffset]) << 8 | _payloadAlarmLog[entryStartOffset + 1];
uint32_t startTimeOffset = 0; uint32_t startTimeOffset = 0;
if (((wcode >> 13) & 0x01) == 1) { if (((wcode >> 13) & 0x01) == 1) {
startTimeOffset = 12 * 60 * 60; startTimeOffset = 12 * 60 * 60;
@ -255,8 +255,8 @@ void AlarmLogParser::getLogEntry(const uint8_t entryId, AlarmLogEntry_t& entry,
} }
entry.MessageId = _payloadAlarmLog[entryStartOffset + 1]; entry.MessageId = _payloadAlarmLog[entryStartOffset + 1];
entry.StartTime = (((uint16_t)_payloadAlarmLog[entryStartOffset + 4] << 8) | ((uint16_t)_payloadAlarmLog[entryStartOffset + 5])) + startTimeOffset + timezoneOffset; entry.StartTime = ((static_cast<uint16_t>(_payloadAlarmLog[entryStartOffset + 4]) << 8) | static_cast<uint16_t>(_payloadAlarmLog[entryStartOffset + 5])) + startTimeOffset + timezoneOffset;
entry.EndTime = ((uint16_t)_payloadAlarmLog[entryStartOffset + 6] << 8) | ((uint16_t)_payloadAlarmLog[entryStartOffset + 7]); entry.EndTime = (static_cast<uint16_t>(_payloadAlarmLog[entryStartOffset + 6]) << 8) | static_cast<uint16_t>(_payloadAlarmLog[entryStartOffset + 7]);
HOY_SEMAPHORE_GIVE(); HOY_SEMAPHORE_GIVE();

View File

@ -61,6 +61,7 @@ const devInfo_t devInfo[] = {
{ { 0x10, 0x10, 0x71, ALL }, 500, "HMS-500-1T" }, // 02 { { 0x10, 0x10, 0x71, ALL }, 500, "HMS-500-1T" }, // 02
{ { 0x10, 0x20, 0x71, ALL }, 500, "HMS-500-1T v2" }, // 02 { { 0x10, 0x20, 0x71, ALL }, 500, "HMS-500-1T v2" }, // 02
{ { 0x10, 0x21, 0x11, ALL }, 600, "HMS-600-2T" }, // 01 { { 0x10, 0x21, 0x11, ALL }, 600, "HMS-600-2T" }, // 01
{ { 0x10, 0x21, 0x21, ALL }, 700, "HMS-700-2T" }, // 00
{ { 0x10, 0x21, 0x41, ALL }, 800, "HMS-800-2T" }, // 00 { { 0x10, 0x21, 0x41, ALL }, 800, "HMS-800-2T" }, // 00
{ { 0x10, 0x11, 0x41, ALL }, 800, "HMS-800-2T-LV" }, // 00 { { 0x10, 0x11, 0x41, ALL }, 800, "HMS-800-2T-LV" }, // 00
{ { 0x10, 0x11, 0x51, ALL }, 900, "HMS-900-2T" }, // 01 { { 0x10, 0x11, 0x51, ALL }, 900, "HMS-900-2T" }, // 01
@ -80,6 +81,7 @@ const devInfo_t devInfo[] = {
{ { 0x10, 0x33, 0x11, ALL }, 1800, "HMT-1800-6T" }, // 01 { { 0x10, 0x33, 0x11, ALL }, 1800, "HMT-1800-6T" }, // 01
{ { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250-6T" }, // 01 { { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250-6T" }, // 01
{ { 0xF1, 0x01, 0x10, ALL }, 600, "HERF-600" }, // 00
{ { 0xF1, 0x01, 0x14, ALL }, 800, "HERF-800" }, // 00 { { 0xF1, 0x01, 0x14, ALL }, 800, "HERF-800" }, // 00
{ { 0xF1, 0x01, 0x24, ALL }, 1600, "HERF-1600" }, // 00 { { 0xF1, 0x01, 0x24, ALL }, 1600, "HERF-1600" }, // 00
{ { 0xF1, 0x01, 0x22, ALL }, 1800, "HERF-1800" }, // 00 { { 0xF1, 0x01, 0x22, ALL }, 1800, "HERF-1800" }, // 00
@ -149,7 +151,7 @@ void DevInfoParser::setLastUpdateSimple(const uint32_t lastUpdate)
uint16_t DevInfoParser::getFwBuildVersion() const uint16_t DevInfoParser::getFwBuildVersion() const
{ {
HOY_SEMAPHORE_TAKE(); HOY_SEMAPHORE_TAKE();
const uint16_t ret = (((uint16_t)_payloadDevInfoAll[0]) << 8) | _payloadDevInfoAll[1]; const uint16_t ret = (static_cast<uint16_t>(_payloadDevInfoAll[0]) << 8) | _payloadDevInfoAll[1];
HOY_SEMAPHORE_GIVE(); HOY_SEMAPHORE_GIVE();
return ret; return ret;
} }
@ -158,13 +160,13 @@ time_t DevInfoParser::getFwBuildDateTime() const
{ {
struct tm timeinfo = {}; struct tm timeinfo = {};
HOY_SEMAPHORE_TAKE(); HOY_SEMAPHORE_TAKE();
timeinfo.tm_year = ((((uint16_t)_payloadDevInfoAll[2]) << 8) | _payloadDevInfoAll[3]) - 1900; timeinfo.tm_year = ((static_cast<uint16_t>(_payloadDevInfoAll[2]) << 8) | _payloadDevInfoAll[3]) - 1900;
timeinfo.tm_mon = ((((uint16_t)_payloadDevInfoAll[4]) << 8) | _payloadDevInfoAll[5]) / 100 - 1; timeinfo.tm_mon = ((static_cast<uint16_t>(_payloadDevInfoAll[4]) << 8) | _payloadDevInfoAll[5]) / 100 - 1;
timeinfo.tm_mday = ((((uint16_t)_payloadDevInfoAll[4]) << 8) | _payloadDevInfoAll[5]) % 100; timeinfo.tm_mday = ((static_cast<uint16_t>(_payloadDevInfoAll[4]) << 8) | _payloadDevInfoAll[5]) % 100;
timeinfo.tm_hour = ((((uint16_t)_payloadDevInfoAll[6]) << 8) | _payloadDevInfoAll[7]) / 100; timeinfo.tm_hour = ((static_cast<uint16_t>(_payloadDevInfoAll[6]) << 8) | _payloadDevInfoAll[7]) / 100;
timeinfo.tm_min = ((((uint16_t)_payloadDevInfoAll[6]) << 8) | _payloadDevInfoAll[7]) % 100; timeinfo.tm_min = ((static_cast<uint16_t>(_payloadDevInfoAll[6]) << 8) | _payloadDevInfoAll[7]) % 100;
HOY_SEMAPHORE_GIVE(); HOY_SEMAPHORE_GIVE();
return timegm(&timeinfo); return timegm(&timeinfo);
@ -181,7 +183,7 @@ String DevInfoParser::getFwBuildDateTimeStr() const
uint16_t DevInfoParser::getFwBootloaderVersion() const uint16_t DevInfoParser::getFwBootloaderVersion() const
{ {
HOY_SEMAPHORE_TAKE(); HOY_SEMAPHORE_TAKE();
const uint16_t ret = (((uint16_t)_payloadDevInfoAll[8]) << 8) | _payloadDevInfoAll[9]; const uint16_t ret = (static_cast<uint16_t>(_payloadDevInfoAll[8]) << 8) | _payloadDevInfoAll[9];
HOY_SEMAPHORE_GIVE(); HOY_SEMAPHORE_GIVE();
return ret; return ret;
} }
@ -189,11 +191,11 @@ uint16_t DevInfoParser::getFwBootloaderVersion() const
uint32_t DevInfoParser::getHwPartNumber() const uint32_t DevInfoParser::getHwPartNumber() const
{ {
HOY_SEMAPHORE_TAKE(); HOY_SEMAPHORE_TAKE();
const uint16_t hwpn_h = (((uint16_t)_payloadDevInfoSimple[2]) << 8) | _payloadDevInfoSimple[3]; const uint16_t hwpn_h = (static_cast<uint16_t>(_payloadDevInfoSimple[2]) << 8) | _payloadDevInfoSimple[3];
const uint16_t hwpn_l = (((uint16_t)_payloadDevInfoSimple[4]) << 8) | _payloadDevInfoSimple[5]; const uint16_t hwpn_l = (static_cast<uint16_t>(_payloadDevInfoSimple[4]) << 8) | _payloadDevInfoSimple[5];
HOY_SEMAPHORE_GIVE(); HOY_SEMAPHORE_GIVE();
return ((uint32_t)hwpn_h << 16) | ((uint32_t)hwpn_l); return (static_cast<uint32_t>(hwpn_h) << 16) | static_cast<uint32_t>(hwpn_l);
} }
String DevInfoParser::getHwVersion() const String DevInfoParser::getHwVersion() const

View File

@ -443,7 +443,7 @@ std::list<GridProfileSection_t> GridProfileParser::getProfile() const
for (uint8_t val_id = 0; val_id < section_size; val_id++) { for (uint8_t val_id = 0; val_id < section_size; val_id++) {
auto itemDefinition = itemDefinitions.at(_profileValues[section_start + val_id].ItemDefinition); auto itemDefinition = itemDefinitions.at(_profileValues[section_start + val_id].ItemDefinition);
float value = (int16_t)((_payloadGridProfile[pos] << 8) | _payloadGridProfile[pos + 1]); float value = static_cast<int16_t>((_payloadGridProfile[pos] << 8) | _payloadGridProfile[pos + 1]);
value /= itemDefinition.Divider; value /= itemDefinition.Divider;
GridProfileItem_t v; GridProfileItem_t v;

View File

@ -45,7 +45,7 @@ void SystemConfigParaParser::appendFragment(const uint8_t offset, const uint8_t*
float SystemConfigParaParser::getLimitPercent() const float SystemConfigParaParser::getLimitPercent() const
{ {
HOY_SEMAPHORE_TAKE(); HOY_SEMAPHORE_TAKE();
const float ret = ((((uint16_t)_payload[2]) << 8) | _payload[3]) / 10.0; const float ret = ((static_cast<uint16_t>(_payload[2]) << 8) | _payload[3]) / 10.0;
HOY_SEMAPHORE_GIVE(); HOY_SEMAPHORE_GIVE();
return ret; return ret;
} }
@ -53,8 +53,8 @@ float SystemConfigParaParser::getLimitPercent() const
void SystemConfigParaParser::setLimitPercent(const float value) void SystemConfigParaParser::setLimitPercent(const float value)
{ {
HOY_SEMAPHORE_TAKE(); HOY_SEMAPHORE_TAKE();
_payload[2] = ((uint16_t)(value * 10)) >> 8; _payload[2] = static_cast<uint16_t>(value * 10) >> 8;
_payload[3] = ((uint16_t)(value * 10)); _payload[3] = static_cast<uint16_t>(value * 10);
HOY_SEMAPHORE_GIVE(); HOY_SEMAPHORE_GIVE();
} }

View File

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

View File

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

View File

@ -0,0 +1,13 @@
{
"name": "SpiManager",
"keywords": "spi",
"description": "Library for managing the allocation of dedicated or shared SPI buses on the ESP32.",
"authors": {
"name": "Lennart Ferlemann"
},
"version": "0.0.1",
"frameworks": "arduino",
"platforms": [
"espressif32"
]
}

View File

@ -0,0 +1,60 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "SpiBus.h"
#include "SpiBusConfig.h"
#include "SpiCallback.h"
SpiBus::SpiBus(const std::string& _id, spi_host_device_t _host_device)
: id(_id)
, host_device(_host_device)
, cur_config(nullptr)
{
spi_bus_config_t bus_config {
.mosi_io_num = -1,
.miso_io_num = -1,
.sclk_io_num = -1,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.data4_io_num = -1,
.data5_io_num = -1,
.data6_io_num = -1,
.data7_io_num = -1,
.max_transfer_sz = 0, // defaults to SPI_MAX_DMA_LEN (=4092) or SOC_SPI_MAXIMUM_BUFFER_SIZE (=64)
.flags = 0,
.intr_flags = 0
};
#if !CONFIG_IDF_TARGET_ESP32S2
spi_dma_chan_t dma_channel = SPI_DMA_CH_AUTO;
#else
// DMA for SPI3 on ESP32-S2 is shared with ADC/DAC, so we cannot use it here
spi_dma_chan_t dma_channel = (host_device != SPI3_HOST ? SPI_DMA_CH_AUTO : SPI_DMA_DISABLED);
#endif
ESP_ERROR_CHECK(spi_bus_initialize(host_device, &bus_config, dma_channel));
}
SpiBus::~SpiBus()
{
ESP_ERROR_CHECK(spi_bus_free(host_device));
}
spi_device_handle_t SpiBus::add_device(const std::shared_ptr<SpiBusConfig>& bus_config, spi_device_interface_config_t& device_config)
{
if (!SpiCallback::patch(shared_from_this(), bus_config, device_config))
return nullptr;
spi_device_handle_t device;
ESP_ERROR_CHECK(spi_bus_add_device(host_device, &device_config, &device));
return device;
}
// TODO: add remove_device (with spi_device_acquire_bus)
void SpiBus::apply_config(SpiBusConfig* config)
{
if (cur_config)
cur_config->unpatch(host_device);
cur_config = config;
if (cur_config)
cur_config->patch(host_device);
}

View File

@ -0,0 +1,49 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <driver/spi_master.h>
#include <memory>
#include <string>
class SpiBusConfig;
class SpiBus : public std::enable_shared_from_this<SpiBus> {
public:
explicit SpiBus(const std::string& id, spi_host_device_t host_device);
SpiBus(const SpiBus&) = delete;
SpiBus& operator=(const SpiBus&) = delete;
~SpiBus();
inline __attribute__((always_inline)) void require_config(SpiBusConfig* config)
{
if (config == cur_config)
return;
apply_config(config);
}
inline __attribute__((always_inline)) void free_config(SpiBusConfig* config)
{
if (config != cur_config)
return;
apply_config(nullptr);
}
inline const std::string& get_id() const
{
return id;
}
inline spi_host_device_t get_host_device() const
{
return host_device;
}
spi_device_handle_t add_device(const std::shared_ptr<SpiBusConfig>& bus_config, spi_device_interface_config_t& device_config);
private:
void apply_config(SpiBusConfig* config);
std::string id;
spi_host_device_t host_device;
SpiBusConfig* cur_config;
};

View File

@ -0,0 +1,71 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "SpiBusConfig.h"
#include <driver/gpio.h>
#include <esp_rom_gpio.h>
#include <soc/spi_periph.h>
SpiBusConfig::SpiBusConfig(gpio_num_t _pin_mosi, gpio_num_t _pin_miso, gpio_num_t _pin_sclk)
: pin_mosi(_pin_mosi)
, pin_miso(_pin_miso)
, pin_sclk(_pin_sclk)
{
if (pin_mosi != GPIO_NUM_NC) {
ESP_ERROR_CHECK(gpio_reset_pin(pin_mosi));
ESP_ERROR_CHECK(gpio_set_direction(pin_mosi, GPIO_MODE_INPUT_OUTPUT));
}
if (pin_miso != GPIO_NUM_NC) {
ESP_ERROR_CHECK(gpio_reset_pin(pin_miso));
ESP_ERROR_CHECK(gpio_set_direction(pin_miso, GPIO_MODE_INPUT));
}
if (pin_sclk != GPIO_NUM_NC) {
ESP_ERROR_CHECK(gpio_reset_pin(pin_sclk));
ESP_ERROR_CHECK(gpio_set_direction(pin_sclk, GPIO_MODE_INPUT_OUTPUT));
}
}
SpiBusConfig::~SpiBusConfig()
{
if (pin_mosi != GPIO_NUM_NC)
ESP_ERROR_CHECK(gpio_reset_pin(pin_mosi));
if (pin_miso != GPIO_NUM_NC)
ESP_ERROR_CHECK(gpio_reset_pin(pin_miso));
if (pin_sclk != GPIO_NUM_NC)
ESP_ERROR_CHECK(gpio_reset_pin(pin_sclk));
}
void SpiBusConfig::patch(spi_host_device_t host_device)
{
if (pin_mosi != GPIO_NUM_NC) {
esp_rom_gpio_connect_out_signal(pin_mosi, spi_periph_signal[host_device].spid_out, false, false);
esp_rom_gpio_connect_in_signal(pin_mosi, spi_periph_signal[host_device].spid_in, false);
}
if (pin_miso != GPIO_NUM_NC)
esp_rom_gpio_connect_in_signal(pin_miso, spi_periph_signal[host_device].spiq_in, false);
if (pin_sclk != GPIO_NUM_NC) {
esp_rom_gpio_connect_out_signal(pin_sclk, spi_periph_signal[host_device].spiclk_out, false, false);
esp_rom_gpio_connect_in_signal(pin_sclk, spi_periph_signal[host_device].spiclk_in, false);
}
}
void SpiBusConfig::unpatch(spi_host_device_t host_device)
{
if (pin_mosi != GPIO_NUM_NC) {
esp_rom_gpio_connect_out_signal(pin_mosi, SIG_GPIO_OUT_IDX, false, false);
esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, spi_periph_signal[host_device].spid_in, false);
}
if (pin_miso != GPIO_NUM_NC)
esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, spi_periph_signal[host_device].spiq_in, false);
if (pin_sclk != GPIO_NUM_NC) {
esp_rom_gpio_connect_out_signal(pin_sclk, SIG_GPIO_OUT_IDX, false, false);
esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, spi_periph_signal[host_device].spiclk_in, false);
}
}

View File

@ -0,0 +1,21 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <hal/gpio_types.h>
#include <hal/spi_types.h>
class SpiBusConfig {
public:
explicit SpiBusConfig(gpio_num_t pin_mosi, gpio_num_t pin_miso, gpio_num_t pin_sclk);
SpiBusConfig(const SpiBusConfig&) = delete;
SpiBusConfig& operator=(const SpiBusConfig&) = delete;
~SpiBusConfig();
void patch(spi_host_device_t host_device);
void unpatch(spi_host_device_t host_device);
private:
gpio_num_t pin_mosi;
gpio_num_t pin_miso;
gpio_num_t pin_sclk;
};

View File

@ -0,0 +1,69 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "SpiCallback.h"
#include "SpiBus.h"
#include <array>
#include <optional>
namespace SpiCallback {
namespace {
struct CallbackData {
std::shared_ptr<SpiBus> bus;
std::shared_ptr<SpiBusConfig> config;
transaction_cb_t inner_pre_cb;
transaction_cb_t inner_post_cb;
};
std::array<std::optional<CallbackData>, SPI_MANAGER_CALLBACK_COUNT> instances;
template <int N>
void IRAM_ATTR fn_pre_cb(spi_transaction_t* trans)
{
instances[N]->bus->require_config(instances[N]->config.get());
if (instances[N]->inner_pre_cb)
instances[N]->inner_pre_cb(trans);
}
template <int N>
void IRAM_ATTR fn_post_cb(spi_transaction_t* trans)
{
if (instances[N]->inner_post_cb)
instances[N]->inner_post_cb(trans);
}
template <int N>
inline __attribute__((always_inline)) bool alloc(CallbackData*& instance, transaction_cb_t& pre_cb, transaction_cb_t& post_cb)
{
if constexpr (N > 0) {
if (alloc<N - 1>(instance, pre_cb, post_cb))
return true;
if (!instances[N - 1]) {
instances[N - 1].emplace();
instance = &*instances[N - 1];
pre_cb = fn_pre_cb<N - 1>;
post_cb = fn_post_cb<N - 1>;
return true;
}
}
return false;
}
}
bool patch(const std::shared_ptr<SpiBus>& bus, const std::shared_ptr<SpiBusConfig>& bus_config, spi_device_interface_config_t& device_config)
{
CallbackData* instance;
transaction_cb_t pre_cb;
transaction_cb_t post_cb;
if (!alloc<SPI_MANAGER_CALLBACK_COUNT>(instance, pre_cb, post_cb))
return false;
instance->bus = bus;
instance->config = bus_config;
instance->inner_pre_cb = device_config.pre_cb;
instance->inner_post_cb = device_config.post_cb;
device_config.pre_cb = pre_cb;
device_config.post_cb = post_cb;
return true;
}
}

View File

@ -0,0 +1,15 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <driver/spi_master.h>
#include <memory>
// Pre and post callbacks for 2 buses with 3 devices each
#define SPI_MANAGER_CALLBACK_COUNT 6
class SpiBus;
class SpiBusConfig;
namespace SpiCallback {
bool patch(const std::shared_ptr<SpiBus>& bus, const std::shared_ptr<SpiBusConfig>& bus_config, spi_device_interface_config_t& device_config);
}

View File

@ -0,0 +1,114 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "SpiManager.h"
#ifdef ARDUINO
#include <SPI.h>
#endif
SpiManager::SpiManager()
{
}
#ifdef ARDUINO
std::optional<uint8_t> SpiManager::to_arduino(spi_host_device_t host_device)
{
switch (host_device) {
#if CONFIG_IDF_TARGET_ESP32
case SPI1_HOST:
return FSPI;
case SPI2_HOST:
return HSPI;
case SPI3_HOST:
return VSPI;
#elif CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
case SPI2_HOST:
return FSPI;
case SPI3_HOST:
return HSPI;
#elif CONFIG_IDF_TARGET_ESP32C3
case SPI2_HOST:
return FSPI;
#endif
default:
return std::nullopt;
}
}
#endif
bool SpiManager::register_bus(spi_host_device_t host_device)
{
for (int i = 0; i < SPI_MANAGER_NUM_BUSES; ++i) {
if (available_buses[i])
continue;
available_buses[i] = host_device;
return true;
}
return false;
}
bool SpiManager::claim_bus(spi_host_device_t& host_device)
{
for (int i = SPI_MANAGER_NUM_BUSES - 1; i >= 0; --i) {
if (!available_buses[i])
continue;
host_device = *available_buses[i];
available_buses[i].reset();
return true;
}
return false;
}
#ifdef ARDUINO
std::optional<uint8_t> SpiManager::claim_bus_arduino()
{
spi_host_device_t host_device;
if (!claim_bus(host_device))
return std::nullopt;
return to_arduino(host_device);
}
#endif
spi_device_handle_t SpiManager::alloc_device(const std::string& bus_id, const std::shared_ptr<SpiBusConfig>& bus_config, spi_device_interface_config_t& device_config)
{
std::shared_ptr<SpiBus> shared_bus = get_shared_bus(bus_id);
if (!shared_bus)
return nullptr;
return shared_bus->add_device(bus_config, device_config);
}
std::shared_ptr<SpiBus> SpiManager::get_shared_bus(const std::string& bus_id)
{
// look for existing shared bus
for (int i = 0; i < SPI_MANAGER_NUM_BUSES; ++i) {
if (!shared_buses[i])
continue;
if (shared_buses[i]->get_id() == bus_id)
return shared_buses[i];
}
// create new shared bus
for (int i = 0; i < SPI_MANAGER_NUM_BUSES; ++i) {
if (shared_buses[i])
continue;
spi_host_device_t host_device;
if (!claim_bus(host_device))
return nullptr;
shared_buses[i] = std::make_shared<SpiBus>(bus_id, host_device);
return shared_buses[i];
}
return nullptr;
}
SpiManager SpiManagerInst;

View File

@ -0,0 +1,41 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "SpiBus.h"
#include "SpiBusConfig.h"
#include <driver/spi_master.h>
#include <array>
#include <memory>
#include <optional>
#include <utility>
#define SPI_MANAGER_NUM_BUSES SOC_SPI_PERIPH_NUM
class SpiManager {
public:
explicit SpiManager();
SpiManager(const SpiManager&) = delete;
SpiManager& operator=(const SpiManager&) = delete;
#ifdef ARDUINO
static std::optional<uint8_t> to_arduino(spi_host_device_t host_device);
#endif
bool register_bus(spi_host_device_t host_device);
bool claim_bus(spi_host_device_t& host_device);
#ifdef ARDUINO
std::optional<uint8_t> claim_bus_arduino();
#endif
spi_device_handle_t alloc_device(const std::string& bus_id, const std::shared_ptr<SpiBusConfig>& bus_config, spi_device_interface_config_t& device_config);
private:
std::shared_ptr<SpiBus> get_shared_bus(const std::string& bus_id);
std::array<std::optional<spi_host_device_t>, SPI_MANAGER_NUM_BUSES> available_buses;
std::array<std::shared_ptr<SpiBus>, SPI_MANAGER_NUM_BUSES> shared_buses;
};
extern SpiManager SpiManagerInst;

View File

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

View File

@ -36,9 +36,20 @@ def get_build_version():
return build_version return build_version
def get_build_branch():
try:
branch_name = porcelain.active_branch('.').decode('utf-8') # '.' refers to the repository root dir
except Exception as err:
branch_name = "master"
print("Firmware Branch: " + branch_name)
return branch_name
def get_firmware_specifier_build_flag(): def get_firmware_specifier_build_flag():
build_version = get_build_version() build_version = get_build_version()
build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version + "\\\"" build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version + "\\\""
build_branch = get_build_branch()
build_flag += " -D AUTO_GIT_BRANCH=\\\"" + build_branch + "\\\""
return (build_flag) return (build_flag)
@ -64,6 +75,8 @@ def do_main():
if 1: if 1:
# Add the description of the current git revision # Add the description of the current git revision
lines += 'const char *__COMPILED_GIT_HASH__ = "%s";\n' % (get_build_version()) lines += 'const char *__COMPILED_GIT_HASH__ = "%s";\n' % (get_build_version())
# ... and git branch
lines += 'const char *__COMPILED_GIT_BRANCH__ = "%s";\n' % (get_build_branch())
updateFileIfChanged(targetfile, bytes(lines, "utf-8")) updateFileIfChanged(targetfile, bytes(lines, "utf-8"))

View File

@ -18,20 +18,64 @@
Import("env") Import("env")
env = DefaultEnvironment()
platform = env.PioPlatform() platform = env.PioPlatform()
import sys import sys
from os.path import join, getsize import csv
import subprocess
import shutil
from os.path import join, getsize, exists, isdir
from os import listdir
sys.path.append(join(platform.get_package_dir("tool-esptoolpy"))) sys.path.append(join(platform.get_package_dir("tool-esptoolpy")))
import esptool import esptool
def esp32_build_filesystem(fs_name, fs_size):
filesystem_dir = env.subst("$PROJECT_DATA_DIR")
print("Creating %dKiB filesystem with content:" % (int(fs_size, 0)/1024) )
if not isdir(filesystem_dir) or not listdir(filesystem_dir):
print("No files added -> will NOT create littlefs.bin and NOT overwrite fs partition!")
return False
# this does not work on GitHub, results in 'mklittlefs: No such file or directory'
tool = shutil.which(env.subst(env["MKFSTOOL"]))
if tool is None or not exists(tool):
print("Using fallback mklittlefs")
tool = "~/.platformio/packages/tool-mklittlefs/mklittlefs"
cmd = (tool, "-c", filesystem_dir, "-s", fs_size, fs_name)
returncode = subprocess.call(cmd, shell=False)
print("Return Code:", returncode)
return True
def esp32_create_combined_bin(source, target, env): def esp32_create_combined_bin(source, target, env):
print("Generating combined binary for serial flashing") print("Generating combined binary for serial flashing")
# The offset from begin of the file where the app0 partition starts # The offset from begin of the file where the app0 partition starts
# This is defined in the partition .csv file # This is defined in the partition .csv file
app_offset = 0x10000 app_offset = 0x10000
fs_offset = -1
fs_name = env.subst("$BUILD_DIR/littlefs.bin")
with open(env.BoardConfig().get("build.partitions")) as csv_file:
print("Read partitions from ", env.BoardConfig().get("build.partitions"))
csv_reader = csv.reader(csv_file, delimiter=',')
line_count = 0
for row in csv_reader:
if line_count == 0:
print(f'{", ".join(row)}')
line_count += 1
else:
if (len(row) < 4):
continue
print(f'{row[0]} {row[1]} {row[2]} {row[3]} {row[4]}')
line_count += 1
if(row[0] == 'app0'):
app_offset = int(row[3], base=16)
elif(row[0] == 'spiffs'):
partition_size = row[4]
if esp32_build_filesystem(fs_name, partition_size):
fs_offset = int(row[3], base=16)
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin") new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin")
sections = env.subst(env.get("FLASH_EXTRA_IMAGES")) sections = env.subst(env.get("FLASH_EXTRA_IMAGES"))
@ -77,9 +121,14 @@ def esp32_create_combined_bin(source, target, env):
print(f" - {hex(app_offset)} | {firmware_name}") print(f" - {hex(app_offset)} | {firmware_name}")
cmd += [hex(app_offset), firmware_name] cmd += [hex(app_offset), firmware_name]
if fs_offset != -1:
print(f" - {hex(fs_offset)} | {fs_name}")
cmd += [hex(fs_offset), fs_name]
print('Using esptool.py arguments: %s' % ' '.join(cmd)) print('Using esptool.py arguments: %s' % ' '.join(cmd))
esptool.main(cmd) esptool.main(cmd)
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) from SCons.Script import AlwaysBuild
AlwaysBuild(env.AddPostAction("buildprog", esp32_create_combined_bin))

View File

@ -23,20 +23,20 @@ def is_tool(name):
return which(name) is not None return which(name) is not None
def replaceInFile(in_file, out_file, text, subs, flags=0): def replaceInFile(in_file, out_file, text, subs, flags=0):
""" """Function for replacing content for the given file."""
Function for replacing content for the given file
Taken from https://www.studytonight.com/python-howtos/search-and-replace-a-text-in-a-file-in-python
"""
if os.path.exists(in_file): if os.path.exists(in_file):
with open(in_file, "rb") as infile: # read the file contents
with open(out_file, "wb") as outfile: with open(in_file, "r", encoding="utf-8") as infile:
#read the file contents file_contents = infile.read()
file_contents = infile.read()
text_pattern = re.compile(re.escape(text), flags) # do replacement
file_contents = text_pattern.sub(subs, file_contents.decode('utf-8')) text_pattern = re.compile(re.escape(text), flags)
outfile.seek(0) file_contents = text_pattern.sub(subs, file_contents)
outfile.truncate()
outfile.write(file_contents.encode()) # write the result
with open(out_file, "w", encoding="utf-8") as outfile:
outfile.write(file_contents)
def main(): def main():
if (env.GetProjectOption('custom_patches', '') == ''): if (env.GetProjectOption('custom_patches', '') == ''):

View File

@ -19,7 +19,9 @@ extra_configs =
custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb
framework = arduino framework = arduino
platform = espressif32@6.8.1 platform = espressif32@6.9.0
platform_packages =
platformio/tool-mklittlefs
build_flags = build_flags =
-DPIOENV=\"$PIOENV\" -DPIOENV=\"$PIOENV\"
@ -28,6 +30,7 @@ build_flags =
-DCONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE=128 -DCONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE=128
-DCONFIG_ASYNC_TCP_QUEUE_SIZE=128 -DCONFIG_ASYNC_TCP_QUEUE_SIZE=128
-DEMC_TASK_STACK_SIZE=6400 -DEMC_TASK_STACK_SIZE=6400
; -DHOY_DEBUG_QUEUE
-Wall -Wextra -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference -Wall -Wextra -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference
; Have to remove -Werror because of ; Have to remove -Werror because of
; https://github.com/espressif/arduino-esp32/issues/9044 and ; https://github.com/espressif/arduino-esp32/issues/9044 and
@ -39,11 +42,11 @@ build_unflags =
-std=gnu++11 -std=gnu++11
lib_deps = lib_deps =
mathieucarbou/ESPAsyncWebServer @ 3.3.1 mathieucarbou/ESPAsyncWebServer @ 3.6.0
bblanchon/ArduinoJson @ 7.1.0 bblanchon/ArduinoJson @ 7.3.0
https://github.com/bertmelis/espMqttClient.git#v1.7.0 https://github.com/bertmelis/espMqttClient.git#v1.7.0
nrf24/RF24 @ 1.4.9 nrf24/RF24 @ 1.4.11
olikraus/U8g2 @ 2.35.30 olikraus/U8g2 @ 2.36.2
buelowp/sunset @ 1.1.7 buelowp/sunset @ 1.1.7
arkhipenko/TaskScheduler @ 3.8.5 arkhipenko/TaskScheduler @ 3.8.5
@ -76,6 +79,7 @@ upload_protocol = esptool
[env:generic_esp32] [env:generic_esp32]
board = esp32dev board = esp32dev
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-DPIN_MAPPING_REQUIRED=1
[env:generic_esp32_16mb_psram] [env:generic_esp32_16mb_psram]
@ -84,6 +88,7 @@ board_build.flash_mode = qio
board_build.partitions = partitions_custom_16mb.csv board_build.partitions = partitions_custom_16mb.csv
board_upload.flash_size = 16MB board_upload.flash_size = 16MB
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-DPIN_MAPPING_REQUIRED=1
-DBOARD_HAS_PSRAM -DBOARD_HAS_PSRAM
-mfix-esp32-psram-cache-issue -mfix-esp32-psram-cache-issue
@ -92,6 +97,7 @@ build_flags = ${env.build_flags}
board = esp32-c3-devkitc-02 board = esp32-c3-devkitc-02
custom_patches = ${env.custom_patches} custom_patches = ${env.custom_patches}
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-DPIN_MAPPING_REQUIRED=1
[env:generic_esp32c3_usb] [env:generic_esp32c3_usb]
@ -100,11 +106,13 @@ custom_patches = ${env.custom_patches}
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-DARDUINO_USB_MODE=1 -DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_CDC_ON_BOOT=1
-DPIN_MAPPING_REQUIRED=1
[env:generic_esp32s3] [env:generic_esp32s3]
board = esp32-s3-devkitc-1 board = esp32-s3-devkitc-1
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-DPIN_MAPPING_REQUIRED=1
[env:generic_esp32s3_usb] [env:generic_esp32s3_usb]
@ -113,6 +121,7 @@ upload_protocol = esp-builtin
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-DARDUINO_USB_MODE=1 -DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_CDC_ON_BOOT=1
-DPIN_MAPPING_REQUIRED=1
[env:generic] [env:generic]
@ -227,6 +236,7 @@ build_flags = ${env.build_flags}
-DLED0=17 -DLED0=17
-DLED1=18 -DLED1=18
-DARDUINO_USB_MODE=1 -DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
[env:opendtufusionv2] [env:opendtufusionv2]
board = esp32-s3-devkitc-1 board = esp32-s3-devkitc-1
@ -250,3 +260,32 @@ build_flags = ${env.build_flags}
-DCMT_SDIO=5 -DCMT_SDIO=5
-DARDUINO_USB_MODE=1 -DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_CDC_ON_BOOT=1
[env:opendtufusionv2_poe]
board = esp32-s3-devkitc-1
upload_protocol = esp-builtin
debug_tool = esp-builtin
debug_speed = 12000
build_flags = ${env.build_flags}
-DHOYMILES_PIN_MISO=48
-DHOYMILES_PIN_MOSI=35
-DHOYMILES_PIN_SCLK=36
-DHOYMILES_PIN_IRQ=47
-DHOYMILES_PIN_CE=38
-DHOYMILES_PIN_CS=37
-DLED0=17
-DLED1=18
-DCMT_CLK=6
-DCMT_CS=4
-DCMT_FCS=21
-DCMT_GPIO2=3
-DCMT_GPIO3=8
-DCMT_SDIO=5
-DW5500_MOSI=40
-DW5500_MISO=41
-DW5500_SCLK=39
-DW5500_CS=42
-DW5500_INT=44
-DW5500_RST=43
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1

View File

@ -13,8 +13,17 @@
CONFIG_T config; CONFIG_T config;
void ConfigurationClass::init() static std::condition_variable sWriterCv;
static std::mutex sWriterMutex;
static unsigned sWriterCount = 0;
void ConfigurationClass::init(Scheduler& scheduler)
{ {
scheduler.addTask(_loopTask);
_loopTask.setCallback(std::bind(&ConfigurationClass::loop, this));
_loopTask.setIterations(TASK_FOREVER);
_loopTask.enable();
memset(&config, 0x0, sizeof(config)); memset(&config, 0x0, sizeof(config));
} }
@ -107,7 +116,7 @@ bool ConfigurationClass::write()
display["screensaver"] = config.Display.ScreenSaver; display["screensaver"] = config.Display.ScreenSaver;
display["rotation"] = config.Display.Rotation; display["rotation"] = config.Display.Rotation;
display["contrast"] = config.Display.Contrast; display["contrast"] = config.Display.Contrast;
display["language"] = config.Display.Language; display["locale"] = config.Display.Locale;
display["diagram_duration"] = config.Display.Diagram.Duration; display["diagram_duration"] = config.Display.Diagram.Duration;
display["diagram_mode"] = config.Display.Diagram.Mode; display["diagram_mode"] = config.Display.Diagram.Mode;
@ -159,6 +168,7 @@ bool ConfigurationClass::write()
bool ConfigurationClass::read() bool ConfigurationClass::read()
{ {
File f = LittleFS.open(CONFIG_FILENAME, "r", false); File f = LittleFS.open(CONFIG_FILENAME, "r", false);
Utils::skipBom(f);
JsonDocument doc; JsonDocument doc;
@ -282,7 +292,7 @@ bool ConfigurationClass::read()
config.Display.ScreenSaver = display["screensaver"] | DISPLAY_SCREENSAVER; config.Display.ScreenSaver = display["screensaver"] | DISPLAY_SCREENSAVER;
config.Display.Rotation = display["rotation"] | DISPLAY_ROTATION; config.Display.Rotation = display["rotation"] | DISPLAY_ROTATION;
config.Display.Contrast = display["contrast"] | DISPLAY_CONTRAST; config.Display.Contrast = display["contrast"] | DISPLAY_CONTRAST;
config.Display.Language = display["language"] | DISPLAY_LANGUAGE; strlcpy(config.Display.Locale, display["locale"] | DISPLAY_LOCALE, sizeof(config.Display.Locale));
config.Display.Diagram.Duration = display["diagram_duration"] | DISPLAY_DIAGRAM_DURATION; config.Display.Diagram.Duration = display["diagram_duration"] | DISPLAY_DIAGRAM_DURATION;
config.Display.Diagram.Mode = display["diagram_mode"] | DISPLAY_DIAGRAM_MODE; config.Display.Diagram.Mode = display["diagram_mode"] | DISPLAY_DIAGRAM_MODE;
@ -318,6 +328,20 @@ bool ConfigurationClass::read()
} }
f.close(); f.close();
// Check for default DTU serial
MessageOutput.print("Check for default DTU serial... ");
if (config.Dtu.Serial == DTU_SERIAL) {
MessageOutput.print("generate serial based on ESP chip id: ");
const uint64_t dtuId = Utils::generateDtuSerial();
MessageOutput.printf("%0" PRIx32 "%08" PRIx32 "... ",
static_cast<uint32_t>((dtuId >> 32) & 0xFFFFFFFF),
static_cast<uint32_t>(dtuId & 0xFFFFFFFF));
config.Dtu.Serial = dtuId;
write();
}
MessageOutput.println("done");
return true; return true;
} }
@ -329,6 +353,8 @@ void ConfigurationClass::migrate()
return; return;
} }
Utils::skipBom(f);
JsonDocument doc; JsonDocument doc;
// Deserialize the JSON document // Deserialize the JSON document
@ -383,6 +409,22 @@ void ConfigurationClass::migrate()
} }
} }
if (config.Cfg.Version < 0x00011d00) {
JsonObject device = doc["device"];
JsonObject display = device["display"];
switch (display["language"] | 0U) {
case 0U:
strlcpy(config.Display.Locale, "en", sizeof(config.Display.Locale));
break;
case 1U:
strlcpy(config.Display.Locale, "de", sizeof(config.Display.Locale));
break;
case 2U:
strlcpy(config.Display.Locale, "fr", sizeof(config.Display.Locale));
break;
}
}
f.close(); f.close();
config.Cfg.Version = CONFIG_VERSION; config.Cfg.Version = CONFIG_VERSION;
@ -390,11 +432,16 @@ void ConfigurationClass::migrate()
read(); read();
} }
CONFIG_T& ConfigurationClass::get() CONFIG_T const& ConfigurationClass::get()
{ {
return config; return config;
} }
ConfigurationClass::WriteGuard ConfigurationClass::getWriteGuard()
{
return WriteGuard();
}
INVERTER_CONFIG_T* ConfigurationClass::getFreeInverterSlot() INVERTER_CONFIG_T* ConfigurationClass::getFreeInverterSlot()
{ {
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
@ -439,4 +486,30 @@ void ConfigurationClass::deleteInverterById(const uint8_t id)
} }
} }
void ConfigurationClass::loop()
{
std::unique_lock<std::mutex> lock(sWriterMutex);
if (sWriterCount == 0) { return; }
sWriterCv.notify_all();
sWriterCv.wait(lock, [] { return sWriterCount == 0; });
}
CONFIG_T& ConfigurationClass::WriteGuard::getConfig()
{
return config;
}
ConfigurationClass::WriteGuard::WriteGuard()
: _lock(sWriterMutex)
{
sWriterCount++;
sWriterCv.wait(_lock);
}
ConfigurationClass::WriteGuard::~WriteGuard() {
sWriterCount--;
if (sWriterCount == 0) { sWriterCv.notify_all(); }
}
ConfigurationClass Configuration; ConfigurationClass Configuration;

View File

@ -4,6 +4,7 @@
*/ */
#include "Display_Graphic.h" #include "Display_Graphic.h"
#include "Datastore.h" #include "Datastore.h"
#include "I18n.h"
#include <NetworkSettings.h> #include <NetworkSettings.h>
#include <map> #include <map>
#include <time.h> #include <time.h>
@ -16,18 +17,11 @@ std::map<DisplayType_t, std::function<U8G2*(uint8_t, uint8_t, uint8_t, uint8_t)>
{ DisplayType_t::ST7567_GM12864I_59N, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_ST7567_ENH_DG128064I_F_HW_I2C(U8G2_R0, reset, clock, data); } }, { DisplayType_t::ST7567_GM12864I_59N, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_ST7567_ENH_DG128064I_F_HW_I2C(U8G2_R0, reset, clock, data); } },
}; };
// Language defintion, respect order in languages[] and translation lists // Language defintion, respect order in translation lists
#define I18N_LOCALE_EN 0 #define I18N_LOCALE_EN 0
#define I18N_LOCALE_DE 1 #define I18N_LOCALE_DE 1
#define I18N_LOCALE_FR 2 #define I18N_LOCALE_FR 2
// Languages supported. Note: the order is important and must match locale_translations.h
const uint8_t languages[] = {
I18N_LOCALE_EN,
I18N_LOCALE_DE,
I18N_LOCALE_FR
};
static const char* const i18n_offline[] = { "Offline", "Offline", "Offline" }; 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" };
@ -166,9 +160,34 @@ void DisplayGraphicClass::setOrientation(const uint8_t rotation)
calcLineHeights(); calcLineHeights();
} }
void DisplayGraphicClass::setLanguage(const uint8_t language) void DisplayGraphicClass::setLocale(const String& locale)
{ {
_display_language = language < sizeof(languages) / sizeof(languages[0]) ? language : DISPLAY_LANGUAGE; _display_language = locale;
uint8_t idx = I18N_LOCALE_EN;
if (locale == "de") {
idx = I18N_LOCALE_DE;
} else if (locale == "fr") {
idx = I18N_LOCALE_FR;
}
_i18n_date_format = i18n_date_format[idx];
_i18n_offline = i18n_offline[idx];
_i18n_current_power_w = i18n_current_power_w[idx];
_i18n_current_power_kw = i18n_current_power_kw[idx];
_i18n_yield_today_wh = i18n_yield_today_wh[idx];
_i18n_yield_today_kwh = i18n_yield_today_kwh[idx];
_i18n_yield_total_kwh = i18n_yield_total_kwh[idx];
_i18n_yield_total_mwh = i18n_yield_total_mwh[idx];
I18n.readDisplayStrings(locale,
_i18n_date_format,
_i18n_offline,
_i18n_current_power_w,
_i18n_current_power_kw,
_i18n_yield_today_wh,
_i18n_yield_today_kwh,
_i18n_yield_total_kwh,
_i18n_yield_total_mwh);
} }
void DisplayGraphicClass::setDiagramMode(DiagramMode_t mode) void DisplayGraphicClass::setDiagramMode(DiagramMode_t mode)
@ -225,9 +244,9 @@ void DisplayGraphicClass::loop()
if (showText) { if (showText) {
const float watts = Datastore.getTotalAcPowerEnabled(); const float watts = Datastore.getTotalAcPowerEnabled();
if (watts > 999) { if (watts > 999) {
snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_kw[_display_language], watts / 1000); snprintf(_fmtText, sizeof(_fmtText), _i18n_current_power_kw.c_str(), watts / 1000);
} else { } else {
snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_w[_display_language], watts); snprintf(_fmtText, sizeof(_fmtText), _i18n_current_power_w.c_str(), watts);
} }
printText(_fmtText, 0); printText(_fmtText, 0);
} }
@ -237,7 +256,7 @@ void DisplayGraphicClass::loop()
//=====> Offline =========== //=====> Offline ===========
else { else {
printText(i18n_offline[_display_language], 0); printText(_i18n_offline.c_str(), 0);
// check if it's time to enter power saving mode // check if it's time to enter power saving mode
if (millis() - _previousMillis >= (_interval * 2)) { if (millis() - _previousMillis >= (_interval * 2)) {
displayPowerSave = enablePowerSafe; displayPowerSave = enablePowerSafe;
@ -249,16 +268,16 @@ void DisplayGraphicClass::loop()
// Daily production // Daily production
float wattsToday = Datastore.getTotalAcYieldDayEnabled(); float wattsToday = Datastore.getTotalAcYieldDayEnabled();
if (wattsToday >= 10000) { if (wattsToday >= 10000) {
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_kwh[_display_language], wattsToday / 1000); snprintf(_fmtText, sizeof(_fmtText), _i18n_yield_today_kwh.c_str(), wattsToday / 1000);
} else { } else {
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], wattsToday); snprintf(_fmtText, sizeof(_fmtText), _i18n_yield_today_wh.c_str(), wattsToday);
} }
printText(_fmtText, 1); printText(_fmtText, 1);
// Total production // Total production
const float wattsTotal = Datastore.getTotalAcYieldTotalEnabled(); const float wattsTotal = Datastore.getTotalAcYieldTotalEnabled();
auto const format = (wattsTotal >= 1000) ? i18n_yield_total_mwh : i18n_yield_total_kwh; auto const format = (wattsTotal >= 1000) ? _i18n_yield_total_mwh : _i18n_yield_total_kwh;
snprintf(_fmtText, sizeof(_fmtText), format[_display_language], wattsTotal); snprintf(_fmtText, sizeof(_fmtText), format.c_str(), wattsTotal);
printText(_fmtText, 2); printText(_fmtText, 2);
//=====> IP or Date-Time ======== //=====> IP or Date-Time ========
@ -268,7 +287,7 @@ void DisplayGraphicClass::loop()
} else { } else {
// Get current time // Get current time
time_t now = time(nullptr); time_t now = time(nullptr);
strftime(_fmtText, sizeof(_fmtText), i18n_date_format[_display_language], localtime(&now)); strftime(_fmtText, sizeof(_fmtText), _i18n_date_format.c_str(), localtime(&now));
printText(_fmtText, 3); printText(_fmtText, 3);
} }
} }

View File

@ -87,7 +87,7 @@ void DisplayGraphicDiagramClass::redraw(uint8_t screenSaverOffsetX, uint8_t xPos
if (maxWatts > 999) { if (maxWatts > 999) {
snprintf(fmtText, sizeof(fmtText), "%2.1fkW", maxWatts / 1000); snprintf(fmtText, sizeof(fmtText), "%2.1fkW", maxWatts / 1000);
} else { } else {
snprintf(fmtText, sizeof(fmtText), "%dW", static_cast<uint16_t>(maxWatts)); snprintf(fmtText, sizeof(fmtText), "%" PRId16 "W", static_cast<uint16_t>(maxWatts));
} }
if (isFullscreen) { if (isFullscreen) {

158
src/I18n.cpp Normal file
View File

@ -0,0 +1,158 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2024 Thomas Basler and others
*/
#include "I18n.h"
#include "MessageOutput.h"
#include "Utils.h"
#include "defaults.h"
#include <ArduinoJson.h>
#include <LittleFS.h>
I18nClass I18n;
I18nClass::I18nClass()
{
}
void I18nClass::init(Scheduler& scheduler)
{
readLangPacks();
}
std::list<LanguageInfo_t> I18nClass::getAvailableLanguages()
{
return _availLanguages;
}
String I18nClass::getFilenameByLocale(const String& locale) const
{
auto it = std::find_if(_availLanguages.begin(), _availLanguages.end(), [locale](const LanguageInfo_t& elem) {
return elem.code == locale;
});
if (it != _availLanguages.end()) {
return it->filename;
} else {
return String();
}
}
void I18nClass::readDisplayStrings(
const String& locale,
String& date_format,
String& offline,
String& power_w, String& power_kw,
String& yield_today_wh, String& yield_today_kwh,
String& yield_total_kwh, String& yield_total_mwh)
{
auto filename = getFilenameByLocale(locale);
if (filename == "") {
return;
}
JsonDocument filter;
filter["display"] = true;
File f = LittleFS.open(filename, "r", false);
JsonDocument doc;
// Deserialize the JSON document
const DeserializationError error = deserializeJson(doc, f, DeserializationOption::Filter(filter));
if (error) {
MessageOutput.printf("Failed to read file %s\r\n", filename.c_str());
f.close();
return;
}
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return;
}
auto displayData = doc["display"];
if (displayData["date_format"].as<String>() != "null") {
date_format = displayData["date_format"].as<String>();
}
if (displayData["offline"].as<String>() != "null") {
offline = displayData["offline"].as<String>();
}
if (displayData["power_w"].as<String>() != "null") {
power_w = displayData["power_w"].as<String>();
}
if (displayData["power_kw"].as<String>() != "null") {
power_kw = displayData["power_kw"].as<String>();
}
if (displayData["yield_today_wh"].as<String>() != "null") {
yield_today_wh = displayData["yield_today_wh"].as<String>();
}
if (displayData["yield_today_kwh"].as<String>() != "null") {
yield_today_kwh = displayData["yield_today_kwh"].as<String>();
}
if (displayData["yield_total_kwh"].as<String>() != "null") {
yield_total_kwh = displayData["yield_total_kwh"].as<String>();
}
if (displayData["yield_total_mwh"].as<String>() != "null") {
yield_total_mwh = displayData["yield_total_mwh"].as<String>();
}
f.close();
}
void I18nClass::readLangPacks()
{
auto root = LittleFS.open("/");
auto file = root.getNextFileName();
while (file != "") {
if (file.endsWith(LANG_PACK_SUFFIX)) {
MessageOutput.printf("Read File %s\r\n", file.c_str());
readConfig(file);
}
file = root.getNextFileName();
}
root.close();
}
void I18nClass::readConfig(String file)
{
JsonDocument filter;
filter["meta"] = true;
File f = LittleFS.open(file, "r", false);
JsonDocument doc;
// Deserialize the JSON document
const DeserializationError error = deserializeJson(doc, f, DeserializationOption::Filter(filter));
if (error) {
MessageOutput.printf("Failed to read file %s\r\n", file.c_str());
f.close();
return;
}
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return;
}
LanguageInfo_t lang;
lang.code = String(doc["meta"]["code"] | "");
lang.name = String(doc["meta"]["name"] | "");
lang.filename = file;
if (lang.code != "" && lang.name != "") {
_availLanguages.push_back(lang);
} else {
MessageOutput.printf("Invalid meta data\r\n");
}
f.close();
}

View File

@ -8,20 +8,7 @@
#include "PinMapping.h" #include "PinMapping.h"
#include "SunPosition.h" #include "SunPosition.h"
#include <Hoymiles.h> #include <Hoymiles.h>
#include <SpiManager.h>
// the NRF shall use the second externally usable HW SPI controller
// for ESP32 that is the so-called VSPI, for ESP32-S2/S3 it is now called implicitly
// HSPI, as it has shifted places for these chip generations
// for all generations, this is equivalent to SPI3_HOST in the lower level driver
// For ESP32-C2, the only externally usable HW SPI controller is SPI2, its signal names
// being prefixed with FSPI.
#if CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
#define SPI_NRF HSPI
#elif CONFIG_IDF_TARGET_ESP32C3
#define SPI_NRF FSPI
#else
#define SPI_NRF VSPI
#endif
InverterSettingsClass InverterSettings; InverterSettingsClass InverterSettings;
@ -44,7 +31,10 @@ void InverterSettingsClass::init(Scheduler& scheduler)
if (PinMapping.isValidNrf24Config() || PinMapping.isValidCmt2300Config()) { if (PinMapping.isValidNrf24Config() || PinMapping.isValidCmt2300Config()) {
if (PinMapping.isValidNrf24Config()) { if (PinMapping.isValidNrf24Config()) {
SPIClass* spiClass = new SPIClass(SPI_NRF); auto spi_bus = SpiManagerInst.claim_bus_arduino();
ESP_ERROR_CHECK(spi_bus ? ESP_OK : ESP_FAIL);
SPIClass* spiClass = new SPIClass(*spi_bus);
spiClass->begin(pin.nrf24_clk, pin.nrf24_miso, pin.nrf24_mosi, pin.nrf24_cs); spiClass->begin(pin.nrf24_clk, pin.nrf24_miso, pin.nrf24_mosi, pin.nrf24_cs);
Hoymiles.initNRF(spiClass, pin.nrf24_en, pin.nrf24_irq); Hoymiles.initNRF(spiClass, pin.nrf24_en, pin.nrf24_irq);
} }
@ -70,10 +60,10 @@ void InverterSettingsClass::init(Scheduler& scheduler)
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.printf(" Adding inverter: %0" PRIx32 "%08" PRIx32 " - %s",
MessageOutput.print(config.Inverter[i].Serial, HEX); static_cast<uint32_t>((config.Inverter[i].Serial >> 32) & 0xFFFFFFFF),
MessageOutput.print(" - "); static_cast<uint32_t>(config.Inverter[i].Serial & 0xFFFFFFFF),
MessageOutput.print(config.Inverter[i].Name); config.Inverter[i].Name);
auto inv = Hoymiles.addInverter( auto inv = Hoymiles.addInverter(
config.Inverter[i].Name, config.Inverter[i].Name,
config.Inverter[i].Serial); config.Inverter[i].Serial);

View File

@ -7,6 +7,7 @@
#include "MqttSettings.h" #include "MqttSettings.h"
#include "NetworkSettings.h" #include "NetworkSettings.h"
#include <Hoymiles.h> #include <Hoymiles.h>
#include <CpuTemperature.h>
MqttHandleDtuClass MqttHandleDtu; MqttHandleDtuClass MqttHandleDtu;
@ -31,11 +32,20 @@ void MqttHandleDtuClass::loop()
return; return;
} }
MqttSettings.publish("dtu/uptime", String(millis() / 1000)); MqttSettings.publish("dtu/uptime", String(esp_timer_get_time() / 1000000));
MqttSettings.publish("dtu/ip", NetworkSettings.localIP().toString()); MqttSettings.publish("dtu/ip", NetworkSettings.localIP().toString());
MqttSettings.publish("dtu/hostname", NetworkSettings.getHostname()); MqttSettings.publish("dtu/hostname", NetworkSettings.getHostname());
MqttSettings.publish("dtu/heap/size", String(ESP.getHeapSize()));
MqttSettings.publish("dtu/heap/free", String(ESP.getFreeHeap()));
MqttSettings.publish("dtu/heap/minfree", String(ESP.getMinFreeHeap()));
MqttSettings.publish("dtu/heap/maxalloc", String(ESP.getMaxAllocHeap()));
if (NetworkSettings.NetworkMode() == network_mode::WiFi) { if (NetworkSettings.NetworkMode() == network_mode::WiFi) {
MqttSettings.publish("dtu/rssi", String(WiFi.RSSI())); MqttSettings.publish("dtu/rssi", String(WiFi.RSSI()));
MqttSettings.publish("dtu/bssid", WiFi.BSSIDstr()); MqttSettings.publish("dtu/bssid", WiFi.BSSIDstr());
} }
float temperature = CpuTemperature.read();
if (!std::isnan(temperature)) {
MqttSettings.publish("dtu/temperature", String(temperature));
}
} }

View File

@ -7,8 +7,8 @@
#include "MqttSettings.h" #include "MqttSettings.h"
#include "NetworkSettings.h" #include "NetworkSettings.h"
#include "Utils.h" #include "Utils.h"
#include "defaults.h"
#include "__compiled_constants.h" #include "__compiled_constants.h"
#include "defaults.h"
MqttHandleHassClass MqttHandleHass; MqttHandleHassClass MqttHandleHass;
@ -58,29 +58,47 @@ void MqttHandleHassClass::publishConfig()
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
// publish DTU sensors // publish DTU sensors
publishDtuSensor("IP", "", "diagnostic", "mdi:network-outline", "", ""); publishDtuSensor("IP", "dtu/ip", "", "mdi:network-outline", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("WiFi Signal", "signal_strength", "diagnostic", "", "dBm", "rssi"); publishDtuSensor("WiFi Signal", "dtu/rssi", "dBm", "", DEVICE_CLS_SIGNAL_STRENGTH, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Uptime", "duration", "diagnostic", "", "s", ""); publishDtuSensor("Uptime", "dtu/uptime", "s", "", DEVICE_CLS_DURATION, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuBinarySensor("Status", "connectivity", "diagnostic", config.Mqtt.Lwt.Value_Online, config.Mqtt.Lwt.Value_Offline, config.Mqtt.Lwt.Topic); publishDtuSensor("Temperature", "dtu/temperature", "°C", "", DEVICE_CLS_TEMPERATURE, STATE_CLS_MEASUREMENT, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Heap Size", "dtu/heap/size", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Heap Free", "dtu/heap/free", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Largest Free Heap Block", "dtu/heap/maxalloc", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Lifetime Minimum Free Heap", "dtu/heap/minfree", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
yield(); publishDtuSensor("Yield Total", "ac/yieldtotal", "kWh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE);
publishDtuSensor("Yield Day", "ac/yieldday", "Wh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE);
publishDtuSensor("AC Power", "ac/power", "W", "", DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT, CATEGORY_NONE);
publishDtuSensor("DC Power", "dc/power", "W", "", DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT, CATEGORY_NONE);
publishDtuBinarySensor("Status", config.Mqtt.Lwt.Topic, config.Mqtt.Lwt.Value_Online, config.Mqtt.Lwt.Value_Offline, DEVICE_CLS_CONNECTIVITY, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
// Loop all inverters // Loop all inverters
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
auto inv = Hoymiles.getInverterByPos(i); auto inv = Hoymiles.getInverterByPos(i);
publishInverterButton(inv, "Turn Inverter Off", "mdi:power-plug-off", "config", "", "cmd/power", "0"); publishInverterButton(inv, "Turn Inverter Off", "cmd/power", "0", "mdi:power-plug-off", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterButton(inv, "Turn Inverter On", "mdi:power-plug", "config", "", "cmd/power", "1"); publishInverterButton(inv, "Turn Inverter On", "cmd/power", "1", "mdi:power-plug", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterButton(inv, "Restart Inverter", "", "config", "restart", "cmd/restart", "1"); publishInverterButton(inv, "Restart Inverter", "cmd/restart", "1", "", DEVICE_CLS_RESTART, STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterButton(inv, "Reset Radio Statistics", "cmd/reset_rf_stats", "1", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterNumber(inv, "Limit NonPersistent Relative", "mdi:speedometer", "config", "cmd/limit_nonpersistent_relative", "status/limit_relative", "%", 0, 100, 0.1); publishInverterNumber(inv, "Limit NonPersistent Relative", "status/limit_relative", "cmd/limit_nonpersistent_relative", 0, 100, 0.1, "%", "mdi:speedometer", STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterNumber(inv, "Limit Persistent Relative", "mdi:speedometer", "config", "cmd/limit_persistent_relative", "status/limit_relative", "%", 0, 100, 0.1); publishInverterNumber(inv, "Limit Persistent Relative", "status/limit_relative", "cmd/limit_persistent_relative", 0, 100, 0.1, "%", "mdi:speedometer", STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterNumber(inv, "Limit NonPersistent Absolute", "mdi:speedometer", "config", "cmd/limit_nonpersistent_absolute", "status/limit_absolute", "W", 0, MAX_INVERTER_LIMIT); publishInverterNumber(inv, "Limit NonPersistent Absolute", "status/limit_absolute", "cmd/limit_nonpersistent_absolute", 0, MAX_INVERTER_LIMIT, 1, "W", "mdi:speedometer", STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterNumber(inv, "Limit Persistent Absolute", "mdi:speedometer", "config", "cmd/limit_persistent_absolute", "status/limit_absolute", "W", 0, MAX_INVERTER_LIMIT); publishInverterNumber(inv, "Limit Persistent Absolute", "status/limit_absolute", "cmd/limit_persistent_absolute", 0, MAX_INVERTER_LIMIT, 1, "W", "mdi:speedometer", STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterBinarySensor(inv, "Reachable", "status/reachable", "1", "0"); publishInverterBinarySensor(inv, "Reachable", "status/reachable", "1", "0", DEVICE_CLS_CONNECTIVITY, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterBinarySensor(inv, "Producing", "status/producing", "1", "0"); publishInverterBinarySensor(inv, "Producing", "status/producing", "1", "0", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_NONE);
publishInverterSensor(inv, "TX Requests", "radio/tx_request", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "RX Success", "radio/rx_success", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "RX Fail Receive Nothing", "radio/rx_fail_nothing", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "RX Fail Receive Partial", "radio/rx_fail_partial", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "RX Fail Receive Corrupt", "radio/rx_fail_corrupt", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "TX Re-Request Fragment", "radio/tx_re_request", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "RSSI", "radio/rssi", "dBm", "", DEVICE_CLS_SIGNAL_STRENGTH, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
// Loop all channels // Loop all channels
for (auto& t : inv->Statistics()->getChannelTypes()) { for (auto& t : inv->Statistics()->getChannelTypes()) {
@ -94,8 +112,6 @@ void MqttHandleHassClass::publishConfig()
} }
} }
} }
yield();
} }
} }
@ -128,8 +144,6 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr<InverterAbstract>
if (!clear) { if (!clear) {
const String stateTopic = MqttSettings.getPrefix() + MqttHandleInverter.getTopic(inv, type, channel, fieldType.fieldId); const String stateTopic = MqttSettings.getPrefix() + MqttHandleInverter.getTopic(inv, type, channel, fieldType.fieldId);
const char* devCls = deviceClasses[fieldType.deviceClsId];
const char* stateCls = stateClasses[fieldType.stateClsId];
String name; String name;
if (type != TYPE_DC) { if (type != TYPE_DC) {
@ -138,46 +152,34 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr<InverterAbstract>
name = "CH" + chanNum + " " + fieldName; name = "CH" + chanNum + " " + fieldName;
} }
String unit_of_measure = inv->Statistics()->getChannelFieldUnit(type, channel, fieldType.fieldId);
JsonDocument root; JsonDocument root;
createInverterInfo(root, inv);
addCommonMetadata(root, unit_of_measure, "", fieldType.deviceClsId, fieldType.stateClsId, CATEGORY_NONE);
root["name"] = name; root["name"] = name;
root["stat_t"] = stateTopic; root["stat_t"] = stateTopic;
root["uniq_id"] = serial + "_ch" + chanNum + "_" + fieldName; root["uniq_id"] = serial + "_ch" + chanNum + "_" + fieldName;
String unit_of_measure = inv->Statistics()->getChannelFieldUnit(type, channel, fieldType.fieldId);
if (unit_of_measure != "") {
root["unit_of_meas"] = unit_of_measure;
}
createInverterInfo(root, inv);
if (Configuration.get().Mqtt.Hass.Expire) { if (Configuration.get().Mqtt.Hass.Expire) {
root["exp_aft"] = Hoymiles.getNumInverters() * max<uint32_t>(Hoymiles.PollInterval(), Configuration.get().Mqtt.PublishInterval) * inv->getReachableThreshold(); root["exp_aft"] = Hoymiles.getNumInverters() * max<uint32_t>(Hoymiles.PollInterval(), Configuration.get().Mqtt.PublishInterval) * inv->getReachableThreshold();
} }
if (devCls != 0) {
root["dev_cla"] = devCls;
}
if (stateCls != 0) {
root["stat_cla"] = stateCls;
}
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { publish(configTopic, root);
return;
}
String buffer;
serializeJson(root, buffer);
publish(configTopic, buffer);
} else { } else {
publish(configTopic, ""); publish(configTopic, "");
} }
} }
void MqttHandleHassClass::publishInverterButton(std::shared_ptr<InverterAbstract> inv, const char* caption, const char* icon, const char* category, const char* deviceClass, const char* subTopic, const char* payload) void MqttHandleHassClass::publishInverterButton(
std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& payload,
const String& icon,
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category)
{ {
const String serial = inv->serialString(); const String serial = inv->serialString();
String buttonId = caption; String buttonId = name;
buttonId.replace(" ", "_"); buttonId.replace(" ", "_");
buttonId.toLowerCase(); buttonId.toLowerCase();
@ -185,41 +187,30 @@ void MqttHandleHassClass::publishInverterButton(std::shared_ptr<InverterAbstract
+ "/" + buttonId + "/" + buttonId
+ "/config"; + "/config";
const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + subTopic; const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + state_topic;
JsonDocument root; JsonDocument root;
createInverterInfo(root, inv);
addCommonMetadata(root, "", icon, device_class, state_class, category);
root["name"] = caption; root["name"] = name;
root["uniq_id"] = serial + "_" + buttonId; root["uniq_id"] = serial + "_" + buttonId;
if (strcmp(icon, "")) {
root["ic"] = icon;
}
if (strcmp(deviceClass, "")) {
root["dev_cla"] = deviceClass;
}
root["ent_cat"] = category;
root["cmd_t"] = cmdTopic; root["cmd_t"] = cmdTopic;
root["payload_press"] = payload; root["payload_press"] = payload;
createInverterInfo(root, inv); publish(configTopic, root);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
serializeJson(root, buffer);
publish(configTopic, buffer);
} }
void MqttHandleHassClass::publishInverterNumber( void MqttHandleHassClass::publishInverterNumber(
std::shared_ptr<InverterAbstract> inv, const char* caption, const char* icon, const char* category, std::shared_ptr<InverterAbstract> inv, const String& name,
const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const String& stateTopic, const String& command_topic,
const int16_t min, const int16_t max, float step) const int16_t min, const int16_t max, float step,
const String& unit_of_measure, const String& icon,
const StateClassType state_class, const CategoryType category)
{ {
const String serial = inv->serialString(); const String serial = inv->serialString();
String buttonId = caption; String buttonId = name;
buttonId.replace(" ", "_"); buttonId.replace(" ", "_");
buttonId.toLowerCase(); buttonId.toLowerCase();
@ -227,150 +218,22 @@ void MqttHandleHassClass::publishInverterNumber(
+ "/" + buttonId + "/" + buttonId
+ "/config"; + "/config";
const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + commandTopic; const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + command_topic;
const String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic; const String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic;
JsonDocument root; JsonDocument root;
createInverterInfo(root, inv);
addCommonMetadata(root, unit_of_measure, icon, DEVICE_CLS_NONE, state_class, category);
root["name"] = caption; root["name"] = name;
root["uniq_id"] = serial + "_" + buttonId; root["uniq_id"] = serial + "_" + buttonId;
if (strcmp(icon, "")) {
root["ic"] = icon;
}
root["ent_cat"] = category;
root["cmd_t"] = cmdTopic; root["cmd_t"] = cmdTopic;
root["stat_t"] = statTopic; root["stat_t"] = statTopic;
root["unit_of_meas"] = unitOfMeasure;
root["min"] = min; root["min"] = min;
root["max"] = max; root["max"] = max;
root["step"] = step; root["step"] = step;
createInverterInfo(root, inv); publish(configTopic, root);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
serializeJson(root, buffer);
publish(configTopic, buffer);
}
void MqttHandleHassClass::publishInverterBinarySensor(std::shared_ptr<InverterAbstract> inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off)
{
const String serial = inv->serialString();
String sensorId = caption;
sensorId.replace(" ", "_");
sensorId.toLowerCase();
const String configTopic = "binary_sensor/dtu_" + serial
+ "/" + sensorId
+ "/config";
const String statTopic = MqttSettings.getPrefix() + serial + "/" + 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;
createInverterInfo(root, inv);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
serializeJson(root, buffer);
publish(configTopic, buffer);
}
void MqttHandleHassClass::publishDtuSensor(const char* name, const char* device_class, const char* category, const char* icon, const char* unit_of_measure, const char* subTopic)
{
String id = name;
id.toLowerCase();
id.replace(" ", "_");
String topic = subTopic;
if (topic == "") {
topic = id;
}
JsonDocument root;
root["name"] = name;
root["uniq_id"] = getDtuUniqueId() + "_" + id;
if (strcmp(device_class, "")) {
root["dev_cla"] = device_class;
}
if (strcmp(category, "")) {
root["ent_cat"] = category;
}
if (strcmp(icon, "")) {
root["ic"] = icon;
}
if (strcmp(unit_of_measure, "")) {
root["unit_of_meas"] = unit_of_measure;
}
root["stat_t"] = MqttSettings.getPrefix() + "dtu" + "/" + topic;
root["avty_t"] = MqttSettings.getPrefix() + Configuration.get().Mqtt.Lwt.Topic;
const CONFIG_T& config = Configuration.get();
root["pl_avail"] = config.Mqtt.Lwt.Value_Online;
root["pl_not_avail"] = config.Mqtt.Lwt.Value_Offline;
createDtuInfo(root);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
const String configTopic = "sensor/" + getDtuUniqueId() + "/" + id + "/config";
serializeJson(root, buffer);
publish(configTopic, buffer);
}
void MqttHandleHassClass::publishDtuBinarySensor(const char* name, const char* device_class, const char* category, const char* payload_on, const char* payload_off, const char* subTopic)
{
String id = name;
id.toLowerCase();
id.replace(" ", "_");
String topic = subTopic;
if (!strcmp(subTopic, "")) {
topic = String("dtu/") + "/" + id;
}
JsonDocument root;
root["name"] = name;
root["uniq_id"] = getDtuUniqueId() + "_" + id;
root["stat_t"] = MqttSettings.getPrefix() + topic;
root["pl_on"] = payload_on;
root["pl_off"] = payload_off;
if (strcmp(device_class, "")) {
root["dev_cla"] = device_class;
}
if (strcmp(category, "")) {
root["ent_cat"] = category;
}
createDtuInfo(root);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
const String configTopic = "binary_sensor/" + getDtuUniqueId() + "/" + id + "/config";
serializeJson(root, buffer);
publish(configTopic, buffer);
} }
void MqttHandleHassClass::createInverterInfo(JsonDocument& root, std::shared_ptr<InverterAbstract> inv) void MqttHandleHassClass::createInverterInfo(JsonDocument& root, std::shared_ptr<InverterAbstract> inv)
@ -433,4 +296,129 @@ void MqttHandleHassClass::publish(const String& subtopic, const String& payload)
String topic = Configuration.get().Mqtt.Hass.Topic; String topic = Configuration.get().Mqtt.Hass.Topic;
topic += subtopic; topic += subtopic;
MqttSettings.publishGeneric(topic, payload, Configuration.get().Mqtt.Hass.Retain); MqttSettings.publishGeneric(topic, payload, Configuration.get().Mqtt.Hass.Retain);
yield();
}
void MqttHandleHassClass::publish(const String& subtopic, const JsonDocument& doc)
{
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
serializeJson(doc, buffer);
publish(subtopic, buffer);
}
void MqttHandleHassClass::addCommonMetadata(
JsonDocument& doc,
const String& unit_of_measure, const String& icon,
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category)
{
if (unit_of_measure != "") {
doc["unit_of_meas"] = unit_of_measure;
}
if (icon != "") {
doc["ic"] = icon;
}
if (device_class != DEVICE_CLS_NONE) {
doc["dev_cla"] = deviceClass_name[device_class];
}
if (state_class != STATE_CLS_NONE) {
doc["stat_cla"] = stateClass_name[state_class];;
}
if (category != CATEGORY_NONE) {
doc["ent_cat"] = category_name[category];
}
}
void MqttHandleHassClass::publishBinarySensor(
JsonDocument& doc,
const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& payload_on, const String& payload_off,
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category)
{
String sensor_id = name;
sensor_id.toLowerCase();
sensor_id.replace(" ", "_");
doc["name"] = name;
doc["uniq_id"] = unique_id_prefix + "_" + sensor_id;
doc["stat_t"] = MqttSettings.getPrefix() + state_topic;
doc["pl_on"] = payload_on;
doc["pl_off"] = payload_off;
addCommonMetadata(doc, "", "", device_class, state_class, category);
const String configTopic = "binary_sensor/" + root_device + "/" + sensor_id + "/config";
publish(configTopic, doc);
}
void MqttHandleHassClass::publishDtuBinarySensor(
const String& name, const String& state_topic, const String& payload_on, const String& payload_off,
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category)
{
const String dtuId = getDtuUniqueId();
JsonDocument root;
createDtuInfo(root);
publishBinarySensor(root, dtuId, dtuId, name, state_topic, payload_on, payload_off, device_class, state_class, category);
}
void MqttHandleHassClass::publishInverterBinarySensor(
std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& payload_on, const String& payload_off,
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category)
{
const String serial = inv->serialString();
JsonDocument root;
createInverterInfo(root, inv);
publishBinarySensor(root, "dtu_" + serial, serial, name, serial + "/" + state_topic, payload_on, payload_off, device_class, state_class, category);
}
void MqttHandleHassClass::publishSensor(
JsonDocument& doc,
const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic,
const String& unit_of_measure, const String& icon,
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category)
{
String sensor_id = name;
sensor_id.toLowerCase();
sensor_id.replace(" ", "_");
doc["name"] = name;
doc["uniq_id"] = unique_id_prefix + "_" + sensor_id;
doc["stat_t"] = MqttSettings.getPrefix() + state_topic;
addCommonMetadata(doc, unit_of_measure, icon, device_class, state_class, category);
const CONFIG_T& config = Configuration.get();
doc["avty_t"] = MqttSettings.getPrefix() + config.Mqtt.Lwt.Topic;
doc["pl_avail"] = config.Mqtt.Lwt.Value_Online;
doc["pl_not_avail"] = config.Mqtt.Lwt.Value_Offline;
const String configTopic = "sensor/" + root_device + "/" + sensor_id + "/config";
publish(configTopic, doc);
}
void MqttHandleHassClass::publishDtuSensor(
const String& name, const String& state_topic,
const String& unit_of_measure, const String& icon,
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category)
{
const String dtuId = getDtuUniqueId();
JsonDocument root;
createDtuInfo(root);
publishSensor(root, dtuId, dtuId, name, state_topic, unit_of_measure, icon, device_class, state_class, category);
}
void MqttHandleHassClass::publishInverterSensor(
std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic,
const String& unit_of_measure, const String& icon,
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category)
{
const String serial = inv->serialString();
JsonDocument root;
createInverterInfo(root, inv);
publishSensor(root, "dtu_" + serial, serial, name, serial + "/" + state_topic, unit_of_measure, icon, device_class, state_class, category);
} }

View File

@ -7,13 +7,6 @@
#include "MqttSettings.h" #include "MqttSettings.h"
#include <ctime> #include <ctime>
#define TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE "limit_persistent_relative"
#define TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE "limit_persistent_absolute"
#define TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE "limit_nonpersistent_relative"
#define TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE "limit_nonpersistent_absolute"
#define TOPIC_SUB_POWER "power"
#define TOPIC_SUB_RESTART "restart"
#define PUBLISH_MAX_INTERVAL 60000 #define PUBLISH_MAX_INTERVAL 60000
MqttHandleInverterClass MqttHandleInverter; MqttHandleInverterClass MqttHandleInverter;
@ -50,6 +43,15 @@ void MqttHandleInverterClass::loop()
// Name // Name
MqttSettings.publish(subtopic + "/name", inv->name()); MqttSettings.publish(subtopic + "/name", inv->name());
// Radio Statistics
MqttSettings.publish(subtopic + "/radio/tx_request", String(inv->RadioStats.TxRequestData));
MqttSettings.publish(subtopic + "/radio/tx_re_request", String(inv->RadioStats.TxReRequestFragment));
MqttSettings.publish(subtopic + "/radio/rx_success", String(inv->RadioStats.RxSuccess));
MqttSettings.publish(subtopic + "/radio/rx_fail_nothing", String(inv->RadioStats.RxFailNoAnswer));
MqttSettings.publish(subtopic + "/radio/rx_fail_partial", String(inv->RadioStats.RxFailPartialAnswer));
MqttSettings.publish(subtopic + "/radio/rx_fail_corrupt", String(inv->RadioStats.RxFailCorruptData));
MqttSettings.publish(subtopic + "/radio/rssi", String(inv->getLastRssi()));
if (inv->DevInfo()->getLastUpdate() > 0) { if (inv->DevInfo()->getLastUpdate() > 0) {
// Bootloader Version // Bootloader Version
MqttSettings.publish(subtopic + "/device/bootloaderversion", String(inv->DevInfo()->getFwBootloaderVersion())); MqttSettings.publish(subtopic + "/device/bootloaderversion", String(inv->DevInfo()->getFwBootloaderVersion()));
@ -146,7 +148,7 @@ String MqttHandleInverterClass::getTopic(std::shared_ptr<InverterAbstract> inv,
return inv->serialString() + "/" + chanNum + "/" + chanName; return inv->serialString() + "/" + chanNum + "/" + chanName;
} }
void MqttHandleInverterClass::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 MqttHandleInverterClass::onMqttMessage(Topic t, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total)
{ {
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
@ -154,15 +156,11 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro
strncpy(token_topic, topic, MQTT_MAX_TOPIC_STRLEN + 40); // convert const char* to char* strncpy(token_topic, topic, MQTT_MAX_TOPIC_STRLEN + 40); // convert const char* to char*
char* serial_str; char* serial_str;
char* subtopic;
char* setting;
char* rest = &token_topic[strlen(config.Mqtt.Topic)]; char* rest = &token_topic[strlen(config.Mqtt.Topic)];
serial_str = strtok_r(rest, "/", &rest); serial_str = strtok_r(rest, "/", &rest);
subtopic = strtok_r(rest, "/", &rest);
setting = strtok_r(rest, "/", &rest);
if (serial_str == NULL || subtopic == NULL || setting == NULL) { if (serial_str == NULL) {
return; return;
} }
@ -175,33 +173,30 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro
return; return;
} }
// check if subtopic is unequal cmd std::string strValue(reinterpret_cast<const char*>(payload), len);
if (strcmp(subtopic, "cmd")) { float payload_val = -1;
try {
payload_val = std::stof(strValue);
} catch (std::invalid_argument const& e) {
MessageOutput.printf("MQTT handler: cannot parse payload of topic '%s' as float: %s\r\n",
topic, strValue.c_str());
return; return;
} }
char* strlimit = new char[len + 1]; switch (t) {
memcpy(strlimit, payload, len); case Topic::LimitPersistentRelative:
strlimit[len] = '\0';
const float payload_val = strtof(strlimit, NULL);
delete[] strlimit;
if (payload_val < 0) {
MessageOutput.printf("MQTT payload < 0 received --> ignoring\r\n");
return;
}
if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)) {
// Set inverter limit relative persistent // Set inverter limit relative persistent
MessageOutput.printf("Limit Persistent: %.1f %%\r\n", payload_val); MessageOutput.printf("Limit Persistent: %.1f %%\r\n", payload_val);
inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::RelativPersistent); inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::RelativPersistent);
break;
} else if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE)) { case Topic::LimitPersistentAbsolute:
// Set inverter limit absolute persistent // Set inverter limit absolute persistent
MessageOutput.printf("Limit Persistent: %.1f W\r\n", payload_val); MessageOutput.printf("Limit Persistent: %.1f W\r\n", payload_val);
inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::AbsolutPersistent); inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::AbsolutPersistent);
break;
} else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE)) { case Topic::LimitNonPersistentRelative:
// Set inverter limit relative non persistent // Set inverter limit relative non persistent
MessageOutput.printf("Limit Non-Persistent: %.1f %%\r\n", payload_val); MessageOutput.printf("Limit Non-Persistent: %.1f %%\r\n", payload_val);
if (!properties.retain) { if (!properties.retain) {
@ -209,8 +204,9 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro
} else { } else {
MessageOutput.println("Ignored because retained"); MessageOutput.println("Ignored because retained");
} }
break;
} else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE)) { case Topic::LimitNonPersistentAbsolute:
// Set inverter limit absolute non persistent // Set inverter limit absolute non persistent
MessageOutput.printf("Limit Non-Persistent: %.1f W\r\n", payload_val); MessageOutput.printf("Limit Non-Persistent: %.1f W\r\n", payload_val);
if (!properties.retain) { if (!properties.retain) {
@ -218,13 +214,15 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro
} else { } else {
MessageOutput.println("Ignored because retained"); MessageOutput.println("Ignored because retained");
} }
break;
} else if (!strcmp(setting, TOPIC_SUB_POWER)) { case Topic::Power:
// Turn inverter on or off // Turn inverter on or off
MessageOutput.printf("Set inverter power to: %d\r\n", static_cast<int32_t>(payload_val)); MessageOutput.printf("Set inverter power to: %" PRId32 "\r\n", static_cast<int32_t>(payload_val));
inv->sendPowerControlRequest(static_cast<int32_t>(payload_val) > 0); inv->sendPowerControlRequest(static_cast<int32_t>(payload_val) > 0);
break;
} else if (!strcmp(setting, TOPIC_SUB_RESTART)) { case Topic::Restart:
// Restart inverter // Restart inverter
MessageOutput.printf("Restart inverter\r\n"); MessageOutput.printf("Restart inverter\r\n");
if (!properties.retain && payload_val == 1) { if (!properties.retain && payload_val == 1) {
@ -232,34 +230,41 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro
} else { } else {
MessageOutput.println("Ignored because retained or numeric value not '1'"); MessageOutput.println("Ignored because retained or numeric value not '1'");
} }
break;
case Topic::ResetRfStats:
// Reset RF Stats
MessageOutput.printf("Reset RF stats\r\n");
if (!properties.retain && payload_val == 1) {
inv->resetRadioStats();
} else {
MessageOutput.println("Ignored because retained or numeric value not '1'");
}
} }
} }
void MqttHandleInverterClass::subscribeTopics() void MqttHandleInverterClass::subscribeTopics()
{ {
using std::placeholders::_1; String const& prefix = MqttSettings.getPrefix();
using std::placeholders::_2;
using std::placeholders::_3;
using std::placeholders::_4;
using std::placeholders::_5;
using std::placeholders::_6;
const String topic = MqttSettings.getPrefix(); auto subscribe = [&prefix, this](char const* subTopic, Topic t) {
MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); String fullTopic(prefix + _cmdtopic.data() + subTopic);
MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); MqttSettings.subscribe(fullTopic.c_str(), 0,
MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); std::bind(&MqttHandleInverterClass::onMqttMessage, this, t,
MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); std::placeholders::_1, std::placeholders::_2,
MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_POWER), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); std::placeholders::_3, std::placeholders::_4,
MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_RESTART), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); std::placeholders::_5, std::placeholders::_6));
};
for (auto const& s : _subscriptions) {
subscribe(s.first.data(), s.second);
}
} }
void MqttHandleInverterClass::unsubscribeTopics() void MqttHandleInverterClass::unsubscribeTopics()
{ {
const String topic = MqttSettings.getPrefix(); String const& prefix = MqttSettings.getPrefix() + _cmdtopic.data();
MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)); for (auto const& s : _subscriptions) {
MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE)); MqttSettings.unsubscribe(prefix + s.first.data());
MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE)); }
MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE));
MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_POWER));
MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_RESTART));
} }

View File

@ -91,8 +91,7 @@ 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: "); MessageOutput.printf("Received MQTT message on topic: %s\r\n", topic);
MessageOutput.println(topic);
_mqttSubscribeParser.handle_message(properties, topic, payload, len, index, total); _mqttSubscribeParser.handle_message(properties, topic, payload, len, index, total);
} }
@ -180,7 +179,7 @@ String MqttSettingsClass::getPrefix() const
return Configuration.get().Mqtt.Topic; return Configuration.get().Mqtt.Topic;
} }
String MqttSettingsClass::getClientId() String MqttSettingsClass::getClientId() const
{ {
String clientId = Configuration.get().Mqtt.ClientId; String clientId = Configuration.get().Mqtt.ClientId;
if (clientId == "") { if (clientId == "") {

View File

@ -7,10 +7,10 @@
#include "MessageOutput.h" #include "MessageOutput.h"
#include "PinMapping.h" #include "PinMapping.h"
#include "Utils.h" #include "Utils.h"
#include "__compiled_constants.h"
#include "defaults.h" #include "defaults.h"
#include <ESPmDNS.h> #include <ESPmDNS.h>
#include <ETH.h> #include <ETH.h>
#include "__compiled_constants.h"
NetworkSettingsClass::NetworkSettingsClass() NetworkSettingsClass::NetworkSettingsClass()
: _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&NetworkSettingsClass::loop, this)) : _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&NetworkSettingsClass::loop, this))
@ -31,6 +31,26 @@ void NetworkSettingsClass::init(Scheduler& scheduler)
WiFi.disconnect(true, true); WiFi.disconnect(true, true);
WiFi.onEvent(std::bind(&NetworkSettingsClass::NetworkEvent, this, _1, _2)); WiFi.onEvent(std::bind(&NetworkSettingsClass::NetworkEvent, this, _1, _2));
if (PinMapping.isValidW5500Config()) {
PinMapping_t& pin = PinMapping.get();
_w5500 = W5500::setup(pin.w5500_mosi, pin.w5500_miso, pin.w5500_sclk, pin.w5500_cs, pin.w5500_int, pin.w5500_rst);
if (_w5500)
MessageOutput.println("W5500: Connection successful");
else
MessageOutput.println("W5500: Connection error!!");
}
#if CONFIG_ETH_USE_ESP32_EMAC
else if (PinMapping.isValidEthConfig()) {
PinMapping_t& pin = PinMapping.get();
#if ESP_ARDUINO_VERSION_MAJOR < 3
ETH.begin(pin.eth_phy_addr, pin.eth_power, pin.eth_mdc, pin.eth_mdio, pin.eth_type, pin.eth_clk_mode);
#else
ETH.begin(pin.eth_type, pin.eth_phy_addr, pin.eth_mdc, pin.eth_mdio, pin.eth_power, pin.eth_clk_mode);
#endif
}
#endif
setupMode(); setupMode();
scheduler.addTask(_loopTask); scheduler.addTask(_loopTask);
@ -78,7 +98,7 @@ void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event, WiFiEventInfo_t
break; break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
// Reason codes can be found here: https://github.com/espressif/esp-idf/blob/5454d37d496a8c58542eb450467471404c606501/components/esp_wifi/include/esp_wifi_types_generic.h#L79-L141 // Reason codes can be found here: https://github.com/espressif/esp-idf/blob/5454d37d496a8c58542eb450467471404c606501/components/esp_wifi/include/esp_wifi_types_generic.h#L79-L141
MessageOutput.printf("WiFi disconnected: %d\r\n", info.wifi_sta_disconnected.reason); MessageOutput.printf("WiFi disconnected: %" PRIu8 "\r\n", info.wifi_sta_disconnected.reason);
if (_networkMode == network_mode::WiFi) { if (_networkMode == network_mode::WiFi) {
MessageOutput.println("Try reconnecting"); MessageOutput.println("Try reconnecting");
WiFi.disconnect(true, false); WiFi.disconnect(true, false);
@ -97,12 +117,12 @@ void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event, WiFiEventInfo_t
} }
} }
bool NetworkSettingsClass::onEvent(NetworkEventCb cbEvent, const network_event event) bool NetworkSettingsClass::onEvent(DtuNetworkEventCb cbEvent, const network_event event)
{ {
if (!cbEvent) { if (!cbEvent) {
return pdFALSE; return pdFALSE;
} }
NetworkEventCbList_t newEventHandler; DtuNetworkEventCbList_t newEventHandler;
newEventHandler.cb = cbEvent; newEventHandler.cb = cbEvent;
newEventHandler.event = event; newEventHandler.event = event;
_cbEventList.push_back(newEventHandler); _cbEventList.push_back(newEventHandler);
@ -111,8 +131,7 @@ bool NetworkSettingsClass::onEvent(NetworkEventCb cbEvent, const network_event e
void NetworkSettingsClass::raiseEvent(const network_event event) void NetworkSettingsClass::raiseEvent(const network_event event)
{ {
for (uint32_t i = 0; i < _cbEventList.size(); i++) { for (auto& entry : _cbEventList) {
const NetworkEventCbList_t entry = _cbEventList[i];
if (entry.cb) { if (entry.cb) {
if (entry.event == event || entry.event == network_event::NETWORK_EVENT_MAX) { if (entry.event == event || entry.event == network_event::NETWORK_EVENT_MAX) {
entry.cb(event); entry.cb(event);
@ -169,11 +188,6 @@ void NetworkSettingsClass::setupMode()
WiFi.mode(WIFI_MODE_NULL); WiFi.mode(WIFI_MODE_NULL);
} }
} }
if (PinMapping.isValidEthConfig()) {
PinMapping_t& pin = PinMapping.get();
ETH.begin(pin.eth_phy_addr, pin.eth_power, pin.eth_mdc, pin.eth_mdio, pin.eth_type, pin.eth_clk_mode);
}
} }
void NetworkSettingsClass::enableAdminMode() void NetworkSettingsClass::enableAdminMode()
@ -212,7 +226,7 @@ void NetworkSettingsClass::loop()
if (_adminEnabled && _adminTimeoutCounterMax > 0) { if (_adminEnabled && _adminTimeoutCounterMax > 0) {
_adminTimeoutCounter++; _adminTimeoutCounter++;
if (_adminTimeoutCounter % 10 == 0) { if (_adminTimeoutCounter % 10 == 0) {
MessageOutput.printf("Admin AP remaining seconds: %d / %d\r\n", _adminTimeoutCounter, _adminTimeoutCounterMax); MessageOutput.printf("Admin AP remaining seconds: %" PRId32 " / %" PRId32 "\r\n", _adminTimeoutCounter, _adminTimeoutCounterMax);
} }
} }
_connectTimeoutTimer++; _connectTimeoutTimer++;
@ -273,8 +287,7 @@ void NetworkSettingsClass::applyConfig()
MessageOutput.print("new credentials... "); MessageOutput.print("new credentials... ");
WiFi.begin( WiFi.begin(
Configuration.get().WiFi.Ssid, Configuration.get().WiFi.Ssid,
Configuration.get().WiFi.Password, Configuration.get().WiFi.Password);
WIFI_ALL_CHANNEL_SCAN);
} else { } else {
MessageOutput.print("existing credentials... "); MessageOutput.print("existing credentials... ");
WiFi.begin(); WiFi.begin();
@ -401,6 +414,9 @@ String NetworkSettingsClass::macAddress() const
{ {
switch (_networkMode) { switch (_networkMode) {
case network_mode::Ethernet: case network_mode::Ethernet:
if (_w5500) {
return _w5500->macAddress();
}
return ETH.macAddress(); return ETH.macAddress();
break; break;
case network_mode::WiFi: case network_mode::WiFi:

View File

@ -4,6 +4,7 @@
*/ */
#include "PinMapping.h" #include "PinMapping.h"
#include "MessageOutput.h" #include "MessageOutput.h"
#include "Utils.h"
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <LittleFS.h> #include <LittleFS.h>
#include <string.h> #include <string.h>
@ -84,6 +85,58 @@
#define CMT_SDIO -1 #define CMT_SDIO -1
#endif #endif
#ifndef W5500_MOSI
#define W5500_MOSI -1
#endif
#ifndef W5500_MISO
#define W5500_MISO -1
#endif
#ifndef W5500_SCLK
#define W5500_SCLK -1
#endif
#ifndef W5500_CS
#define W5500_CS -1
#endif
#ifndef W5500_INT
#define W5500_INT -1
#endif
#ifndef W5500_RST
#define W5500_RST -1
#endif
#if CONFIG_ETH_USE_ESP32_EMAC
#ifndef ETH_PHY_ADDR
#define ETH_PHY_ADDR -1
#endif
#ifndef ETH_PHY_POWER
#define ETH_PHY_POWER -1
#endif
#ifndef ETH_PHY_MDC
#define ETH_PHY_MDC -1
#endif
#ifndef ETH_PHY_MDIO
#define ETH_PHY_MDIO -1
#endif
#ifndef ETH_PHY_TYPE
#define ETH_PHY_TYPE ETH_PHY_LAN8720
#endif
#ifndef ETH_CLK_MODE
#define ETH_CLK_MODE ETH_CLOCK_GPIO0_IN
#endif
#endif
PinMappingClass PinMapping; PinMappingClass PinMapping;
PinMappingClass::PinMappingClass() PinMappingClass::PinMappingClass()
@ -103,18 +156,26 @@ PinMappingClass::PinMappingClass()
_pinMapping.cmt_gpio3 = CMT_GPIO3; _pinMapping.cmt_gpio3 = CMT_GPIO3;
_pinMapping.cmt_sdio = CMT_SDIO; _pinMapping.cmt_sdio = CMT_SDIO;
_pinMapping.w5500_mosi = W5500_MOSI;
_pinMapping.w5500_miso = W5500_MISO;
_pinMapping.w5500_sclk = W5500_SCLK;
_pinMapping.w5500_cs = W5500_CS;
_pinMapping.w5500_int = W5500_INT;
_pinMapping.w5500_rst = W5500_RST;
#if CONFIG_ETH_USE_ESP32_EMAC
#ifdef OPENDTU_ETHERNET #ifdef OPENDTU_ETHERNET
_pinMapping.eth_enabled = true; _pinMapping.eth_enabled = true;
#else #else
_pinMapping.eth_enabled = false; _pinMapping.eth_enabled = false;
#endif #endif
_pinMapping.eth_phy_addr = ETH_PHY_ADDR; _pinMapping.eth_phy_addr = ETH_PHY_ADDR;
_pinMapping.eth_power = ETH_PHY_POWER; _pinMapping.eth_power = ETH_PHY_POWER;
_pinMapping.eth_mdc = ETH_PHY_MDC; _pinMapping.eth_mdc = ETH_PHY_MDC;
_pinMapping.eth_mdio = ETH_PHY_MDIO; _pinMapping.eth_mdio = ETH_PHY_MDIO;
_pinMapping.eth_type = ETH_PHY_TYPE; _pinMapping.eth_type = ETH_PHY_TYPE;
_pinMapping.eth_clk_mode = ETH_CLK_MODE; _pinMapping.eth_clk_mode = ETH_CLK_MODE;
#endif
_pinMapping.display_type = DISPLAY_TYPE; _pinMapping.display_type = DISPLAY_TYPE;
_pinMapping.display_data = DISPLAY_DATA; _pinMapping.display_data = DISPLAY_DATA;
@ -139,6 +200,8 @@ bool PinMappingClass::init(const String& deviceMapping)
return false; return false;
} }
Utils::skipBom(f);
JsonDocument doc; JsonDocument doc;
// Deserialize the JSON document // Deserialize the JSON document
DeserializationError error = deserializeJson(doc, f); DeserializationError error = deserializeJson(doc, f);
@ -149,6 +212,8 @@ bool PinMappingClass::init(const String& deviceMapping)
for (uint8_t i = 0; i < doc.size(); i++) { for (uint8_t i = 0; i < doc.size(); i++) {
String devName = doc[i]["name"] | ""; String devName = doc[i]["name"] | "";
if (devName == deviceMapping) { if (devName == deviceMapping) {
_mappingSelected = true;
strlcpy(_pinMapping.name, devName.c_str(), sizeof(_pinMapping.name)); strlcpy(_pinMapping.name, devName.c_str(), sizeof(_pinMapping.name));
_pinMapping.nrf24_clk = doc[i]["nrf24"]["clk"] | HOYMILES_PIN_SCLK; _pinMapping.nrf24_clk = doc[i]["nrf24"]["clk"] | HOYMILES_PIN_SCLK;
_pinMapping.nrf24_cs = doc[i]["nrf24"]["cs"] | HOYMILES_PIN_CS; _pinMapping.nrf24_cs = doc[i]["nrf24"]["cs"] | HOYMILES_PIN_CS;
@ -164,18 +229,26 @@ bool PinMappingClass::init(const String& deviceMapping)
_pinMapping.cmt_gpio3 = doc[i]["cmt"]["gpio3"] | CMT_GPIO3; _pinMapping.cmt_gpio3 = doc[i]["cmt"]["gpio3"] | CMT_GPIO3;
_pinMapping.cmt_sdio = doc[i]["cmt"]["sdio"] | CMT_SDIO; _pinMapping.cmt_sdio = doc[i]["cmt"]["sdio"] | CMT_SDIO;
_pinMapping.w5500_mosi = doc[i]["w5500"]["mosi"] | W5500_MOSI;
_pinMapping.w5500_miso = doc[i]["w5500"]["miso"] | W5500_MISO;
_pinMapping.w5500_sclk = doc[i]["w5500"]["sclk"] | W5500_SCLK;
_pinMapping.w5500_cs = doc[i]["w5500"]["cs"] | W5500_CS;
_pinMapping.w5500_int = doc[i]["w5500"]["int"] | W5500_INT;
_pinMapping.w5500_rst = doc[i]["w5500"]["rst"] | W5500_RST;
#if CONFIG_ETH_USE_ESP32_EMAC
#ifdef OPENDTU_ETHERNET #ifdef OPENDTU_ETHERNET
_pinMapping.eth_enabled = doc[i]["eth"]["enabled"] | true; _pinMapping.eth_enabled = doc[i]["eth"]["enabled"] | true;
#else #else
_pinMapping.eth_enabled = doc[i]["eth"]["enabled"] | false; _pinMapping.eth_enabled = doc[i]["eth"]["enabled"] | false;
#endif #endif
_pinMapping.eth_phy_addr = doc[i]["eth"]["phy_addr"] | ETH_PHY_ADDR; _pinMapping.eth_phy_addr = doc[i]["eth"]["phy_addr"] | ETH_PHY_ADDR;
_pinMapping.eth_power = doc[i]["eth"]["power"] | ETH_PHY_POWER; _pinMapping.eth_power = doc[i]["eth"]["power"] | ETH_PHY_POWER;
_pinMapping.eth_mdc = doc[i]["eth"]["mdc"] | ETH_PHY_MDC; _pinMapping.eth_mdc = doc[i]["eth"]["mdc"] | ETH_PHY_MDC;
_pinMapping.eth_mdio = doc[i]["eth"]["mdio"] | ETH_PHY_MDIO; _pinMapping.eth_mdio = doc[i]["eth"]["mdio"] | ETH_PHY_MDIO;
_pinMapping.eth_type = doc[i]["eth"]["type"] | ETH_PHY_TYPE; _pinMapping.eth_type = doc[i]["eth"]["type"] | ETH_PHY_TYPE;
_pinMapping.eth_clk_mode = doc[i]["eth"]["clk_mode"] | ETH_CLK_MODE; _pinMapping.eth_clk_mode = doc[i]["eth"]["clk_mode"] | ETH_CLK_MODE;
#endif
_pinMapping.display_type = doc[i]["display"]["type"] | DISPLAY_TYPE; _pinMapping.display_type = doc[i]["display"]["type"] | DISPLAY_TYPE;
_pinMapping.display_data = doc[i]["display"]["data"] | DISPLAY_DATA; _pinMapping.display_data = doc[i]["display"]["data"] | DISPLAY_DATA;
@ -211,7 +284,21 @@ bool PinMappingClass::isValidCmt2300Config() const
&& _pinMapping.cmt_sdio >= 0; && _pinMapping.cmt_sdio >= 0;
} }
bool PinMappingClass::isValidW5500Config() const
{
return _pinMapping.w5500_mosi >= 0
&& _pinMapping.w5500_miso >= 0
&& _pinMapping.w5500_sclk >= 0
&& _pinMapping.w5500_cs >= 0
&& _pinMapping.w5500_int >= 0
&& _pinMapping.w5500_rst >= 0;
}
#if CONFIG_ETH_USE_ESP32_EMAC
bool PinMappingClass::isValidEthConfig() const bool PinMappingClass::isValidEthConfig() const
{ {
return _pinMapping.eth_enabled; return _pinMapping.eth_enabled
&& _pinMapping.eth_mdc >= 0
&& _pinMapping.eth_mdio >= 0;
} }
#endif

36
src/RestartHelper.cpp Normal file
View File

@ -0,0 +1,36 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2024 Thomas Basler and others
*/
#include "RestartHelper.h"
#include "Display_Graphic.h"
#include "Led_Single.h"
#include <Esp.h>
RestartHelperClass RestartHelper;
RestartHelperClass::RestartHelperClass()
: _rebootTask(1 * TASK_SECOND, TASK_FOREVER, std::bind(&RestartHelperClass::loop, this))
{
}
void RestartHelperClass::init(Scheduler& scheduler)
{
scheduler.addTask(_rebootTask);
}
void RestartHelperClass::triggerRestart()
{
_rebootTask.enable();
_rebootTask.restart();
}
void RestartHelperClass::loop()
{
if (_rebootTask.isFirstIteration()) {
LedSingle.turnAllOff();
Display.setStatus(false);
} else {
ESP.restart();
}
}

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