[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-81763":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":12,"openIssues":14,"contributorsCount":14,"subscribersCount":14,"size":14,"stars1d":14,"stars7d":14,"stars30d":14,"stars90d":14,"forks30d":14,"starsTrendScore":14,"compositeScore":15,"rankGlobal":10,"rankLanguage":10,"license":16,"archived":17,"fork":17,"defaultBranch":18,"hasWiki":19,"hasPages":17,"topics":20,"createdAt":10,"pushedAt":10,"updatedAt":27,"readmeContent":28,"aiSummary":29,"trendingCount":14,"starSnapshotCount":14,"syncStatus":13,"lastSyncTime":30,"discoverSource":31},81763,"grosz","consi\u002Fgrosz","consi","Cost-aware home EV charging companion for Renault EVs, MyEnergi Zappi2, and the Pstryk.pl dynamic tariff","",null,"Go",24,2,0,41.43,"Other",false,"main",true,[21,22,23,24,25,26],"charging","charging-stations","ev","myenergi","renault","zappi","2026-06-12 04:01:35","# grosz\n\n> Cost-aware home EV charging companion for Renault EVs, MyEnergi Zappi2, and the Pstryk.pl dynamic tariff.\n\n\u003Cp align=\"center\">\n  \u003Cimg src=\"screenshot.png\" alt=\"grosz dashboard\" width=\"800\">\n\u003C\u002Fp>\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 \u002F 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:\u002F\u002Fsupport.myenergi.com\u002Fhc\u002Fen-gb\u002Farticles\u002F16864772981137-Setting-up-Open-Charge-Point-Protocol-OCPP):\n\n> 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\u002FSSE 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 \u002F 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.\u002Fgrosz         # 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:\u002F\u002Flocalhost:3000> and log in with the default `admin` \u002F `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\u002FAAAA 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:\u002F\u002Fgithub.com\u002Fconsi\u002Fgrosz\u002Freleases\u002Flatest\u002Fdownload\u002Fgrosz_\u003Cversion>_linux_amd64.deb\nsudo dpkg -i grosz_\u003Cversion>_linux_amd64.deb\n```\n\nThe package:\n\n- Installs the binary to `\u002Fusr\u002Fbin\u002Fgrosz`\n- Creates a `grosz` system user\n- Stores SQLite at `\u002Fvar\u002Flib\u002Fgrosz\u002Fgrosz.db`\n- Drops a systemd unit at `\u002Flib\u002Fsystemd\u002Fsystem\u002Fgrosz.service` and starts it\n\nVerify:\n\n```bash\nsudo systemctl status grosz\ncurl -I http:\u002F\u002F127.0.0.1:3000\n```\n\n### 3. nginx vhosts\n\nInstall nginx, then drop these three files in place.\n\n`\u002Fetc\u002Fnginx\u002Fconf.d\u002Fproxy-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]*)*([!#$%&'*+.^_`|~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\";\n    default \"$proxy_forwarded_elem\";\n}\n\nmap $http_upgrade $connection_upgrade {\n    default upgrade;\n    ''      close;\n}\n```\n\n`\u002Fetc\u002Fnginx\u002Fsites-available\u002Fgrosz`, 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:\u002F\u002F$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     \u002Fetc\u002Fletsencrypt\u002Flive\u002Fgrosz.example.com\u002Ffullchain.pem;\n    ssl_certificate_key \u002Fetc\u002Fletsencrypt\u002Flive\u002Fgrosz.example.com\u002Fprivkey.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 \u002F {\n        proxy_set_header Upgrade    $http_upgrade;\n        proxy_set_header Connection $connection_upgrade;\n        proxy_pass http:\u002F\u002Fgrosz_api;\n    }\n}\n```\n\n`\u002Fetc\u002Fnginx\u002Fsites-available\u002Fgrosz-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:\u002F\u002F$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     \u002Fetc\u002Fletsencrypt\u002Flive\u002Focpp.example.com\u002Ffullchain.pem;\n    ssl_certificate_key \u002Fetc\u002Fletsencrypt\u002Flive\u002Focpp.example.com\u002Fprivkey.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 \u002F {\n        proxy_pass http:\u002F\u002Fgrosz_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 \u002Fetc\u002Fnginx\u002Fsites-available\u002Fgrosz       \u002Fetc\u002Fnginx\u002Fsites-enabled\u002F\nsudo ln -s \u002Fetc\u002Fnginx\u002Fsites-available\u002Fgrosz-ocpp  \u002Fetc\u002Fnginx\u002Fsites-enabled\u002F\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 `\u002Fetc\u002Fletsencrypt\u002Flive\u002F...`, 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:\u002F\u002Focpp.example.com\u002F`\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:\u002F\u002Fsupport.myenergi.com\u002Fhc\u002Fen-gb\u002Farticles\u002F16864772981137-Setting-up-Open-Charge-Point-Protocol-OCPP) for screenshots.\n\n### 6. First-run UI configuration\n\nVisit `https:\u002F\u002Fgrosz.example.com` and log in with the default `admin` \u002F `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\u002Focpp-go`](https:\u002F\u002Fgithub.com\u002Florenzodonini\u002Focpp-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\u002Fsqlite`, 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 \u002F Kamereon API, polled to drive charge-target awareness.\n\nA pre-built container image is also published at `ghcr.io\u002Fconsi\u002Fgrosz` if you'd rather containerise.\n\n## Acknowledgements\n\n- [`lorenzodonini\u002Focpp-go`](https:\u002F\u002Fgithub.com\u002Florenzodonini\u002Focpp-go) for the OCPP 1.6 protocol library.\n- [`python-renault-api`](https:\u002F\u002Fgithub.com\u002Fhacf-fr\u002Frenault-api) and the broader Renault open-source community for documenting the Gigya\u002FKamereon authentication flow used by `internal\u002Fvehicle\u002Frenault.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:\u002F\u002Fwww.elastic.co\u002Flicensing\u002Felastic-license>.\n","grosz 是一个成本意识的家庭电动汽车充电伴侣，专为雷诺电动汽车、MyEnergi Zappi2 充电器以及 Pstryk.pl 动态电价设计。该项目通过 Go 语言开发，能够整合来自 Pstryk.pl 的每小时电价数据、控制 MyEnergi Zappi2 充电器，并可选地监控雷诺汽车的充电状态和剩余电量。它每小时自动计算并安排在最便宜的时间段内完成充电，同时提供实时和历史会话记录及成本报告。grosz 适合那些希望优化家庭电动汽车充电成本的用户，尤其是在使用动态电价计划的情况下。","2026-06-11 04:06:17","CREATED_QUERY"]