{"id":51030593,"url":"https://github.com/consi/grosz","last_synced_at":"2026-06-22T00:01:39.383Z","repository":{"id":355595923,"uuid":"1228761213","full_name":"consi/grosz","owner":"consi","description":"Cost-aware home EV charging companion for Renault EVs, MyEnergi Zappi2, and the Pstryk.pl dynamic tariff","archived":false,"fork":false,"pushed_at":"2026-06-08T17:41:49.000Z","size":996,"stargazers_count":24,"open_issues_count":0,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-08T19:21:55.276Z","etag":null,"topics":["charging","charging-stations","ev","myenergi","renault","zappi"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/consi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-04T10:54:46.000Z","updated_at":"2026-06-08T17:41:53.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/consi/grosz","commit_stats":null,"previous_names":["consi/grosz"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/consi/grosz","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/consi%2Fgrosz","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/consi%2Fgrosz/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/consi%2Fgrosz/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/consi%2Fgrosz/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/consi","download_url":"https://codeload.github.com/consi/grosz/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/consi%2Fgrosz/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34629658,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-21T02:00:05.568Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["charging","charging-stations","ev","myenergi","renault","zappi"],"created_at":"2026-06-22T00:01:02.609Z","updated_at":"2026-06-22T00:01:39.242Z","avatar_url":"https://github.com/consi.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# grosz\n\n\u003e Cost-aware home EV charging companion for Renault EVs, MyEnergi Zappi2, and the Pstryk.pl dynamic tariff.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"screenshot.png\" alt=\"grosz dashboard\" width=\"800\"\u003e\n\u003c/p\u003e\n\n## What it does\n\ngrosz glues together three things that don't natively talk to each other:\n\n- **Pstryk.pl**: a Polish dynamic-tariff provider. grosz pulls hour-by-hour electricity prices for today and tomorrow.\n- **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.\n- **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.\n\nOnce 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.\n\n## Why two public vhosts\n\ngrosz exposes two listeners, so you need a public DNS name for each:\n\n| Vhost | Default port | Protocol | Who connects |\n|---|---|---|---|\n| `grosz.example.com` | `:3000` | HTTPS + SSE | You (browser) |\n| `ocpp.example.com` | `:8887` | WebSocket (OCPP 1.6J) | MyEnergi cloud, on behalf of your Zappi |\n\n**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](https://support.myenergi.com/hc/en-gb/articles/16864772981137-Setting-up-Open-Charge-Point-Protocol-OCPP):\n\n\u003e 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.\n\nSplitting 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.\n\n## Features\n\n- Cheapest-hour scheduling against the Pstryk dynamic tariff, refreshed each hour\n- Forced-charge windows that bypass the optimiser when you need to leave early\n- Live SoC, range, and plug status from MyRenault / Kamereon\n- Zappi quirks handled for you: commercial-mode reset, single `TxDefaultProfile`, virtual ID tags, meter-interval setup\n- Per-session cost reporting and historical session history\n- Live OCPP and system event logs in the UI\n- Optional WebAuthn login on top of username and password\n- Single static binary, embedded React UI, SQLite persistence, no external runtime\n\n## Quick start (local development)\n\nRequirements: Go 1.22+, Node 20+, npm.\n\n```bash\nmake build      # builds the React UI and the Go binary\n./grosz         # runs on :3000 (Web UI) and :8887 (OCPP)\n```\n\nFor hot-reload during development:\n\n```bash\nmake dev        # runs Go server + Vite dev server side by side\n```\n\nOpen \u003chttp://localhost:3000\u003e 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.\n\n## Deploying on Debian (production)\n\nTested 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.\n\n### 1. DNS\n\nPoint two A/AAAA records at the public IP of your server:\n\n- `grosz.example.com` for the Web UI\n- `ocpp.example.com` for the OCPP endpoint MyEnergi will reach\n\nBoth must resolve publicly.\n\n### 2. Install the .deb\n\nGrab the latest release from the GitHub Releases page:\n\n```bash\ncurl -LO https://github.com/consi/grosz/releases/latest/download/grosz_\u003cversion\u003e_linux_amd64.deb\nsudo dpkg -i grosz_\u003cversion\u003e_linux_amd64.deb\n```\n\nThe package:\n\n- Installs the binary to `/usr/bin/grosz`\n- Creates a `grosz` system user\n- Stores SQLite at `/var/lib/grosz/grosz.db`\n- Drops a systemd unit at `/lib/systemd/system/grosz.service` and starts it\n\nVerify:\n\n```bash\nsudo systemctl status grosz\ncurl -I http://127.0.0.1:3000\n```\n\n### 3. nginx vhosts\n\nInstall nginx, then drop these three files in place.\n\n`/etc/nginx/conf.d/proxy-maps.conf`, shared map blocks used by both vhosts:\n\n```nginx\nmap $remote_addr $proxy_forwarded_elem {\n    ~^[0-9.]+$        \"for=$remote_addr\";\n    ~^[0-9A-Fa-f:.]+$ \"for=\\\"[$remote_addr]\\\"\";\n    default            \"for=unknown\";\n}\n\nmap $http_forwarded $proxy_add_forwarded {\n    \"~^(,[ \\\\t]*)*([!#$%\u0026'*+.^_`|~0-9A-Za-z-]+=([!#$%\u0026'*+.^_`|~0-9A-Za-z-]+|\\\"([\\\\t \\\\x21\\\\x23-\\\\x5B\\\\x5D-\\\\x7E\\\\x80-\\\\xFF]|\\\\\\\\[\\\\t \\\\x21-\\\\x7E\\\\x80-\\\\xFF])*\\\"))?(;([!#$%\u0026'*+.^_`|~0-9A-Za-z-]+=([!#$%\u0026'*+.^_`|~0-9A-Za-z-]+|\\\"([\\\\t \\\\x21\\\\x23-\\\\x5B\\\\x5D-\\\\x7E\\\\x80-\\\\xFF]|\\\\\\\\[\\\\t \\\\x21-\\\\x7E\\\\x80-\\\\xFF])*\\\"))?)*([ \\\\t]*,([ \\\\t]*([!#$%\u0026'*+.^_`|~0-9A-Za-z-]+=([!#$%\u0026'*+.^_`|~0-9A-Za-z-]+|\\\"([\\\\t \\\\x21\\\\x23-\\\\x5B\\\\x5D-\\\\x7E\\\\x80-\\\\xFF]|\\\\\\\\[\\\\t \\\\x21-\\\\x7E\\\\x80-\\\\xFF])*\\\"))?(;([!#$%\u0026'*+.^_`|~0-9A-Za-z-]+=([!#$%\u0026'*+.^_`|~0-9A-Za-z-]+|\\\"([\\\\t \\\\x21\\\\x23-\\\\x5B\\\\x5D-\\\\x7E\\\\x80-\\\\xFF]|\\\\\\\\[\\\\t \\\\x21-\\\\x7E\\\\x80-\\\\xFF])*\\\"))?)*)?)*$\" \"$http_forwarded, $proxy_forwarded_elem\";\n    default \"$proxy_forwarded_elem\";\n}\n\nmap $http_upgrade $connection_upgrade {\n    default upgrade;\n    ''      close;\n}\n```\n\n`/etc/nginx/sites-available/grosz`, the Web UI vhost:\n\n```nginx\nupstream grosz_api {\n    server 127.0.0.1:3000;\n    keepalive 32;\n}\n\nserver {\n    listen 80;\n    listen [::]:80;\n    server_name grosz.example.com;\n    return 301 https://$host$request_uri;\n}\n\nserver {\n    listen 443 ssl;\n    listen [::]:443 ssl;\n    http2 on;\n    server_name grosz.example.com;\n\n    # filled in by certbot in step 4\n    ssl_certificate     /etc/letsencrypt/live/grosz.example.com/fullchain.pem;\n    ssl_certificate_key /etc/letsencrypt/live/grosz.example.com/privkey.pem;\n    ssl_session_timeout 1d;\n    ssl_session_cache   shared:SSL:10m;\n    ssl_session_tickets off;\n    ssl_protocols TLSv1.2 TLSv1.3;\n\n    client_max_body_size 10m;\n\n    proxy_http_version 1.1;\n    proxy_buffering    off;\n    proxy_cache        off;\n    proxy_read_timeout 24h;          # SSE streams\n    proxy_set_header Host              $host;\n    proxy_set_header X-Real-IP         $remote_addr;\n    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;\n    proxy_set_header X-Forwarded-Proto $scheme;\n    proxy_set_header Forwarded         $proxy_add_forwarded;\n\n    location / {\n        proxy_set_header Upgrade    $http_upgrade;\n        proxy_set_header Connection $connection_upgrade;\n        proxy_pass http://grosz_api;\n    }\n}\n```\n\n`/etc/nginx/sites-available/grosz-ocpp`, the OCPP vhost (long-lived WebSocket):\n\n```nginx\nupstream grosz_ocpp {\n    server 127.0.0.1:8887;\n    keepalive 32;\n}\n\nserver {\n    listen 80;\n    listen [::]:80;\n    server_name ocpp.example.com;\n    return 301 https://$host$request_uri;\n}\n\nserver {\n    listen 443 ssl;\n    listen [::]:443 ssl;\n    http2 on;\n    server_name ocpp.example.com;\n\n    ssl_certificate     /etc/letsencrypt/live/ocpp.example.com/fullchain.pem;\n    ssl_certificate_key /etc/letsencrypt/live/ocpp.example.com/privkey.pem;\n    ssl_session_timeout 1d;\n    ssl_session_cache   shared:SSL:10m;\n    ssl_session_tickets off;\n    ssl_protocols TLSv1.2 TLSv1.3;\n\n    location / {\n        proxy_pass http://grosz_ocpp;\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade           $http_upgrade;\n        proxy_set_header Connection        $connection_upgrade;\n        proxy_set_header Host              $host;\n        proxy_set_header X-Real-IP         $remote_addr;\n        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n        proxy_set_header Forwarded         $proxy_add_forwarded;\n        proxy_read_timeout    604800s;     # 7d, OCPP keeps this open\n        proxy_send_timeout    604800s;\n        proxy_connect_timeout 10s;\n        proxy_buffering off;\n        proxy_cache off;\n    }\n}\n```\n\nEnable and test:\n\n```bash\nsudo ln -s /etc/nginx/sites-available/grosz       /etc/nginx/sites-enabled/\nsudo ln -s /etc/nginx/sites-available/grosz-ocpp  /etc/nginx/sites-enabled/\nsudo nginx -t\nsudo systemctl reload nginx\n```\n\n### 4. TLS with Let's Encrypt\n\n```bash\nsudo apt install certbot python3-certbot-nginx\nsudo certbot --nginx -d grosz.example.com -d ocpp.example.com\n```\n\ncertbot 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.\n\n### 5. Point Zappi at your OCPP URL\n\nIn the myenergi app, open your Zappi → **OCPP**:\n\n- **Backend URI:** `wss://ocpp.example.com/`\n- **ChargePoint ID:** your Zappi's serial number\n- **Authorisation Key:** any value, but set the same one in the grosz UI under Settings → OCPP\n\nThe 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](https://support.myenergi.com/hc/en-gb/articles/16864772981137-Setting-up-Open-Charge-Point-Protocol-OCPP) for screenshots.\n\n### 6. First-run UI configuration\n\nVisit `https://grosz.example.com` and log in with the default `admin` / `admin`. In **Settings**:\n\n1. Change the admin password (and optionally register a WebAuthn key)\n2. Enter your Pstryk.pl API token\n3. (Optional) Enter MyRenault credentials for SoC integration\n4. Set the OCPP authorisation key to match what you configured on the Zappi\n5. Set scheduler parameters: target SoC, deadline, maximum charge power, skip threshold\n\nThat's it. grosz pulls the next price window and schedules the cheapest hours automatically.\n\n## Architecture\n\n- **OCPP 1.6J** central system on `:8887`, built on [`lorenzodonini/ocpp-go`](https://github.com/lorenzodonini/ocpp-go). The Zappi connects via the MyEnergi cloud OCPP proxy.\n- **Web UI** on `:3000`. React + Vite SPA, embedded into the Go binary via `go:embed`. SSE for live updates.\n- **Persistence**: pure-Go SQLite (`modernc.org/sqlite`, no CGO) for settings, OCPP events, sessions, tariff cache.\n- **Tariff**: Pstryk.pl REST API. Prices cached locally; the scheduler picks the cheapest hours that satisfy the charge target.\n- **Vehicle SoC (optional)**: MyRenault / Kamereon API, polled to drive charge-target awareness.\n\nA pre-built container image is also published at `ghcr.io/consi/grosz` if you'd rather containerise.\n\n## Acknowledgements\n\n- [`lorenzodonini/ocpp-go`](https://github.com/lorenzodonini/ocpp-go) for the OCPP 1.6 protocol library.\n- [`python-renault-api`](https://github.com/hacf-fr/renault-api) and the broader Renault open-source community for documenting the Gigya/Kamereon authentication flow used by `internal/vehicle/renault.go`.\n\n## License\n\ngrosz is **source-available** under the [Elastic License 2.0](LICENSE), not OSI-approved open source. In short:\n\n- ✅ Self-hosting, modification, and redistribution are permitted.\n- ✅ Contributions are welcome and accepted under the same license.\n- ❌ Providing grosz to third parties as a hosted or managed service that exposes a substantial set of its features is **not** permitted.\n\nFull text and FAQ: \u003chttps://www.elastic.co/licensing/elastic-license\u003e.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fconsi%2Fgrosz","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fconsi%2Fgrosz","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fconsi%2Fgrosz/lists"}