<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://www.sebastianplattner.ch/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.sebastianplattner.ch/" rel="alternate" type="text/html" /><updated>2026-09-23T12:13:36+00:00</updated><id>https://www.sebastianplattner.ch/feed.xml</id><title type="html">Sebastian Plattner</title><subtitle>Cloud Native &amp; Kubernetes engineer building open source home automation. Creator of OpenZEV.</subtitle><entry><title type="html">Introducing OpenZEV</title><link href="https://www.sebastianplattner.ch/blog/introducing-openzev/" rel="alternate" type="text/html" title="Introducing OpenZEV" /><published>2026-09-23T00:00:00+00:00</published><updated>2026-09-23T00:00:00+00:00</updated><id>https://www.sebastianplattner.ch/blog/introducing-openzev</id><content type="html" xml:base="https://www.sebastianplattner.ch/blog/introducing-openzev/"><![CDATA[<p>If you share one grid connection with your neighbours — a “ZEV” (Zusammenschluss
zum Eigenverbrauch) or “vZEV” in Swiss terminology — someone has to work out who
owes what for the solar that was produced and consumed behind that single meter.
That someone was me, doing it by hand in a spreadsheet, for my own ZEV. It got old
fast. So I built <a href="https://github.com/splattner/openzev">OpenZEV</a>, an open source
billing platform for exactly this problem, and put it out at
<a href="https://www.openzev.ch/">openzev.ch</a> in case it’s useful for other communities too.</p>

<h2 id="what-a-zev-billing-problem-actually-looks-like">What a ZEV billing problem actually looks like</h2>

<p>A ZEV pools solar production and consumption across several households behind one
grid connection. Correct billing means importing every participant’s meter data,
splitting each 15-minute interval of shared production and consumption fairly
between them, pricing it against the community’s tariffs, and turning that into an
invoice someone can actually pay. Do it manually and it’s tedious and error-prone;
get it wrong and someone is quietly overpaying for power they didn’t use.</p>

<p>OpenZEV does that whole chain end to end.</p>

<h2 id="what-it-does">What it does</h2>

<ul>
  <li><strong>Communities, roles and metering points.</strong> Four roles — admin, ZEV owner,
participant, guest — each see the slice of data relevant to them. Assignments
between participants and metering points carry validity dates, so someone moving
out mid-month is billed correctly and the next tenant picks up cleanly from where
they left off.</li>
  <li><strong>Metering imports.</strong> CSV/Excel with configurable column mapping, or SDAT-CH for
utilities that speak it. Every import runs as a preview first, with a per-row
protocol, so you see exactly what a file will do before it touches the database.
A data-quality view flags gaps, duplicates and implausible readings.</li>
  <li><strong>Tariffs and billing.</strong> Allocation runs per timestamp — every 15-minute
interval is split across participants and priced with whichever tariff version
was valid at that exact moment. Tariffs are versioned, with high/low bands and
seasonal windows, so an invoice raised last year still prices at last year’s
rate even if tariffs have changed since. Dynamic price series and a grid
operator’s machine-readable tariff file (Art. 7b StromVV) are both supported.</li>
  <li><strong>Documents.</strong> Invoices render as PDF/A-3b with a Swiss QR-bill payment slip.
Annual statements and participation contracts come out of the same renderer,
and every issued version is preserved exactly as it was sent.</li>
  <li><strong>Feasibility planning.</strong> Before you’ve even founded a community, OpenZEV can
estimate savings, payback, ROI and NPV from modelled producers and consumers —
useful for figuring out whether starting a ZEV is worth it in the first place.</li>
</ul>

<p>It’s self-hosted and licensed AGPL-3.0.</p>

<h2 id="how-it-was-built">How it was built</h2>

<p>OpenZEV was built with generous AI assistance — right down to the specs, ADRs and
user docs. That’s a deliberate trade-off, not an oversight: some design choices
might look a little unconventional compared to what a more seasoned team would
land on. The project is optimized for learning, experimentation, and running my
own ZEV well, not for enterprise-grade process perfection. It’s shipped as-is —
check your own data and billing outputs before they reach a participant.</p>

