Cost-aware home EV charging companion for Renault EVs, MyEnergi Zappi2, and the Pstryk.pl dynamic tariff.
grosz glues together three things that don't natively talk to each other:
- Pstryk.pl: a Polish dynamic-tariff provider. grosz pulls hour-by-hour electricity prices for today and tomorrow.
- MyEnergi Zappi2: your home EV charger. grosz drives it as an OCPP 1.6J central system, setting the charging profile, starting and stopping sessions, and reading meter values.
- Renault EV (MyRenault / Kamereon): optional. grosz polls your car's State of Charge, range, and plug status so it knows when (and how much) to charge.
Once an hour grosz takes all of that and schedules charging into the cheapest hourly slots that will still hit your target SoC by your deadline. It then pushes a TxDefaultProfile to the Zappi over OCPP. The schedule shows up overlaid on the price chart, you can override it with a forced-charge window, and every decision is logged so you can later check why it did or didn't charge.
grosz exposes two listeners, so you need a public DNS name for each:
| Vhost | Default port | Protocol | Who connects |
|---|---|---|---|
grosz.example.com |
:3000 |
HTTPS + SSE | You (browser) |
ocpp.example.com |
:8887 |
WebSocket (OCPP 1.6J) | MyEnergi cloud, on behalf of your Zappi |
Both must be reachable from the public internet. The Zappi does not connect to your LAN. Instead it hands its OCPP backend URI off to MyEnergi's cloud, and MyEnergi opens the WebSocket to that URI from their own datacentre. A LAN-only or VPN-only endpoint will never get a connection. From MyEnergi support:
The OCPP service is hosted in the cloud and is accessible over the internet. For security reasons, it is not possible to set an internal IP address as the backend URI for a locally hosted OCPP platform. Customers who wish to use OCPP with a platform hosted within their internal network must provide an externally facing IP address.
Splitting the UI and the OCPP endpoint onto separate hostnames keeps the protocols clean (HTTP/SSE on one, long-lived WebSocket on the other), lets you firewall the OCPP vhost tightly (its only legitimate client is MyEnergi's egress), and makes certbot easy.
- Cheapest-hour scheduling against the Pstryk dynamic tariff, refreshed each hour
- Forced-charge windows that bypass the optimiser when you need to leave early
- Live SoC, range, and plug status from MyRenault / Kamereon
- Zappi quirks handled for you: commercial-mode reset, single
TxDefaultProfile, virtual ID tags, meter-interval setup - Per-session cost reporting and historical session history
- Live OCPP and system event logs in the UI
- Optional WebAuthn login on top of username and password
- Single static binary, embedded React UI, SQLite persistence, no external runtime
Requirements: Go 1.22+, Node 20+, npm.
make build # builds the React UI and the Go binary
./grosz # runs on :3000 (Web UI) and :8887 (OCPP)For hot-reload during development:
make dev # runs Go server + Vite dev server side by sideOpen http://localhost:3000 and log in with the default admin / admin. Configure tariff, charger, and (optional) Renault credentials in Settings. All runtime configuration lives in SQLite, so there is no env file to maintain.
Tested on Debian 13. Assumes nginx and grosz live on the same host. If they don't, replace 127.0.0.1 in the upstreams with the grosz host's IP and firewall accordingly. Replace example.com with your own domain throughout.
Point two A/AAAA records at the public IP of your server:
grosz.example.comfor the Web UIocpp.example.comfor the OCPP endpoint MyEnergi will reach
Both must resolve publicly.
Grab the latest release from the GitHub Releases page:
curl -LO https://github.com/consi/grosz/releases/latest/download/grosz_<version>_linux_amd64.deb
sudo dpkg -i grosz_<version>_linux_amd64.debThe package:
- Installs the binary to
/usr/bin/grosz - Creates a
groszsystem user - Stores SQLite at
/var/lib/grosz/grosz.db - Drops a systemd unit at
/lib/systemd/system/grosz.serviceand starts it
Verify:
sudo systemctl status grosz
curl -I http://127.0.0.1:3000Install nginx, then drop these three files in place.
/etc/nginx/conf.d/proxy-maps.conf, shared map blocks used by both vhosts:
map $remote_addr $proxy_forwarded_elem {
~^[0-9.]+$ "for=$remote_addr";
~^[0-9A-Fa-f:.]+$ "for=\"[$remote_addr]\"";
default "for=unknown";
}
map $http_forwarded $proxy_add_forwarded {
"~^(,[ \\t]*)*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*([ \\t]*,([ \\t]*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*)?)*$" "$http_forwarded, $proxy_forwarded_elem";
default "$proxy_forwarded_elem";
}
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}/etc/nginx/sites-available/grosz, the Web UI vhost:
upstream grosz_api {
server 127.0.0.1:3000;
keepalive 32;
}
server {
listen 80;
listen [::]:80;
server_name grosz.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name grosz.example.com;
# filled in by certbot in step 4
ssl_certificate /etc/letsencrypt/live/grosz.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/grosz.example.com/privkey.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 10m;
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 24h; # SSE streams
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Forwarded $proxy_add_forwarded;
location / {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://grosz_api;
}
}/etc/nginx/sites-available/grosz-ocpp, the OCPP vhost (long-lived WebSocket):
upstream grosz_ocpp {
server 127.0.0.1:8887;
keepalive 32;
}
server {
listen 80;
listen [::]:80;
server_name ocpp.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name ocpp.example.com;
ssl_certificate /etc/letsencrypt/live/ocpp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ocpp.example.com/privkey.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://grosz_ocpp;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Forwarded $proxy_add_forwarded;
proxy_read_timeout 604800s; # 7d, OCPP keeps this open
proxy_send_timeout 604800s;
proxy_connect_timeout 10s;
proxy_buffering off;
proxy_cache off;
}
}Enable and test:
sudo ln -s /etc/nginx/sites-available/grosz /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/grosz-ocpp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxsudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d grosz.example.com -d ocpp.example.comcertbot picks up the two server blocks above, drops the certs into /etc/letsencrypt/live/..., and rewrites the vhosts. Renewal runs automatically via the certbot.timer systemd unit.
In the myenergi app, open your Zappi → OCPP:
- Backend URI:
wss://ocpp.example.com/ - ChargePoint ID: your Zappi's serial number
- Authorisation Key: any value, but set the same one in the grosz UI under Settings → OCPP
The Zappi will connect (via MyEnergi's cloud) within a minute or two. You should see a BootNotification show up in OCPP Log in the grosz UI. See MyEnergi's OCPP setup guide for screenshots.
Visit https://grosz.example.com and log in with the default admin / admin. In Settings:
- Change the admin password (and optionally register a WebAuthn key)
- Enter your Pstryk.pl API token
- (Optional) Enter MyRenault credentials for SoC integration
- Set the OCPP authorisation key to match what you configured on the Zappi
- Set scheduler parameters: target SoC, deadline, maximum charge power, skip threshold
That's it. grosz pulls the next price window and schedules the cheapest hours automatically.
- OCPP 1.6J central system on
:8887, built onlorenzodonini/ocpp-go. The Zappi connects via the MyEnergi cloud OCPP proxy. - Web UI on
:3000. React + Vite SPA, embedded into the Go binary viago:embed. SSE for live updates. - Persistence: pure-Go SQLite (
modernc.org/sqlite, no CGO) for settings, OCPP events, sessions, tariff cache. - Tariff: Pstryk.pl REST API. Prices cached locally; the scheduler picks the cheapest hours that satisfy the charge target.
- Vehicle SoC (optional): MyRenault / Kamereon API, polled to drive charge-target awareness.
A pre-built container image is also published at ghcr.io/consi/grosz if you'd rather containerise.
lorenzodonini/ocpp-gofor the OCPP 1.6 protocol library.python-renault-apiand the broader Renault open-source community for documenting the Gigya/Kamereon authentication flow used byinternal/vehicle/renault.go.
grosz is source-available under the Elastic License 2.0, not OSI-approved open source. In short:
- ✅ Self-hosting, modification, and redistribution are permitted.
- ✅ Contributions are welcome and accepted under the same license.
- ❌ Providing grosz to third parties as a hosted or managed service that exposes a substantial set of its features is not permitted.
Full text and FAQ: https://www.elastic.co/licensing/elastic-license.
