Many things were added/improved during the last few days:
- Settings added (EEPROM storage is used)
- Improved serving of static files
- Introduced an automated build process for WebApp (using Webpack)
- build-in dev server with hot reload
- production build
- minification of files
- file compression (gzip)
- inline SVG icons (to serve fewer files)
- Restructured WebApp
- Added caching
- Added notification about an update available (PWA specific, more details later)
- Possibility to install the app is added
- Improved UI/UX
Settings
Now we can configure:
- Limits: min/max voltage max power and current
- MQTT: server, port, user & password, topic
- System: OTA password and how often the module should request new data from the sensor
There is no validation on the server, so I wouldn't say that everything is super secure and production-ready, but it works. To simplify server-side code, all fields are combined into a single structure so in that way, we can read/write everything at once.
The only tricky part was receiving a JSON to later parse it using the ArduinoJson library.
server.on("/power/api/settings", HTTP_PUT, [](AsyncWebServerRequest *request){}, NULL, _saveSettings);
... ... ...
void _saveSettings(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
{
... ... ...
}
Depending on the body size, a request callback can be executed once or a few times. In my case, I had two executions of the callback, and getting a single JSON result requires additional logic (plus an intermediate buffer). The current code looks like this:
void _saveSettings(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
{
if (total > sizeof(dataBuffer))
{
request->send(500, CONTENT_TYPE_TEXT, "Content is to big");
return;
}
memcpy(dataBuffer + index, data, len);
if (len + index < total)
return;
// JSON parsing here
}
The dataBuffer defined in main.cpp. First of all, we check whether the total body size is bigger than our intermediate buffer if yes, we won't be able to receive data and have to report an error. Then we copy the data into the buffer starting from the particular index, and if we haven't received all data (len + index < total) stop execution. In that way, only the last executed callback will do real work (parsing and saving).
As you probably noticed, the current approach is not suitable for a multi-user scenario when two users simultaneously save settings. Maybe, I'll fix it in the future.
Static files and automated build process
Web server configuration was simplified a lot:
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->redirect("/power/index.html"); });
server.on("/power/", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->redirect("/power/index.html"); });
server.serveStatic("/power/", SPIFFS, "/");
server.serveStatic("/power/images", SPIFFS, "/images");
The two first directives redirect a user to index.html, next two serve all static files. Additionally, all static files are compressed by gzip (during the build) to use less space and speed up loading. All SVG icons used from CSS were embedded into the resulting CSS file to have fewer requests and simpler caching.
Playing with Webpack took me more time than the rest of things altogether. But it was interesting, and now it is easier to develop new changes.
Progressive web app
From the beginning, I wanted to have a progressive web application (PWA) that could be installed on the phone/desktop like a native app. I will not talk about how it works in detail because it is a broad topic, but more information can be found on web.dev/progressive-web-apps or any other resource.


The app uses the cache first strategy, so all files are loaded from the browser cache (even if the network is available). This approach has one pitfall: even when the app is updated on the device/server and the user opens it in a browser, it will see the cached version instead of the new one. To get a new version, the user has to:
- close the app and open it again
- manually refresh the page
The simplest way to handle such a case is to notify the user about using an outdated version. I don't think this functionality is truly needed for this particular app, but it was an excellent time to learn something new.

I've almost forgotten to add one more important note about PWA. To make it installable, the app should satisfy a few criteria:
- The web app is not already installed
- Meets a user engagement heuristic
- Be served over HTTPS
- Includes a web app manifest that includes:
- short_name or name
- icons - must include a 192px and a 512px icon
- start_url
- display - must be one of fullscreen, standalone, or minimal-ui
- prefer_related_applications must not be present, or be false
- Registers a service worker with a fetch handler
All points except 3rd one are implemented in the application. I don't want to add SSL/TLS support to the device, though it is theoretically possible. In my case, a standalone server with a public IP and dedicated domain is proxying requests to the module. It already has a valid SSL certificate and auto-update functionality using Let's Encrypt. By the way, this is the reason why I configured everything to be served from /power/ instead of the root /.
Next steps
As for now, almost everything planned has already been implemented. The only important thing left is resetting the energy counter at the beginning of the month. I also have a few additional ideas that might be implemented too:
- New property on the settings page to configure screen brightness
- Possibility to reset energy counter via the web UI
- Power/current chart for the last 24 hours (without persistent data storage, so it'll show nothing after the reboot)
I already have all charts in the Grafana, but adding something to the device itself looks interesting and relatively simple, so why not?

Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.
Hi there, after a busy months of project at work, I finally have the time to start with the similar project but unfortunately I am stucked and I wonder if you could help:
1. I managed to intergrate PZEM004T to ESPHOME via Ethernet
2. The following is the YAML:
substitutions:
name: esphome-web-7bfdf0
friendly_name: WT32-L1
esphome:
name: ${name}
friendly_name: ${friendly_name}
name_add_mac_suffix: false
platformio_options:
upload_speed: 115200
project:
name: esphome.web
version: '1.0'
esp32:
board: wt32-eth01
framework:
type: arduino
# Enable logging
logger:
# Enable Home Assistant API
api:
# Allow Over-The-Air updates
ota:
# Allow provisioning Wi-Fi via serial
#improv_serial:
#wifi:
# Set up a wifi access point
# ap: {}
# In combination with the `ap` this allows the user
# to provision wifi credentials to the device via WiFi AP.
#captive_portal:
dashboard_import:
package_import_url: github://esphome/example-configs/esphome-web/esp32.yaml@main
import_full_config: true
# Sets up Bluetooth LE (Only on ESP32) to allow the user
# to provision wifi credentials to the device.
#esp32_improv:
# authorizer: none
ethernet:
type: 'LAN8720'
mdc_pin: 'GPIO23'
mdio_pin: 'GPIO18'
clk_mode: 'GPIO0_IN'
phy_addr: 1
power_pin: 'GPIO16'
# To have a "next url" for improv serial
web_server:
uart:
rx_pin: 'GPIO5'
tx_pin: 'GPIO17'
baud_rate: 9600
sensor:
- platform: pzemac
current:
name: 'PZEM-004T V3 Current'
voltage:
name: 'PZEM-004T V3 Voltage'
energy:
name: 'PZEM-004T V3 Energy'
power:
name: 'PZEM-004T V3 Power'
frequency:
name: 'PZEM-004T V3 Frequency'
power_factor:
name: 'PZEM-004T V3 Power Factor'
update_interval: 10s
3. Unfortunately the dashboard show all values as "unknown" (I will try to find a way to upload a screenshot)
Could you please help?
Thank you in advance.
Kind regards
Ed
Are you sure? yes | no