<p>If you’re running (or thinking about starting) a Swiss ZEV or vZEV, the code is on
<a href="https://github.com/splattner/openzev">GitHub</a> and the project has its own home at
<a href="https://www.openzev.ch/">openzev.ch</a>.</p>]]></content><author><name></name></author><category term="Home Automation" /><category term="Open Source" /><summary type="html"><![CDATA[If you share one grid connection with your neighbours — a “ZEV” (Zusammenschluss zum Eigenverbrauch) or “vZEV” in Swiss terminology — someone has to work out who owes what for the solar that was produced and consumed behind that single meter. That someone was me, doing it by hand in a spreadsheet, for my own ZEV. It got old fast. So I built OpenZEV, an open source billing platform for exactly this problem, and put it out at openzev.ch in case it’s useful for other communities too.]]></summary></entry><entry><title type="html">Reverse-Engineering My Ökoboiler’s Internal UART Bus</title><link href="https://www.sebastianplattner.ch/blog/reverse-engineering-oekoboiler/" rel="alternate" type="text/html" title="Reverse-Engineering My Ökoboiler’s Internal UART Bus" /><published>2026-09-22T00:00:00+00:00</published><updated>2026-09-22T00:00:00+00:00</updated><id>https://www.sebastianplattner.ch/blog/reverse-engineering-oekoboiler</id><content type="html" xml:base="https://www.sebastianplattner.ch/blog/reverse-engineering-oekoboiler/"><![CDATA[<p>My <a href="https://www.oekoboiler.com/">Ökoboiler</a> heat-pump water heater is an older
generation with zero connectivity — no WiFi, no app, no API. Inside, two circuit
boards, a display/operation panel on the front and the main control board, talk to
each other over a single-wire serial bus. That bus turned out to be the only way
in, so I tapped it with a €5 ESP32, captured the traffic, and reverse-engineered
the protocol. The full write-up, code and captures are on
<a href="https://github.com/splattner/oekoboiler-uart-reverse-engineering">GitHub</a>; this
is the short version.</p>

<p>The result is a local, cloud-free sensor reading water tank temperature (to about
0.3°C — finer than the front panel’s own 1°C display), operating mode
(heating/idle/defrost), and PV/solar state — fed straight into ESPHome and Home
Assistant.</p>

<h2 id="the-hardware">The hardware</h2>

<p>The main control board is an ATmega16-based SY-384. It talks to the front panel
over a single-wire, half-duplex UART bus at 2400 baud, 8N1, optically isolated.
The panel is wired with only three conductors: UART data, 5V and GND — no sensors
of its own. That detail mattered: it proved that everything the panel displays,
including the tank temperature, has to arrive over this one wire. It was in there
somewhere.</p>

<h2 id="finding-structure-in-the-bytes">Finding structure in the bytes</h2>

<p>Dumping the raw stream, a repeating 32-byte frame jumped out, always starting with
the same two header bytes. Byte 5 changes every frame, and XOR-ing bytes 2–29
against it produces a stable, readable payload — after which a constant signature
appears at bytes 6–9, a reliable sanity check that decoding worked. Bytes 30–31
turned out to be a CRC-16/Modbus over the rest of the frame, which let me discard
corrupted frames with confidence. Decoded byte 11 splits the traffic into two
families, one per direction of the conversation.</p>

<p>Two fields fell quickly just by watching decoded byte 10 while poking the
machine: the low bits give operating mode (heating/idle/defrost), and one bit
flags PV/solar mode.</p>

<h2 id="the-pv-mode-that-wasnt-supposed-to-be-there">The PV mode that wasn’t supposed to be there</h2>

<p>My unit was sold as the cheaper model, advertised <em>without</em> PV/solar mode. Except
it basically has one: PV mode is documented in the manual that shipped with the
unit, with its own target temperatures, and the schematic has a terminal labelled
“PV Input” sitting right there on the board. The firmware never lost the feature —
the pricier model just adds something to drive the trigger.</p>

<p>So I enabled PV mode in the settings menu, wired a Shelly 1 Mini Gen3 relay onto
that PV Input terminal, and let Solar Manager close it whenever my system has
surplus solar. The boiler now jumps to its PV setpoint on sunny afternoons, and
the bus decode confirms it by reading the resulting PV state straight back.</p>

<h2 id="the-water-temperature-hunt">The water-temperature hunt</h2>

