<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Marco Conti</title>
    <description>The latest articles on DEV Community by Marco Conti (@contimarco77).</description>
    <link>https://dev.to/contimarco77</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4042007%2Fe7c392ba-cea9-403b-a4e0-00b4bf09e044.jpg</url>
      <title>DEV Community: Marco Conti</title>
      <link>https://dev.to/contimarco77</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/contimarco77"/>
    <language>en</language>
    <item>
      <title>The export price matters more than the battery size</title>
      <dc:creator>Marco Conti</dc:creator>
      <pubDate>Tue, 18 Aug 2026 06:46:08 +0000</pubDate>
      <link>https://dev.to/contimarco77/the-export-price-matters-more-than-the-battery-size-4m3d</link>
      <guid>https://dev.to/contimarco77/the-export-price-matters-more-than-the-battery-size-4m3d</guid>
      <description>&lt;p&gt;I built a CLI that answers one question on your own metered data: would a home battery have paid for itself? Not a forecast from a typical-year profile — a retrospective fact, computed from energy that actually flowed through your meter.&lt;/p&gt;

&lt;p&gt;Two results came out of it that I did not expect, and they are the reason the tool is shaped the way it is.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvf52g30zkqiraup5svod.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvf52g30zkqiraup5svod.png" alt="Sample card" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The biggest battery saves the most and is the worst investment
&lt;/h3&gt;

&lt;p&gt;On the household above: 15 kWh saves 261 EUR a year and takes 34.5 years to return its cost. 5 kWh saves 185 and takes 16.2. Savings and payback point in opposite directions, and every vendor quote I have seen shows the first one.&lt;/p&gt;

&lt;p&gt;It is obvious once written down and almost impossible to see from a single number, which is why every run sweeps a range of capacities and prints both columns side by side. The tension is the output.&lt;/p&gt;

&lt;h3&gt;
  
  
  The export price swamps everything else
&lt;/h3&gt;

&lt;p&gt;This is the one that changed how I think about home storage.&lt;/p&gt;

&lt;p&gt;Same house, same battery, same year of data: payback of roughly 10 years when the utility pays 0.05 EUR/kWh for exported energy, and over 23 years when it pays 0.15. A factor of two, from a variable nobody shops on.&lt;/p&gt;

&lt;p&gt;The mechanism is simple once stated. A battery earns its money by &lt;em&gt;not&lt;/em&gt; exporting — every kWh you store and later self-consume is a kWh you would have sold cheaply and bought back expensively. The value of that swap is the spread.&lt;br&gt;
Cut the spread and the battery has almost nothing to recover, no matter how big it is. Battery capacity is a second-order variable next to the tariff, and the tariff is the thing outside your control that changes when a net metering scheme ends.&lt;/p&gt;

&lt;p&gt;Every report carries a sensitivity table across export prices for that reason.&lt;/p&gt;

&lt;p&gt;There is a design decision behind that table I would defend in an interview. The strategy is greedy self-consumption: surplus charges, deficit discharges, and it &lt;strong&gt;never reads a price&lt;/strong&gt;. That sounds like a limitation. What it buys is that the energy flows are &lt;em&gt;identical&lt;/em&gt; at every export price — only the costing changes — so the entire sensitivity table is a closed-form re-costing of one simulation rather than three separate runs. Exact, not approximate. A price-aware strategy would have made that table more expensive to compute and harder to trust.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sometimes no battery helps
&lt;/h3&gt;

&lt;p&gt;A household already self-consuming most of its own production has very little surplus left to store. On one of the example homes, no capacity pays back inside a battery's lifetime, and the honest output is to say so rather than recommend the least-bad size.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn3fh218ycm6dny4nfpkg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn3fh218ycm6dny4nfpkg.png" alt="A household where no capacity pays back" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That verdict is worth more to the reader than the other two combined, and it is the one a calculator on a vendor's site will never produce.&lt;/p&gt;

&lt;h3&gt;
  
  
  Under the hood
&lt;/h3&gt;

&lt;p&gt;Deterministic end-to-end, no LLM anywhere in the compute path. Pandas-vectorized, so a multi-year file at 15-minute resolution is seconds. Pydantic at the boundaries, &lt;code&gt;mypy --strict&lt;/code&gt;, 363 tests, Apache 2.0.&lt;/p&gt;

