<?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: Csott Károly Péter</title>
    <description>The latest articles on DEV Community by Csott Károly Péter (@csottkarolypeter).</description>
    <link>https://dev.to/csottkarolypeter</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%2F4214188%2F03055191-6f4e-44d8-8046-4d576e104106.png</url>
      <title>DEV Community: Csott Károly Péter</title>
      <link>https://dev.to/csottkarolypeter</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/csottkarolypeter"/>
    <language>en</language>
    <item>
      <title>How to Validate 15-Minute Meter Data Before PV/BESS Modelling</title>
      <dc:creator>Csott Károly Péter</dc:creator>
      <pubDate>Thu, 29 Oct 2026 03:25:13 +0000</pubDate>
      <link>https://dev.to/csottkarolypeter/how-to-validate-15-minute-meter-data-before-pvbess-modelling-1bo</link>
      <guid>https://dev.to/csottkarolypeter/how-to-validate-15-minute-meter-data-before-pvbess-modelling-1bo</guid>
      <description>&lt;p&gt;&lt;strong&gt;Csott Károly Péter&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A meter export can open successfully and still describe the wrong timeline. Before using it in a solar PV or battery-storage model, I want to establish what one row means, how its timestamp is encoded and which quantity the meter supplied.&lt;/p&gt;

&lt;p&gt;This tutorial checks those questions with Python. Every number below is synthetic. The purpose is a reproducible data-quality exercise, with no customer dataset or commercial offer.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Define the interval before parsing values
&lt;/h2&gt;

&lt;p&gt;Use an interval start and an exclusive end. Record whether energy is already aggregated for that interval or is a cumulative register that needs a separate, documented conversion.&lt;/p&gt;

&lt;p&gt;For the example schema, a row contains 15-minute energy in kWh. Average power is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;average_kW = interval_kWh / (interval_minutes / 60)
125 kWh / 0.25 h = 500 kW
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That 500 kW is an interval average. It does not describe the maximum instantaneous power. Nor is it a PV nameplate rating in kWp.&lt;/p&gt;

&lt;p&gt;Do not repair an ambiguous unit by multiplying a column until the result looks plausible. Ask for the source definition, meter multiplier and export documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Reject naive timestamps
&lt;/h2&gt;

&lt;p&gt;An unqualified &lt;code&gt;2026-10-25T02:00:00&lt;/code&gt; is ambiguous during the autumn clock change in Europe/Budapest. These two timestamps identify different instants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-10-25T02:00:00+02:00  -&amp;gt; 2026-10-25T00:00:00+00:00
2026-10-25T02:00:00+01:00  -&amp;gt; 2026-10-25T01:00:00+00:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normalize to UTC before comparing or deduplicating. Keep the named local timezone as metadata so local tariff periods can later be interpreted correctly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;as_utc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+00:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcoffset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;An explicit UTC offset is required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astimezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The standard-library &lt;a href="https://docs.python.org/3/library/zoneinfo.html" rel="noopener noreferrer"&gt;&lt;code&gt;zoneinfo&lt;/code&gt;&lt;/a&gt; module supplies IANA timezone rules. Python needs an installed timezone database; a machine without one needs the maintained &lt;code&gt;tzdata&lt;/code&gt; package.&lt;/p&gt;

&lt;p&gt;Reopen the actual exported CSV during testing. Spreadsheet or import/export tools can reinterpret date-like strings even when the in-memory source data was correct. Compare the exported timestamps with the intended instants and offsets before using the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Check coverage against the requested period
&lt;/h2&gt;

&lt;p&gt;Checking only gaps between the rows received misses a missing first or last interval. Build the expected UTC grid from independently specified period boundaries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;coverage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;starts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;period_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;period_end&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;period_end&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;period_start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected period must have positive duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;period_end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;period_start&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected period is not a whole number of intervals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;period_start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;starts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;starts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outside_period&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;starts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duplicates&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All datetime inputs to this function must already be aware UTC timestamps. For a 25-hour local day, a complete quarter-hour series contains 100 intervals. For the 23-hour day it contains 92. A hardcoded expectation of 96 would reject correct data and could hide a faulty correction.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Check quantities and the measurement boundary
&lt;/h2&gt;

&lt;p&gt;Reject blank required quantities, &lt;code&gt;NaN&lt;/code&gt;, infinity and signs that conflict with the documented convention. A real zero is different from a missing reading.&lt;/p&gt;

&lt;p&gt;For a common-AC baseline without storage, the interval balance is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;grid import + PV generation = site load + grid export
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This identity requires aligned channels and a consistent boundary. When storage is present, the balance needs charge/discharge and relevant loss terms. A checker for the no-storage schema should report that limitation instead of giving an incorrect pass.&lt;/p&gt;

&lt;p&gt;Directional import and export can both be positive within a quarter-hour when power flow changes direction inside that interval. Do not reject that situation merely because a simplified dispatch model assumed a single constant flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Keep suspicious peaks visible
&lt;/h2&gt;

&lt;p&gt;In a synthetic sequence, 230 kWh of import during one quarter-hour corresponds to 920 kW on average. Flag it for review with the meter data owner. Check demand-window definitions, operating events and meter limits. Do not replace it with a percentile or cap it automatically: a real peak can matter for the subsequent assessment.&lt;/p&gt;

&lt;p&gt;Similarly, imported energy is not gross site consumption when PV already supplies some loads. Keep the available physical channels separate.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Test the failure cases
&lt;/h2&gt;

&lt;p&gt;The companion &lt;a href="https://github.com/Csottkarolypeter/industrial-energy-data-readiness" rel="noopener noreferrer"&gt;Industrial Energy Data Readiness Toolkit&lt;/a&gt; includes a read-only checker and a deterministic synthetic generator. Its regression cases cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;92- and 100-interval DST days and a 676-interval synthetic week;&lt;/li&gt;
&lt;li&gt;missing first and last intervals;&lt;/li&gt;
&lt;li&gt;repeated UTC instants and legitimate repeated local clocks;&lt;/li&gt;
&lt;li&gt;naive timestamps, wrong energy units and negative directional quantities;&lt;/li&gt;
&lt;li&gt;non-finite values, broken balances and an unsupported storage boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run the code from its repository with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; unittest discover &lt;span class="nt"&gt;-s&lt;/span&gt; tests &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful consistency check is a documented input to the next modelling step. It does not prove that the meter is calibrated, that a week represents a year or that a battery investment will perform as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  References and disclosure
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.eia.gov/energyexplained/electricity/measuring-electricity.php" rel="noopener noreferrer"&gt;EIA: Measuring electricity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.python.org/3/library/zoneinfo.html" rel="noopener noreferrer"&gt;Python: zoneinfo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://samrepo.nlr.gov/help/battery_storage_fom.html" rel="noopener noreferrer"&gt;System Advisor Model: battery inputs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This tutorial was prepared with generative AI assistance, including drafting and code preparation. The examples are synthetic and the implementation is accompanied by executable tests. Author: Csott Károly Péter, working in B2B energy sales, business development and commercial project screening.&lt;/p&gt;

</description>
      <category>python</category>
      <category>datascience</category>
      <category>testing</category>
      <category>energy</category>
    </item>
  </channel>
</rss>