<p>This was the hard part, and it took days. The obvious approach — note the display
reading, find a matching byte, repeat — kept failing, because the board reads
several temperature sensors (tank, evaporator, ambient, exhaust) and their bytes
all shift with operating state. Two moments showing the same tank temperature on
the display can have very different readings everywhere else on the bus. Spot
checks confound temperature with machine state; you need a real sweep of labelled
data to untangle it.</p>

<p>I re-enabled a camera-and-OCR rig I already had pointed at the front panel, logging
the displayed temperature into InfluxDB alongside every decoded byte, and started
correlating properly — Pearson correlation between the ground-truth display
reading and each of roughly 60 candidate bytes.</p>

<p>The first attempt was a trap: overnight, as the tank cools passively, <em>every</em>
byte that drifts downward correlates with temperature at ~0.98, because they’re
all cooling together. I needed a heating event — a moment where the tank diverges
from everything else — so I waited for a full day-night “V”: cool down overnight,
reheat during the day. Even then, several coil-sensor bytes still scored ~0.9 on
plain correlation, so I ran a time-lag scan — sliding each candidate byte forward
and backward in time before recomputing the correlation. The real tank reading
peaks sharply at lag zero; a coil sensor that merely follows the same daily
heating rhythm peaks 90–120 minutes off, revealing it as a coincidence of timing
rather than the thing itself.</p>

<p>That scan pointed straight at decoded byte 14: it peaks at lag zero, uses the full
0–255 range, and had been dismissed early on as noise precisely because it’s
higher-resolution than the display and wraps around. <code class="language-plaintext highlighter-rouge">byte14 mod 64</code>, plotted
against temperature, is a clean line — falling about 10 units per °C and wrapping
every 6.4°C. A second byte, 15, supplies the coarse band needed to disambiguate
which wrap you’re in.</p>

<h2 id="the-payoff">The payoff</h2>

<p>No cloud, no API reverse-engineered from an app — just a wire tap, a lot of
correlation, and a sensor that now feeds temperature, mode and PV state directly
into ESPHome and Home Assistant. The full protocol notes, capture data and code
are in the <a href="https://github.com/splattner/oekoboiler-uart-reverse-engineering">repo</a>
if you’re staring at a similarly silent appliance.</p>]]></content><author><name></name></author><category term="Home Automation" /><category term="Reverse Engineering" /><summary type="html"><![CDATA[My Ökoboiler heat-pump water heater is an older generation with zero connectivity — no WiFi, no app, no API. Inside, two circuit boards, a display/operation panel on the front and the main control board, talk to each other over a single-wire serial bus. That bus turned out to be the only way in, so I tapped it with a €5 ESP32, captured the traffic, and reverse-engineered the protocol. The full write-up, code and captures are on GitHub; this is the short version.]]></summary></entry><entry><title type="html">Three Small Kubernetes Tools Born From Real Annoyances</title><link href="https://www.sebastianplattner.ch/blog/kubernetes-side-projects/" rel="alternate" type="text/html" title="Three Small Kubernetes Tools Born From Real Annoyances" /><published>2026-09-21T00:00:00+00:00</published><updated>2026-09-21T00:00:00+00:00</updated><id>https://www.sebastianplattner.ch/blog/kubernetes-side-projects</id><content type="html" xml:base="https://www.sebastianplattner.ch/blog/kubernetes-side-projects/"><![CDATA[<p>Not every project needs a grand plan. These three came out of specific problems I
ran into — some in my homelab, some during my day job as a Platform Architect —
and each one just solves that problem and stops. A lot of AI assistance went into
all three, they’re shipped as-is with no roadmap for continuous development, and
I’m fully open to contributions if someone finds them useful and wants to take
them further. I’ll keep contributing myself whenever I have time or hit a need for
a new feature.</p>

<h2 id="node-providerid">node-providerid</h2>

<p><a href="https://github.com/splattner/node-providerid">node-providerid</a> is a Go CLI that
reads or changes a Kubernetes Node’s <code class="language-plaintext highlighter-rouge">spec.providerID</code> directly through the etcd
v3 API, targeting embedded etcd on a k3s server by default.</p>