&lt;p&gt;Ingest was most of the work, as it always is with metered data. Two column schemas, DST transitions including the ambiguous autumn hour, gap detection, and per-column auto-detection of cumulative meters versus per-interval energy — a reading of 41,238 kWh and a reading of 0.4 kWh are the same physical quantity expressed two ways, and guessing wrong silently produces a plausible answer.&lt;/p&gt;

&lt;p&gt;That last phrase is the whole difficulty of this domain, and it caught me four times. Every one of those bugs was an entire layer uniformly wrong with a green test suite, because the test fixture concealed the defect through a property that made it convenient: it was exactly 365 days long, so anything dividing by a hardcoded 365 was invisible, and it had no gaps at all, so anything assuming continuity was invisible for a different reason. The rule I write down now is to enumerate every property that makes a fixture convenient and anchor a test against a shape it does not have. If that pattern is interesting on its own, I will write it up separately — it deserves more room than a launch post.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/contimarco77/battery-worth
&lt;span class="nb"&gt;cd &lt;/span&gt;battery-worth &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Home Assistant users: a standalone script exports your long-term statistics to CSV, and the analysis engine itself contains no network code at all.&lt;/p&gt;

&lt;p&gt;The example cards are two real households from Open Power System Data, in Konstanz, Germany, CC BY 4.0. I do not have a PV system of my own, which is why the examples are somebody else's real data rather than mine — and why I would like to see what it says on yours.&lt;/p&gt;

&lt;p&gt;Sibling project: &lt;a href="https://github.com/contimarco77/solar-report" rel="noopener noreferrer"&gt;solar-report&lt;/a&gt;, narrative reporting for PV production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/contimarco77/battery-worth" rel="noopener noreferrer"&gt;https://github.com/contimarco77/battery-worth&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>datascience</category>
      <category>energy</category>
    </item>
    <item>
      <title>Building an LLM Reporting Layer That Never Invents Numbers</title>
      <dc:creator>Marco Conti</dc:creator>
      <pubDate>Wed, 22 Jul 2026 15:34:00 +0000</pubDate>
      <link>https://dev.to/contimarco77/building-an-llm-reporting-layer-that-never-invents-numbers-1ci3</link>
      <guid>https://dev.to/contimarco77/building-an-llm-reporting-layer-that-never-invents-numbers-1ci3</guid>
      <description>&lt;p&gt;&lt;em&gt;Why a solar production report generator treats grounding as a hard constraint, not a nice-to-have — and what actually enforcing that looks like in a system prompt.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Most PV monitoring dashboards are good at showing data and bad at explaining it. A chart tells you your system produced 26 kWh yesterday; it doesn't tell you whether that's normal, whether something's wrong, or whether you should care. For anyone who isn't already fluent in reading production curves, a dashboard is a wall of numbers with no verdict attached.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;solar-report&lt;/code&gt; is a small open-source CLI that turns periodic production data — plus, optionally, logged inverter events — into a short written report: an Overview, a Trend description, a list of Observations, and, when warranted, Recommendations. The interesting part isn't the report format. It's what had to be true about the pipeline before I trusted an LLM (Claude, via the Anthropic API) to write the paragraphs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The LLM never calculates anything
&lt;/h2&gt;

&lt;p&gt;The first design decision, and the one everything else follows from: the model that writes the report never sees raw time-series data, and it never does arithmetic.&lt;/p&gt;

&lt;p&gt;Aggregation, baseline computation, and anomaly detection all happen in plain Python before any API call is made. What reaches the model is a pre-computed summary — total kWh, a rolling baseline, a short list of flagged anomalies, an optional list of events — already reduced to the numbers that matter. The model's job is narrower than "analyze this data and report on it." Its job is "narrate these specific facts, in this structure, without adding your own."&lt;/p&gt;

&lt;p&gt;That distinction matters because it's the difference between a tool that occasionally hallucinates a comparison and a tool that structurally can't, because the comparison was never computed by the part of the system capable of hallucinating.&lt;/p&gt;

&lt;h2&gt;
  
  
  A threshold that came from data, not from a guess
&lt;/h2&gt;

&lt;p&gt;The anomaly detection threshold is a good example of what "grounded" costs in practice, because getting it right took a wrong first attempt.&lt;/p&gt;

&lt;p&gt;The first version flagged any day where production fell more than 15% below a rolling 4-week baseline. A reasonable-sounding number, chosen without much evidence. Running it against a few weeks of real production data flagged ordinary weather variability as an anomaly almost every week — a slightly cloudier Tuesday would trigger the same "notable event" language as an actual fault. The report cried wolf constantly, which is worse than not flagging anomalies at all: it teaches the reader to stop reading the Observations section.&lt;/p&gt;