<p><code class="language-plaintext highlighter-rouge">providerID</code> links a Node object to its machine in the cloud provider, and
Kubernetes treats it as immutable once set — the API server rejects any attempt
to change a non-empty value, and it can’t be cleared through <code class="language-plaintext highlighter-rouge">kubectl edit</code> or
<code class="language-plaintext highlighter-rouge">kubectl patch</code>. In practice it still ends up wrong sometimes: a Node registered
before the cloud provider was configured, a migration between providers, or a bad
<code class="language-plaintext highlighter-rouge">--provider-id</code> baked in at first boot. The usual fix is to drain, delete and
re-register the Node. When that isn’t practical, the only place left to correct
the value is the datastore itself — which is exactly what this tool does, as
narrowly as possible: it patches only the <code class="language-plaintext highlighter-rouge">providerID</code> field, guards the write
with an etcd compare-and-swap on the Node’s revision, and writes a per-key backup
first.</p>

<p>Worth saying plainly: <code class="language-plaintext highlighter-rouge">set</code> writes directly to your cluster’s backing store and
bypasses every Kubernetes safety layer — schema validation, admission control,
authorization, the lot. <code class="language-plaintext highlighter-rouge">get</code> is read-only and safe; <code class="language-plaintext highlighter-rouge">set</code> is a deliberate,
last-resort operation, and the README says so at length before you get anywhere
near running it.</p>

<h2 id="burrow">burrow</h2>

<p><a href="https://github.com/splattner/burrow">burrow</a> exposes a TCP service running
outside a Kubernetes cluster to workloads running inside it, without opening
inbound firewall ports or configuring static routes.</p>

<p>The client runs wherever the service actually lives — laptop, edge node, private
server — and dials <em>outbound</em> to a WebSocket endpoint on a server component
running inside the cluster. Because the client always initiates the connection,
it works through NAT, firewalls and most corporate networks. Traffic from pods
reaches the local service through that persistent tunnel; the server can
optionally manage a Kubernetes <code class="language-plaintext highlighter-rouge">Service</code> object per connected client, so pods
reach tunnelled services by a stable DNS name rather than tracking ports by hand.
It’s the kind of thing that’s handy for reaching a homelab machine from an
in-cluster job, or bridging a service that will never itself run in Kubernetes.</p>

<h2 id="alertmanager-label-enricher">alertmanager-label-enricher</h2>

<p><a href="https://github.com/splattner/alertmanager-label-enricher">alertmanager-label-enricher</a>
is an inline proxy that sits between Prometheus and Alertmanager and enriches
alert labels from external lookups — Kubernetes objects, HTTP endpoints, or a
static file — before forwarding alerts on.</p>

<p>Prometheus’s own <code class="language-plaintext highlighter-rouge">alert_relabel_configs</code> already covers static and conditional
label rewriting, but nothing for a lookup against something external — for
example, adding a <code class="language-plaintext highlighter-rouge">team</code> label read off the Kubernetes object for the alert’s
namespace, or off a CMDB via HTTP. Because enrichment happens <em>before</em>
Alertmanager sees the alert, the new labels participate fully in routing,
grouping, inhibition and silences, exactly like any label Prometheus set itself.</p>

<p>Rules run in declared order, all matching rules apply, and later rules see labels
earlier ones added. Overwriting or dropping an existing label changes the alert’s
fingerprint in Alertmanager — which can invalidate existing silences — so both
require an explicit opt-in, while annotations (no fingerprint risk) don’t.
<code class="language-plaintext highlighter-rouge">alertname</code> gets extra protection since it’s Alertmanager’s primary identifying
label. A rule can be marked <code class="language-plaintext highlighter-rouge">required</code>, failing the whole batch closed so
Prometheus retries if its lookup fails; everything else fails open rather than
block delivery.</p>

<hr />

<p>None of these three are trying to be a platform or a product — they each fix one
specific thing that was in my way. If one of them is in your way too, they’re on
GitHub and I’ll happily look at a PR.</p>]]></content><author><name></name></author><category term="Kubernetes" /><category term="Open Source" /><summary type="html"><![CDATA[Not every project needs a grand plan. These three came out of specific problems I ran into — some in my homelab, some during my day job as a Platform Architect — and each one just solves that problem and stops. A lot of AI assistance went into all three, they’re shipped as-is with no roadmap for continuous development, and I’m fully open to contributions if someone finds them useful and wants to take them further. I’ll keep contributing myself whenever I have time or hit a need for a new feature.]]></summary></entry><entry><title type="html">Bridging digitalSTROM to the Rest of My Smart Home</title><link href="https://www.sebastianplattner.ch/blog/digitalstrom-vdc-bridge/" rel="alternate" type="text/html" title="Bridging digitalSTROM to the Rest of My Smart Home" /><published>2026-09-20T00:00:00+00:00</published><updated>2026-09-20T00:00:00+00:00</updated><id>https://www.sebastianplattner.ch/blog/digitalstrom-vdc-bridge</id><content type="html" xml:base="https://www.sebastianplattner.ch/blog/digitalstrom-vdc-bridge/"><![CDATA[<p><a href="https://www.digitalstrom.com/">digitalSTROM</a> is a Swiss home automation system
built around smart relays wired directly into the electrical installation — solid
for switching and metering, but an island on its own unless something bridges it
to everything else. That bridge is a “vDC” (Virtual Device Connector), a protocol
digitalSTROM’s own server uses to talk to third-party devices as if they were
native digitalSTROM hardware. I wanted my digitalSTROM installation to see my Home
Assistant, WLED, Tasmota and Zigbee2MQTT devices, so I wrote one:
<a href="https://github.com/splattner/digitalstrom-vdc-bridge">digitalstrom-vdc-bridge</a>,
a Go implementation of the vDC API with a web-based configuration UI and a plugin
system for bridging in other systems.</p>

<h2 id="what-it-does">What it does</h2>

<ul>
  <li><strong>Full vDC API compatibility</strong> — the protobuf wire protocol, all 16 inbound
message types, scenes, channels, sensor descriptors, and DNS-SD advertisement
so the digitalSTROM Server discovers the bridge automatically on the network.</li>
  <li><strong>A web UI</strong>, embedded in the binary with no separate server needed, for
schema-driven plugin management, device discovery and bridge mapping.</li>
  <li><strong>A plugin system</strong> built around a shared MQTT broker connection, with plugins
for Home Assistant (discovers lights and sensors via its WebSocket API), WLED
(mDNS discovery), Tasmota (via Tasmota’s own MQTT discovery), and Zigbee2MQTT
(via its bridge/devices topic).</li>
  <li><strong>Persistent storage</strong>, so scenes, device configs and bridge mappings survive a
restart.</li>
</ul>

<p>It’s heavily inspired by, and builds on protocol work from,
<a href="https://github.com/plan44/vdcd">plan44/vdcd</a> by Lukas Zeller — the vDC API
protocol handling and device model abstractions follow the patterns established
in that project, and I’m grateful for the open reference implementation.</p>

<h2 id="running-it">Running it</h2>

<p>It ships as a container and, more usefully for most of my own setup, as a Home
Assistant add-on. The standalone container needs <code class="language-plaintext highlighter-rouge">--network host</code>, because mDNS
(DNS-SD) discovery relies on multicast traffic that a Docker bridge network would
otherwise isolate — without it, the digitalSTROM Server simply never finds the
bridge on its own. That does mean the web UI and API are reachable by anything on
the LAN with no login by default, so there’s an optional HTTP Basic Auth flag for
the standalone case; running it as a Home Assistant add-on instead gates access
through your HA session via ingress, which is the simpler and safer path if
you’re already on Home Assistant.</p>

<p>If you’re running digitalSTROM alongside anything else — Home Assistant, WLED,
Tasmota, Zigbee2MQTT — the bridge, its docs and the Home Assistant add-on
repository are on <a href="https://github.com/splattner/digitalstrom-vdc-bridge">GitHub</a>.</p>]]></content><author><name></name></author><category term="Home Automation" /><summary type="html"><![CDATA[digitalSTROM is a Swiss home automation system built around smart relays wired directly into the electrical installation — solid for switching and metering, but an island on its own unless something bridges it to everything else. That bridge is a “vDC” (Virtual Device Connector), a protocol digitalSTROM’s own server uses to talk to third-party devices as if they were native digitalSTROM hardware. I wanted my digitalSTROM installation to see my Home Assistant, WLED, Tasmota and Zigbee2MQTT devices, so I wrote one: digitalstrom-vdc-bridge, a Go implementation of the vDC API with a web-based configuration UI and a plugin system for bridging in other systems.]]></summary></entry></feed>