&lt;p&gt;Two changes, both driven by what the data actually looked like rather than what sounded plausible: raising the threshold to 25%, and restricting it to &lt;em&gt;negative&lt;/em&gt; deviations only. Days that overperform aren't anomalies in any sense a system owner cares about — they're just a good day. Once both changes landed, the anomaly list started matching what a person looking at the data would actually flag as unusual.&lt;/p&gt;

&lt;p&gt;The reason this is worth mentioning in an article about LLMs, and not just a footnote about statistics: the model never had a say in this. It doesn't decide what counts as anomalous — a Python function does, tested against real data, before the model ever runs. The LLM's only involvement with anomalies is describing ones it's handed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the system prompt do less, on purpose
&lt;/h2&gt;

&lt;p&gt;Once the input to the model is fully pre-computed, the system prompt's job shifts from "understand this data" to "don't add anything to it." That turns out to need more explicit engineering than the reverse.&lt;/p&gt;

&lt;p&gt;A few of the rules that ended up in the prompt, close to verbatim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STRICT OBSERVATIONS RULE: The Observations section must reflect ONLY
the entries in "ANOMALIES DETECTED" from the input. Do not compare
daily values yourself to identify additional patterns. Do not mention
days that are not in the anomalies list, even if they appear lower
than others in the daily breakdown.

STRICT EVENTS RULE: [...] Never infer or state a correlation between
an event and a production anomaly yourself; only report a correlation
that is already marked as such in the input.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second rule exists because of a specific failure mode: once event logs (inverter alarms, derating warnings) were added as optional input, the model started drawing its own conclusions about which events explained which anomalies — sometimes correctly, sometimes not, and with no way for a reader to tell which. The fix wasn't a smarter prompt asking the model to "be careful." It was moving the correlation logic itself into Python — matching event timestamps against anomaly days before the model ever sees either — and then explicitly forbidding the model from performing that inference on its own. The input to the model already says &lt;code&gt;[matches anomaly day]&lt;/code&gt; next to any event that correlates; anything not marked that way, the model isn't allowed to connect.&lt;/p&gt;

&lt;p&gt;The same pattern shows up elsewhere in the prompt: numbers must always come with context (no bare figures), causal language about anomalies must stay hedged ("possible", "worth checking") rather than definitive, and — maybe the least glamorous rule, but one that matters for a recurring report — no fact is allowed to appear in more than one section, so the same anomaly doesn't get described three different ways in one report.&lt;/p&gt;

&lt;p&gt;None of these rules makes the model smarter. They make it narrower, deliberately, so the parts of the report that look like judgment are actually judgment that happened in Python, upstream, where it can be tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands today
&lt;/h2&gt;

&lt;p&gt;This is v0.1, and it's honest about what that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data source is CSV only for now.&lt;/strong&gt; A structured production-data source is easy to swap — Home Assistant's REST API and long-term statistics are next — but right now, getting your data in means a CSV with a &lt;code&gt;timestamp&lt;/code&gt; and &lt;code&gt;production_wh&lt;/code&gt; column.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Events are opt-in and vendor-agnostic.&lt;/strong&gt; A second CSV of logged alarms/events enriches the report when present; the tool works exactly the same without it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No retry logic beyond the Anthropic SDK's defaults, no streaming.&lt;/strong&gt; Fine for a report generated on a schedule; worth knowing if a transient API failure matters to your setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Here's what a generated report actually looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftmra02173zj67jqsl5vd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftmra02173zj67jqsl5vd.png" alt="Sample solar-report output" width="781" height="1048"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The repo, including the sample data and the exact system prompt above, is at &lt;a href="https://github.com/contimarco77/solar-report" rel="noopener noreferrer"&gt;github.com/contimarco77/solar-report&lt;/a&gt;. It runs a &lt;code&gt;--dry-run&lt;/code&gt; mode that exercises the full pipeline — CSV parsing, aggregation, anomaly detection — without calling the API, if you want to see the mechanics before spending anything on a real report.&lt;/p&gt;

&lt;p&gt;If you've hit similar grounding problems with LLM output in a different domain, or think the anomaly threshold is wrong for a use case I haven't considered, I'd genuinely like to hear about it.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>python</category>
      <category>llm</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
