<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://mkuthan.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://mkuthan.github.io/" rel="alternate" type="text/html" /><updated>2026-08-13T10:10:52+00:00</updated><id>https://mkuthan.github.io/feed.xml</id><title type="html">Passionate Developer</title><subtitle>Software engineering blog</subtitle><author><name>Marcin Kuthan</name></author><entry><title type="html">Home Assistant solar energy management V2</title><link href="https://mkuthan.github.io/blog/2025/11/26/home-assistant-solar-v2/" rel="alternate" type="text/html" title="Home Assistant solar energy management V2" /><published>2025-11-26T00:00:00+00:00</published><updated>2025-11-26T00:00:00+00:00</updated><id>https://mkuthan.github.io/blog/2025/11/26/home-assistant-solar-v2</id><content type="html" xml:base="https://mkuthan.github.io/blog/2025/11/26/home-assistant-solar-v2/"><![CDATA[<p>Recently I decided to learn Python seriously and studied Fluent Python: Clear, Concise, and Effective Programming by Luciano Ramalho.
The hardest part was finding a project to apply the new skills.
At work I mostly use Python to write small scripts and Apache Airflow DAGs.
I wanted something more challenging with complex domain logic and real-world data ⚙️📊</p>

<p><img src="/assets/images/2025-11-26-home-assistant-solar-v2/fluent_python_book_cover.jpg" alt="Fluent Python" /></p>

<h2 id="introduction">Introduction</h2>

<p>This spring I installed a photovoltaic (PV) system on my roof to generate renewable energy for my home.
Soon after, I wrote a Home Assistant automation to optimize solar use.
You can read more in my previous post <a href="/blog/2025/04/12/home-assistant-solar/">Home Assistant solar energy management</a>.</p>

<p>Summer, with plenty of sunshine, didn’t really challenge the YAML-based setup.
But as autumn and winter approached — with shorter days, less predictable weather, and increasing heating energy consumption — I realized I needed something more robust than a collection of YAML automations.</p>

<p class="notice--info">It was the perfect chance to put lessons from Fluent Python into practice. 😂</p>

<p>After two to three months of tinkering in my spare time, I implemented a new solar and heating energy management system using <a href="https://github.com/AppDaemon/appdaemon">AppDaemon</a>.
Here are some stats about the project:</p>

<ul>
  <li>📦 2,529 lines of production code across 55 Python files</li>
  <li>🧪 4,248 lines of test code in 37 test files</li>
  <li>🧾 476 test scenarios executed (170 test functions expanded through parametrization)</li>
  <li>✅ 94% test coverage</li>
</ul>

<p>I originally planned to describe every part of the project, but that would be too much for most readers. So I split the post into three parts:</p>

<ol>
  <li>Configuration snippets that reveal the system’s complexity without overwhelming.</li>
  <li>A closer look at a few Python techniques I used, could be useful even if you aren’t interested in renewable energy.</li>
  <li>Selected algorithms and how they work in practice.</li>
</ol>

<h2 id="appdaemon-applications">AppDaemon applications</h2>

<p>AppDaemon is a lightweight Python daemon I use alongside Home Assistant.
It listens to events and instantiates small Python classes (apps) that register callbacks, read state, and call services.
I installed AppDaemon as <a href="https://github.com/hassio-addons/addon-appdaemon">Home Assistant add-on</a> so it runs in a docker container inside Home Assistant.</p>

<h3 id="solar">Solar</h3>

<p>The first app is responsible for solar energy management:</p>

<ul>
  <li>Setting up battery reserve SoC control every 5 minutes</li>
  <li>Setting up storage mode control triggers</li>
  <li>Setting up battery discharge schedule</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
</pre></td><td class="rouge-code"><pre><span class="kn">import</span> <span class="n">appdaemon.plugins.hass.hassapi</span> <span class="k">as</span> <span class="n">hass</span>

<span class="k">class</span> <span class="nc">SolarApp</span><span class="p">(</span><span class="n">hass</span><span class="p">.</span><span class="n">Hass</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">self</span><span class="p">.</span><span class="n">solar</span> <span class="o">=</span> <span class="nc">Solar</span><span class="p">(</span>
            <span class="n">configuration</span><span class="o">=</span><span class="n">configuration</span><span class="p">,</span>
            <span class="n">state_factory</span><span class="o">=</span><span class="n">state_factory</span><span class="p">,</span>
            <span class="n">battery_discharge_slot_estimator</span><span class="o">=</span><span class="nc">BatteryDischargeSlotEstimator</span><span class="p">(...),</span>
            <span class="n">battery_reserve_soc_estimator</span><span class="o">=</span><span class="nc">BatteryReserveSocEstimator</span><span class="p">(...),</span>
            <span class="n">storage_mode_estimator</span><span class="o">=</span><span class="nc">StorageModeEstimator</span><span class="p">(...),</span>
        <span class="p">)</span>

        <span class="n">self</span><span class="p">.</span><span class="nf">run_every</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">control_battery_reserve_soc</span><span class="p">,</span> <span class="sh">"</span><span class="s">00:00:00</span><span class="sh">"</span><span class="p">,</span> <span class="mi">5</span> <span class="o">*</span> <span class="mi">60</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">listen_state</span><span class="p">(</span>
            <span class="n">self</span><span class="p">.</span><span class="n">control_storage_mode</span><span class="p">,</span> 
            <span class="p">[</span><span class="n">BATTERY_SOC_ENTITY</span><span class="p">,</span> <span class="n">PRICE_FORECAST_ENTITY</span><span class="p">],</span>
            <span class="n">constrain_start_time</span><span class="o">=</span><span class="sh">"</span><span class="s">sunrise +01:00:00</span><span class="sh">"</span><span class="p">,</span>
            <span class="n">constrain_end_time</span><span class="o">=</span><span class="sh">"</span><span class="s">sunset -01:00:00</span><span class="sh">"</span><span class="p">,</span>
        <span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">run_daily</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">schedule_battery_discharge</span><span class="p">,</span> <span class="sh">"</span><span class="s">15:30:00</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">run_daily</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">disable_battery_discharge</span><span class="p">,</span> <span class="sh">"</span><span class="s">22:00:00</span><span class="sh">"</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">control_battery_reserve_soc</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">:</span> <span class="nb">object</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">self</span><span class="p">.</span><span class="n">solar</span><span class="p">.</span><span class="nf">control_battery_reserve_soc</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="nf">get_now</span><span class="p">())</span>

    <span class="k">def</span> <span class="nf">control_storage_mode</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">entity</span><span class="p">,</span> <span class="n">attribute</span><span class="p">,</span> <span class="n">old</span><span class="p">,</span> <span class="n">new</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">self</span><span class="p">.</span><span class="n">solar</span><span class="p">.</span><span class="nf">control_storage_mode</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="nf">get_now</span><span class="p">())</span>

    <span class="k">def</span> <span class="nf">schedule_battery_discharge</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">:</span> <span class="nb">object</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">self</span><span class="p">.</span><span class="n">solar</span><span class="p">.</span><span class="nf">schedule_battery_discharge</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="nf">get_now</span><span class="p">())</span>

    <span class="k">def</span> <span class="nf">disable_battery_discharge</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">:</span> <span class="nb">object</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">self</span><span class="p">.</span><span class="n">solar</span><span class="p">.</span><span class="nf">disable_battery_discharge</span><span class="p">()</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>As you can see, the app mostly wires AppDaemon framework with the core <code class="language-plaintext highlighter-rouge">Solar</code> class that implements the actual logic.
This is a design strategy because AppDaemon has limited support for testing.</p>

<p>The interesting part is the configuration of the <code class="language-plaintext highlighter-rouge">Solar</code> class to externalize all domain-specific parameters.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
</pre></td><td class="rouge-code"><pre><span class="n">configuration</span> <span class="o">=</span> <span class="nc">SolarConfiguration</span><span class="p">(</span>
    <span class="c1"># nominal battery capacity
</span>    <span class="n">battery_capacity</span><span class="o">=</span><span class="nc">EnergyKwh</span><span class="p">(</span><span class="mf">10.0</span><span class="p">),</span>
    <span class="c1"># nominal battery voltage
</span>    <span class="n">battery_voltage</span><span class="o">=</span><span class="nc">BatteryVoltage</span><span class="p">(</span><span class="mf">52.0</span><span class="p">),</span>
    <span class="c1"># maximum battery discharge/charge current
</span>    <span class="n">battery_maximum_current</span><span class="o">=</span><span class="nc">BatteryCurrent</span><span class="p">(</span><span class="mf">80.0</span><span class="p">),</span>
    <span class="c1"># minimum reserve SOC
</span>    <span class="n">battery_reserve_soc_min</span><span class="o">=</span><span class="nc">BatterySoc</span><span class="p">(</span><span class="mf">20.0</span><span class="p">),</span>
    <span class="c1"># margin above minimum reserve SOC
</span>    <span class="n">battery_reserve_soc_margin</span><span class="o">=</span><span class="nc">BatterySoc</span><span class="p">(</span><span class="mf">8.0</span><span class="p">),</span>
    <span class="c1"># upper limit when charging from the grid
</span>    <span class="n">battery_reserve_soc_max</span><span class="o">=</span><span class="nc">BatterySoc</span><span class="p">(</span><span class="mf">90.0</span><span class="p">),</span>
    <span class="c1"># indoor temperature setpoint to estimate heating needs
</span>    <span class="n">temp_in</span><span class="o">=</span><span class="nc">Celsius</span><span class="p">(</span><span class="mf">21.0</span><span class="p">),</span>
    <span class="c1"># outdoor temperature threshold to apply heating energy consumption in eco mode
</span>    <span class="n">temp_out_threshold</span><span class="o">=</span><span class="nc">Celsius</span><span class="p">(</span><span class="mf">2.0</span><span class="p">),</span>
    <span class="c1"># coefficient of heat-pump performance at 7 degrees Celsius
</span>    <span class="n">heating_cop_at_7c</span><span class="o">=</span><span class="mf">4.0</span><span class="p">,</span>
    <span class="c1"># coefficient representing building heat loss rate in kW/°C
</span>    <span class="n">heating_h</span><span class="o">=</span><span class="mf">0.18</span><span class="p">,</span>
    <span class="c1"># outdoor temperature if weather forecast isn't available
</span>    <span class="n">temp_out_fallback</span><span class="o">=</span><span class="nc">Celsius</span><span class="p">(</span><span class="mf">2.0</span><span class="p">),</span>
    <span class="c1"># outdoor humidity if weather forecast isn't available
</span>    <span class="n">humidity_out_fallback</span><span class="o">=</span><span class="mf">80.0</span><span class="p">,</span>
    <span class="c1"># regular consumption when in away mode
</span>    <span class="n">regular_consumption_away</span><span class="o">=</span><span class="nc">EnergyKwh</span><span class="p">(</span><span class="mf">0.35</span><span class="p">),</span>
    <span class="c1"># consumption during daytime
</span>    <span class="n">regular_consumption_day</span><span class="o">=</span><span class="nc">EnergyKwh</span><span class="p">(</span><span class="mf">0.5</span><span class="p">),</span>
    <span class="c1"># consumption during evening
</span>    <span class="n">regular_consumption_evening</span><span class="o">=</span><span class="nc">EnergyKwh</span><span class="p">(</span><span class="mf">0.8</span><span class="p">),</span>
    <span class="c1"># threshold for exporting PV energy, net price
</span>    <span class="n">pv_export_min_price_margin</span><span class="o">=</span><span class="n">EnergyPrice</span><span class="p">.</span><span class="nf">pln_per_mwh</span><span class="p">(</span><span class="nc">Decimal</span><span class="p">(</span><span class="mi">200</span><span class="p">)),</span>
    <span class="c1"># threshold for exporting battery energy, net price
</span>    <span class="n">battery_export_threshold_price</span><span class="o">=</span><span class="n">EnergyPrice</span><span class="p">.</span><span class="nf">pln_per_mwh</span><span class="p">(</span><span class="nc">Decimal</span><span class="p">(</span><span class="mi">1000</span><span class="p">)),</span>
    <span class="c1"># skip battery export below this threshold
</span>    <span class="n">battery_export_threshold_energy</span><span class="o">=</span><span class="nc">EnergyKwh</span><span class="p">(</span><span class="mf">1.0</span><span class="p">),</span>
    <span class="c1"># start time of night low tariff period (with margin)
</span>    <span class="n">night_low_tariff_time_start</span><span class="o">=</span><span class="n">time</span><span class="p">.</span><span class="nf">fromisoformat</span><span class="p">(</span><span class="sh">"</span><span class="s">22:05:00</span><span class="sh">"</span><span class="p">),</span>
    <span class="c1"># end time of night low tariff period (with margin)
</span>    <span class="n">night_low_tariff_time_end</span><span class="o">=</span><span class="n">time</span><span class="p">.</span><span class="nf">fromisoformat</span><span class="p">(</span><span class="sh">"</span><span class="s">06:55:00</span><span class="sh">"</span><span class="p">),</span>
    <span class="c1"># start time of day low tariff period (with margin)
</span>    <span class="n">day_low_tariff_time_start</span><span class="o">=</span><span class="n">time</span><span class="p">.</span><span class="nf">fromisoformat</span><span class="p">(</span><span class="sh">"</span><span class="s">13:05:00</span><span class="sh">"</span><span class="p">),</span>
    <span class="c1"># end time of day low tariff period (with margin)
</span>    <span class="n">day_low_tariff_time_end</span><span class="o">=</span><span class="n">time</span><span class="p">.</span><span class="nf">fromisoformat</span><span class="p">(</span><span class="sh">"</span><span class="s">15:55:00</span><span class="sh">"</span><span class="p">),</span>
<span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Static configuration is complemented by dynamic state information about the system taken from Home Assistant sensors and integrations:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="k">class</span> <span class="nc">SolarState</span><span class="p">:</span>
    <span class="n">battery_soc</span><span class="p">:</span> <span class="n">BatterySoc</span> <span class="c1"># current battery state of charge
</span>    <span class="n">battery_reserve_soc</span><span class="p">:</span> <span class="n">BatterySoc</span> <span class="c1"># current battery reserve state of charge
</span>    <span class="n">is_away_mode</span><span class="p">:</span> <span class="nb">bool</span> <span class="c1"># away mode status
</span>    <span class="n">is_eco_mode</span><span class="p">:</span> <span class="nb">bool</span> <span class="c1"># eco mode status
</span>    <span class="n">inverter_storage_mode</span><span class="p">:</span> <span class="n">StorageMode</span> <span class="c1"># current inverter storage mode
</span>    <span class="n">is_slot1_discharge_enabled</span><span class="p">:</span> <span class="nb">bool</span> <span class="c1"># whether slot 1 discharge is enabled
</span>    <span class="n">slot1_discharge_time</span><span class="p">:</span> <span class="nb">str</span> <span class="c1"># discharge time for slot 1
</span>    <span class="n">slot1_discharge_current</span><span class="p">:</span> <span class="n">BatteryCurrent</span> <span class="c1"># discharge current for slot 1
</span>    <span class="n">hvac_heating_mode</span><span class="p">:</span> <span class="nb">str</span> <span class="c1"># heating mode
</span>    <span class="n">hourly_price</span><span class="p">:</span> <span class="n">EnergyPrice</span> <span class="c1"># current hourly energy price
</span>    <span class="n">pv_forecast_today</span><span class="p">:</span> <span class="nb">list</span> <span class="c1"># today's PV forecast
</span>    <span class="n">pv_forecast_tomorrow</span><span class="p">:</span> <span class="nb">list</span> <span class="c1"># tomorrow's PV forecast
</span>    <span class="n">weather_forecast</span><span class="p">:</span> <span class="nb">dict</span> <span class="o">|</span> <span class="bp">None</span> <span class="c1"># weather forecast data
</span>    <span class="n">price_forecast</span><span class="p">:</span> <span class="nb">list</span> <span class="o">|</span> <span class="bp">None</span> <span class="c1"># energy price forecast
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>Like a tip of the iceberg, configuration and state should give you an idea what the system takes into account when making autonomous decisions 🤔
The only manual input is the eco/away mode toggles.</p>

<h3 id="heating-cooling-and-domestic-hot-water">Heating, cooling and domestic hot water</h3>

<p>The second app manages my heat pump: domestic hot water and heating or cooling.
Nothing too fancy, just periodic control and reaction to eco mode and heating/cooling mode changes.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="rouge-code"><pre><span class="kn">import</span> <span class="n">appdaemon.plugins.hass.hassapi</span> <span class="k">as</span> <span class="n">hass</span>

<span class="k">class</span> <span class="nc">HvacApp</span><span class="p">(</span><span class="n">hass</span><span class="p">.</span><span class="n">Hass</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">self</span><span class="p">.</span><span class="n">hvac</span> <span class="o">=</span> <span class="nc">Hvac</span><span class="p">(</span>
            <span class="n">configuration</span><span class="o">=</span><span class="n">configuration</span><span class="p">,</span>
            <span class="n">state_factory</span><span class="o">=</span><span class="n">state_factory</span><span class="p">,</span>
            <span class="n">dhw_estimator</span><span class="o">=</span><span class="nc">DhwEstimator</span><span class="p">(...),</span>
            <span class="n">heating_estimator</span><span class="o">=</span><span class="nc">HeatingEstimator</span><span class="p">(...),</span>
            <span class="n">cooling_estimator</span><span class="o">=</span><span class="nc">CoolingEstimator</span><span class="p">(...),</span>
        <span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">run_every</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">control_scheduled</span><span class="p">,</span> <span class="sh">"</span><span class="s">00:00:00</span><span class="sh">"</span><span class="p">,</span> <span class="mi">5</span> <span class="o">*</span> <span class="mi">60</span><span class="p">)</span>

        <span class="n">self</span><span class="p">.</span><span class="nf">listen_state</span><span class="p">(</span>
            <span class="n">self</span><span class="p">.</span><span class="n">control_triggered</span><span class="p">,</span>
            <span class="p">[</span><span class="n">ECO_MODE_ENTITY</span><span class="p">,</span> <span class="n">HEATING_ENTITY</span><span class="p">,</span>  <span class="n">COOLING_ENTITY</span><span class="p">],</span>
        <span class="p">)</span>

    <span class="k">def</span> <span class="nf">control_scheduled</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">:</span> <span class="nb">dict</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">self</span><span class="p">.</span><span class="n">hvac</span><span class="p">.</span><span class="nf">control</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="nf">get_now</span><span class="p">())</span>

    <span class="k">def</span> <span class="nf">control_triggered</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">entity</span><span class="p">,</span> <span class="n">attribute</span><span class="p">,</span> <span class="n">old</span><span class="p">,</span> <span class="n">new</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">self</span><span class="p">.</span><span class="n">hvac</span><span class="p">.</span><span class="nf">control</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="nf">get_now</span><span class="p">())</span>        
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Again the most interesting part is the static configuration and dynamic state of the <code class="language-plaintext highlighter-rouge">Hvac</code> class.
Everything is automated, the only manual input are the eco mode toggle and temperature adjustment (+/- 1 degree).</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
</pre></td><td class="rouge-code"><pre><span class="n">configuration</span> <span class="o">=</span> <span class="nc">HvacConfiguration</span><span class="p">(</span>
    <span class="c1"># domestic hot water temperature
</span>    <span class="n">dhw_temp</span><span class="o">=</span><span class="nc">Celsius</span><span class="p">(</span><span class="mf">48.0</span><span class="p">),</span>
    <span class="c1"># domestic hot water temperature in eco mode
</span>    <span class="n">dhw_temp_eco</span><span class="o">=</span><span class="nc">Celsius</span><span class="p">(</span><span class="mf">40.0</span><span class="p">),</span>
    <span class="c1"># when to start boosting DHW depends on temperature difference
</span>    <span class="n">dhw_delta_temp</span><span class="o">=</span><span class="nc">Celsius</span><span class="p">(</span><span class="mf">6.0</span><span class="p">),</span>
    <span class="c1"># 5 minutes after low tariff starts to avoid clocks drift issues
</span>    <span class="n">dhw_boost_start</span><span class="o">=</span><span class="n">time</span><span class="p">.</span><span class="nf">fromisoformat</span><span class="p">(</span><span class="sh">"</span><span class="s">13:05:00</span><span class="sh">"</span><span class="p">),</span>
    <span class="c1"># 5 minutes before high tariff starts to avoid clocks drift issues
</span>    <span class="n">dhw_boost_end</span><span class="o">=</span><span class="n">time</span><span class="p">.</span><span class="nf">fromisoformat</span><span class="p">(</span><span class="sh">"</span><span class="s">15:55:00</span><span class="sh">"</span><span class="p">),</span>
    <span class="c1"># heating temperature
</span>    <span class="n">heating_temp</span><span class="o">=</span><span class="nc">Celsius</span><span class="p">(</span><span class="mf">20.0</span><span class="p">),</span>
    <span class="c1"># heating temperature in eco mode
</span>    <span class="n">heating_temp_eco</span><span class="o">=</span><span class="nc">Celsius</span><span class="p">(</span><span class="mf">18.0</span><span class="p">),</span>
    <span class="c1"># heating boost delta
</span>    <span class="n">heating_boost_delta_temp</span><span class="o">=</span><span class="nc">Celsius</span><span class="p">(</span><span class="mf">1.0</span><span class="p">),</span>
    <span class="c1"># heating boost delta in eco mode
</span>    <span class="n">heating_boost_delta_temp_eco</span><span class="o">=</span><span class="nc">Celsius</span><span class="p">(</span><span class="mf">2.0</span><span class="p">),</span>
    <span class="c1"># 5 minutes after low tariff starts to avoid clocks drift issues
</span>    <span class="n">heating_boost_time_start_eco_on</span><span class="o">=</span><span class="n">time</span><span class="p">.</span><span class="nf">fromisoformat</span><span class="p">(</span><span class="sh">"</span><span class="s">22:05:00</span><span class="sh">"</span><span class="p">),</span>
    <span class="c1"># 15 minutes before high tariff starts because stop heating takes longer
</span>    <span class="n">heating_boost_time_end_eco_on</span><span class="o">=</span><span class="n">time</span><span class="p">.</span><span class="nf">fromisoformat</span><span class="p">(</span><span class="sh">"</span><span class="s">06:45:00</span><span class="sh">"</span><span class="p">),</span>
    <span class="c1"># 1 hour before wake up time
</span>    <span class="n">heating_boost_time_start_eco_off</span><span class="o">=</span><span class="n">time</span><span class="p">.</span><span class="nf">fromisoformat</span><span class="p">(</span><span class="sh">"</span><span class="s">05:00:00</span><span class="sh">"</span><span class="p">),</span>
    <span class="c1"># 1 hour before bed time
</span>    <span class="n">heating_boost_time_end_eco_off</span><span class="o">=</span><span class="n">time</span><span class="p">.</span><span class="nf">fromisoformat</span><span class="p">(</span><span class="sh">"</span><span class="s">21:00:00</span><span class="sh">"</span><span class="p">),</span>
    <span class="c1"># cooling temperature
</span>    <span class="n">cooling_temp</span><span class="o">=</span><span class="nc">Celsius</span><span class="p">(</span><span class="mf">24.0</span><span class="p">),</span>
    <span class="c1"># cooling temperature in eco mode
</span>    <span class="n">cooling_temp_eco</span><span class="o">=</span><span class="nc">Celsius</span><span class="p">(</span><span class="mf">26.0</span><span class="p">),</span>
    <span class="c1"># cooling boost delta
</span>    <span class="n">cooling_boost_delta_temp</span><span class="o">=</span><span class="nc">Celsius</span><span class="p">(</span><span class="mf">2.0</span><span class="p">),</span>
    <span class="c1"># cooling boost delta in eco mode
</span>    <span class="n">cooling_boost_delta_temp_eco</span><span class="o">=</span><span class="nc">Celsius</span><span class="p">(</span><span class="mf">2.0</span><span class="p">),</span>
    <span class="c1"># cool when there is plenty of solar energy
</span>    <span class="n">cooling_boost_time_start_eco_on</span><span class="o">=</span><span class="n">time</span><span class="p">.</span><span class="nf">fromisoformat</span><span class="p">(</span><span class="sh">"</span><span class="s">12:00:00</span><span class="sh">"</span><span class="p">),</span>
    <span class="n">cooling_boost_time_end_eco_on</span><span class="o">=</span><span class="n">time</span><span class="p">.</span><span class="nf">fromisoformat</span><span class="p">(</span><span class="sh">"</span><span class="s">16:00:00</span><span class="sh">"</span><span class="p">),</span>
    <span class="c1"># extends cooling period a bit when eco mode is off
</span>    <span class="n">cooling_boost_time_start_eco_off</span><span class="o">=</span><span class="n">time</span><span class="p">.</span><span class="nf">fromisoformat</span><span class="p">(</span><span class="sh">"</span><span class="s">10:00:00</span><span class="sh">"</span><span class="p">),</span>
    <span class="n">cooling_boost_time_end_eco_off</span><span class="o">=</span><span class="n">time</span><span class="p">.</span><span class="nf">fromisoformat</span><span class="p">(</span><span class="sh">"</span><span class="s">18:00:00</span><span class="sh">"</span><span class="p">),</span>
<span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="k">class</span> <span class="nc">HvacState</span><span class="p">:</span>
    <span class="n">is_eco_mode</span><span class="p">:</span> <span class="nb">bool</span>  <span class="c1"># eco mode status
</span>    <span class="n">dhw_actual_temperature</span><span class="p">:</span> <span class="n">Celsius</span>  <span class="c1"># actual domestic hot water temperature
</span>    <span class="n">dhw_target_temperature</span><span class="p">:</span> <span class="n">Celsius</span>  <span class="c1"># target domestic hot water temperature
</span>    <span class="n">indoor_actual_temperature</span><span class="p">:</span> <span class="n">Celsius</span>  <span class="c1"># actual indoor temperature
</span>    <span class="n">heating_target_temperature</span><span class="p">:</span> <span class="n">Celsius</span>  <span class="c1"># target heating temperature
</span>    <span class="n">heating_mode</span><span class="p">:</span> <span class="nb">str</span>  <span class="c1"># heating mode
</span>    <span class="n">cooling_target_temperature</span><span class="p">:</span> <span class="n">Celsius</span>  <span class="c1"># target cooling temperature
</span>    <span class="n">cooling_mode</span><span class="p">:</span> <span class="nb">str</span>  <span class="c1"># cooling mode
</span>    <span class="n">temperature_adjustment</span><span class="p">:</span> <span class="n">Celsius</span>  <span class="c1"># temperature adjustment, +-1 degree
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>If you’d like to see the complete implementation, explore the source on GitHub: <a href="https://github.com/mkuthan/home-assistant-appdaemon">https://github.com/mkuthan/home-assistant-appdaemon</a>. The main branch contains the production code I run at home.</p>

<h2 id="python-goodies">Python goodies</h2>

<p>Let’s move to the second part of the post, where I describe some interesting Python techniques I applied in the project.</p>

<h3 id="typing">Typing</h3>

<p>As a seasoned Java and Scala developer, I appreciate strong typing.
In Python typing is optional, but I decided to use it extensively to check it’s maturity.
I configured my build and CI pipeline to use <a href="https://github.com/microsoft/pyright">pyright</a> from Microsoft and <a href="https://github.com/astral-sh/ty">ty</a> from Astral for type checking.</p>

<p>“Pyright” has the advantage of being fully compatible with Pylance in VS Code. However, I found “ty” to be blazingly fast — like other Astral tools such as ruff. The Astral tool is the clear winner in this comparison.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span><span class="nb">time </span>pyright
Found 95 <span class="nb">source </span>files
0 errors, 0 warnings, 0 informations

real    0m2.661s
user    0m3.672s
sys     0m0.259s
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span><span class="nb">time </span>ty check
INFO Indexed 95 file<span class="o">(</span>s<span class="o">)</span> <span class="k">in </span>0.003s
All checks passed!

real    0m0.125s
user    0m0.250s
sys     0m0.059s
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="value-classes">Value classes</h3>

<p>Around 2013 I read Domain-Driven Design: Tackling Complexity in the Heart of Software by Eric Evans.
One of the key takeaways was to use value objects to represent domain concepts instead of bunch of primitive types, like float for energy, temperature, etc.
I implemented value classes in Python using the <code class="language-plaintext highlighter-rouge">@dataclass(frozen=True)</code> decorator and a set of dunder (magic) methods for seamless integration with the Python SDK. For example:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
</pre></td><td class="rouge-code"><pre><span class="nd">@dataclass</span><span class="p">(</span><span class="n">frozen</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">order</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">EnergyKwh</span><span class="p">:</span>
    <span class="n">_ZERO_VALUE</span><span class="p">:</span> <span class="n">ClassVar</span><span class="p">[</span><span class="nb">float</span><span class="p">]</span> <span class="o">=</span> <span class="mf">0.0</span>

    <span class="n">value</span><span class="p">:</span> <span class="nb">float</span>

    <span class="k">def</span> <span class="nf">__add__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">other</span><span class="p">:</span> <span class="sh">"</span><span class="s">EnergyKwh</span><span class="sh">"</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="sh">"</span><span class="s">EnergyKwh</span><span class="sh">"</span><span class="p">:</span>
        <span class="k">return</span> <span class="nc">EnergyKwh</span><span class="p">(</span><span class="n">value</span><span class="o">=</span><span class="n">self</span><span class="p">.</span><span class="n">value</span> <span class="o">+</span> <span class="n">other</span><span class="p">.</span><span class="n">value</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">__sub__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">other</span><span class="p">:</span> <span class="sh">"</span><span class="s">EnergyKwh</span><span class="sh">"</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="sh">"</span><span class="s">EnergyKwh</span><span class="sh">"</span><span class="p">:</span>
        <span class="k">return</span> <span class="nc">EnergyKwh</span><span class="p">(</span><span class="n">value</span><span class="o">=</span><span class="n">self</span><span class="p">.</span><span class="n">value</span> <span class="o">-</span> <span class="n">other</span><span class="p">.</span><span class="n">value</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">__truediv__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">other</span><span class="p">:</span> <span class="sh">"</span><span class="s">EnergyKwh</span><span class="sh">"</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">other</span> <span class="o">==</span> <span class="n">ENERGY_KWH_ZERO</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nc">ValueError</span><span class="p">(</span><span class="sh">"</span><span class="s">Cannot divide by zero energy</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">value</span> <span class="o">/</span> <span class="n">other</span><span class="p">.</span><span class="n">value</span>

    <span class="k">def</span> <span class="nf">__neg__</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="sh">"</span><span class="s">EnergyKwh</span><span class="sh">"</span><span class="p">:</span>
        <span class="k">return</span> <span class="nc">EnergyKwh</span><span class="p">(</span><span class="n">value</span><span class="o">=-</span><span class="n">self</span><span class="p">.</span><span class="n">value</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">__str__</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="k">return</span> <span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">self</span><span class="p">.</span><span class="n">value</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s">kWh</span><span class="sh">"</span>


<span class="n">ENERGY_KWH_ZERO</span> <span class="o">=</span> <span class="nc">EnergyKwh</span><span class="p">(</span><span class="n">EnergyKwh</span><span class="p">.</span><span class="n">_ZERO_VALUE</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
</pre></td><td class="rouge-code"><pre><span class="k">class</span> <span class="nc">Celsius</span><span class="p">:</span>
    <span class="n">_ZERO_VALUE</span><span class="p">:</span> <span class="n">ClassVar</span><span class="p">[</span><span class="nb">float</span><span class="p">]</span> <span class="o">=</span> <span class="mf">0.0</span>

    <span class="n">value</span><span class="p">:</span> <span class="nb">float</span>

    <span class="k">def</span> <span class="nf">__add__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">other</span><span class="p">:</span> <span class="sh">"</span><span class="s">Celsius</span><span class="sh">"</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="sh">"</span><span class="s">Celsius</span><span class="sh">"</span><span class="p">:</span>
        <span class="k">return</span> <span class="nc">Celsius</span><span class="p">(</span><span class="n">value</span><span class="o">=</span><span class="n">self</span><span class="p">.</span><span class="n">value</span> <span class="o">+</span> <span class="n">other</span><span class="p">.</span><span class="n">value</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">__sub__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">other</span><span class="p">:</span> <span class="sh">"</span><span class="s">Celsius</span><span class="sh">"</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="sh">"</span><span class="s">Celsius</span><span class="sh">"</span><span class="p">:</span>
        <span class="k">return</span> <span class="nc">Celsius</span><span class="p">(</span><span class="n">value</span><span class="o">=</span><span class="n">self</span><span class="p">.</span><span class="n">value</span> <span class="o">-</span> <span class="n">other</span><span class="p">.</span><span class="n">value</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">__mul__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">other</span><span class="p">:</span> <span class="nb">float</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="sh">"</span><span class="s">Celsius</span><span class="sh">"</span><span class="p">:</span>
        <span class="k">return</span> <span class="nc">Celsius</span><span class="p">(</span><span class="n">value</span><span class="o">=</span><span class="n">self</span><span class="p">.</span><span class="n">value</span> <span class="o">*</span> <span class="n">other</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">__truediv__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">other</span><span class="p">:</span> <span class="sh">"</span><span class="s">Celsius</span><span class="sh">"</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">other</span> <span class="o">==</span> <span class="n">CELSIUS_ZERO</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nc">ValueError</span><span class="p">(</span><span class="sh">"</span><span class="s">Cannot divide by zero temperature</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">value</span> <span class="o">/</span> <span class="n">other</span><span class="p">.</span><span class="n">value</span>

    <span class="k">def</span> <span class="nf">__round__</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="sh">"</span><span class="s">Celsius</span><span class="sh">"</span><span class="p">:</span>
        <span class="k">return</span> <span class="nc">Celsius</span><span class="p">(</span><span class="n">value</span><span class="o">=</span><span class="nf">floor</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">value</span> <span class="o">+</span> <span class="mf">0.5</span><span class="p">))</span>

    <span class="k">def</span> <span class="nf">__str__</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="k">return</span> <span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">self</span><span class="p">.</span><span class="n">value</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s">°C</span><span class="sh">"</span>


<span class="n">CELSIUS_ZERO</span> <span class="o">=</span> <span class="nc">Celsius</span><span class="p">(</span><span class="n">Celsius</span><span class="p">.</span><span class="n">_ZERO_VALUE</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
</pre></td><td class="rouge-code"><pre><span class="nd">@dataclass</span><span class="p">(</span><span class="n">frozen</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">order</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">BatterySoc</span><span class="p">:</span>
    <span class="n">_MIN_VALUE</span><span class="p">:</span> <span class="n">ClassVar</span><span class="p">[</span><span class="nb">float</span><span class="p">]</span> <span class="o">=</span> <span class="mf">0.0</span>
    <span class="n">_MAX_VALUE</span><span class="p">:</span> <span class="n">ClassVar</span><span class="p">[</span><span class="nb">float</span><span class="p">]</span> <span class="o">=</span> <span class="mf">100.0</span>

    <span class="n">value</span><span class="p">:</span> <span class="nb">float</span>

    <span class="k">def</span> <span class="nf">__post_init__</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">self</span><span class="p">.</span><span class="n">_MIN_VALUE</span> <span class="o">&lt;=</span> <span class="n">self</span><span class="p">.</span><span class="n">value</span> <span class="o">&lt;=</span> <span class="n">self</span><span class="p">.</span><span class="n">_MAX_VALUE</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nc">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Battery SOC must be between </span><span class="si">{</span><span class="n">self</span><span class="p">.</span><span class="n">_MIN_VALUE</span><span class="si">}</span><span class="s"> and </span><span class="si">{</span><span class="n">self</span><span class="p">.</span><span class="n">_MAX_VALUE</span><span class="si">}</span><span class="s">, got </span><span class="si">{</span><span class="n">self</span><span class="p">.</span><span class="n">value</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">__add__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">other</span><span class="p">:</span> <span class="sh">"</span><span class="s">BatterySoc</span><span class="sh">"</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="sh">"</span><span class="s">BatterySoc</span><span class="sh">"</span><span class="p">:</span>
        <span class="k">return</span> <span class="nc">BatterySoc</span><span class="p">(</span><span class="n">value</span><span class="o">=</span><span class="nf">min</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">value</span> <span class="o">+</span> <span class="n">other</span><span class="p">.</span><span class="n">value</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">_MAX_VALUE</span><span class="p">))</span>

    <span class="k">def</span> <span class="nf">__sub__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">other</span><span class="p">:</span> <span class="sh">"</span><span class="s">BatterySoc</span><span class="sh">"</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="sh">"</span><span class="s">BatterySoc</span><span class="sh">"</span><span class="p">:</span>
        <span class="k">return</span> <span class="nc">BatterySoc</span><span class="p">(</span><span class="n">value</span><span class="o">=</span><span class="nf">max</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">value</span> <span class="o">-</span> <span class="n">other</span><span class="p">.</span><span class="n">value</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">_MIN_VALUE</span><span class="p">))</span>

    <span class="k">def</span> <span class="nf">__round__</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="sh">"</span><span class="s">BatterySoc</span><span class="sh">"</span><span class="p">:</span>
        <span class="k">return</span> <span class="nc">BatterySoc</span><span class="p">(</span><span class="n">value</span><span class="o">=</span><span class="nf">floor</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">value</span> <span class="o">+</span> <span class="mf">0.5</span><span class="p">))</span>

    <span class="k">def</span> <span class="nf">__str__</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="k">return</span> <span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">self</span><span class="p">.</span><span class="n">value</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s">%</span><span class="sh">"</span>


<span class="n">BATTERY_SOC_MIN</span> <span class="o">=</span> <span class="nc">BatterySoc</span><span class="p">(</span><span class="n">BatterySoc</span><span class="p">.</span><span class="n">_MIN_VALUE</span><span class="p">)</span>
<span class="n">BATTERY_SOC_MAX</span> <span class="o">=</span> <span class="nc">BatterySoc</span><span class="p">(</span><span class="n">BatterySoc</span><span class="p">.</span><span class="n">_MAX_VALUE</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>With value classes I gained several benefits:</p>

<ul>
  <li>With dataclass you get boilerplate code generation for free (<code class="language-plaintext highlighter-rouge">__init__</code>, <code class="language-plaintext highlighter-rouge">__eq__</code>, etc.)</li>
  <li>With frozen dataclass you get immutability for free</li>
  <li>With order=True you get comparison operators for free</li>
  <li>The type checker warns if you try to add an EnergyKwh value to a Celsius temperature</li>
  <li>You can’t create invalid values — for example, BatterySoc is constrained to the 0–100% range</li>
  <li>You get consistent string representation across the application</li>
  <li>You can add domain-specific methods to value classes, e.g., rounding temperature values</li>
</ul>

<h3 id="protocols">Protocols</h3>

<p>AppDeamon support for testing is limited, so I had to extract logic from the framework-specific code and make it testable in isolation.
I decided to use duck typing with <code class="language-plaintext highlighter-rouge">Protocol</code> classes to define AppDaemon interfaces, for example:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="k">class</span> <span class="nc">AppdaemonService</span><span class="p">(</span><span class="n">Protocol</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">call_service</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">service</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="o">**</span><span class="n">data</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">object</span><span class="p">:</span> <span class="bp">...</span>
    
<span class="k">class</span> <span class="nc">AppdaemonState</span><span class="p">(</span><span class="n">Protocol</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">get_state</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">entity_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">attribute</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">object</span><span class="p">:</span> <span class="bp">...</span>

<span class="k">class</span> <span class="nc">AppdaemonLogger</span><span class="p">(</span><span class="n">Protocol</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">log</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">msg</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="n">level</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="nb">int</span> <span class="o">=</span> <span class="n">logging</span><span class="p">.</span><span class="n">INFO</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span> <span class="bp">...</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Those three protocols are all I needed to interact with AppDaemon framework in my applications!
The protocols initialization is straightforward because the protocols match the AppDaemon API:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="kn">import</span> <span class="n">appdaemon.plugins.hass.hassapi</span> <span class="k">as</span> <span class="n">hass</span>

<span class="k">class</span> <span class="nc">HvacApp</span><span class="p">(</span><span class="n">hass</span><span class="p">.</span><span class="n">Hass</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">appdaemon_logger</span> <span class="o">=</span> <span class="n">self</span>
        <span class="n">appdaemon_state</span> <span class="o">=</span> <span class="n">self</span>
        <span class="n">appdaemon_service</span> <span class="o">=</span> <span class="n">self</span>
        <span class="p">(...)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>For testing I define mock implementations of those protocols in the top level <code class="language-plaintext highlighter-rouge">conftest.py</code> file.
The fixtures are automatically available in all test modules without the need to monkey-patch AppDaemon classes.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="nd">@pytest.fixture</span>
<span class="k">def</span> <span class="nf">mock_appdaemon_logger</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="n">Mock</span><span class="p">:</span>
    <span class="k">return</span> <span class="nc">Mock</span><span class="p">()</span>


<span class="nd">@pytest.fixture</span>
<span class="k">def</span> <span class="nf">mock_appdaemon_state</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="n">Mock</span><span class="p">:</span>
    <span class="k">return</span> <span class="nc">Mock</span><span class="p">()</span>


<span class="nd">@pytest.fixture</span>
<span class="k">def</span> <span class="nf">mock_appdaemon_service</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="n">Mock</span><span class="p">:</span>
    <span class="k">return</span> <span class="nc">Mock</span><span class="p">()</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="for-comprehensions">For comprehensions</h3>

<p>I love Scala’s for-comprehensions, and I also enjoy their Python equivalent. 
For example, I implemented separate classes for different consumption-forecast strategies and combined them with a composite class.
Using a nested for clause inside a single list comprehension is an elegant way to implement the composite design pattern.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">class</span> <span class="nc">ConsumptionForecast</span><span class="p">(</span><span class="n">Protocol</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">hourly</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">period_start</span><span class="p">:</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">period_hours</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="n">HourlyConsumptionEnergy</span><span class="p">]:</span> <span class="bp">...</span>

<span class="k">class</span> <span class="nc">ConsumptionForecastComposite</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="o">*</span><span class="n">components</span><span class="p">:</span> <span class="n">ConsumptionForecast</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">self</span><span class="p">.</span><span class="n">components</span> <span class="o">=</span> <span class="n">components</span>

    <span class="k">def</span> <span class="nf">hourly</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">period_start</span><span class="p">:</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">period_hours</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="n">HourlyConsumptionEnergy</span><span class="p">]:</span>
        <span class="k">return</span> <span class="p">[</span><span class="n">item</span> <span class="k">for</span> <span class="n">component</span> <span class="ow">in</span> <span class="n">self</span><span class="p">.</span><span class="n">components</span> <span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">component</span><span class="p">.</span><span class="nf">hourly</span><span class="p">(</span><span class="n">period_start</span><span class="p">,</span> <span class="n">period_hours</span><span class="p">)]</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="interesting-algorithms">Interesting algorithms</h2>

<p>Algorithms have never been my top strength but with help from GitHub Copilot and a basic grasp of intuition, math and physics, I implemented a few useful ones.</p>

<h3 id="find-the-continuous-time-window-with-maximum-revenue">Find the continuous time window with maximum revenue</h3>

<p>This algorithm evaluates all possible starting minutes to find the optimal battery discharge window with maximum revenue.
It uses a variable-length sliding window approach with time complexity <code class="language-plaintext highlighter-rouge">O(n * d)</code> where “n” is the number of periods and “d” is max_duration_minutes.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
</pre></td><td class="rouge-code"><pre><span class="k">def</span> <span class="nf">find_max_revenue_period</span><span class="p">(</span>
    <span class="n">hourly_prices</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">HourlyPrice</span><span class="p">],</span> <span class="n">min_price_threshold</span><span class="p">:</span> <span class="n">EnergyPrice</span><span class="p">,</span> <span class="n">max_duration_minutes</span><span class="p">:</span> <span class="nb">int</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">tuple</span><span class="p">[</span><span class="n">EnergyPrice</span><span class="p">,</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">datetime</span><span class="p">]</span> <span class="o">|</span> <span class="bp">None</span><span class="p">:</span>
    <span class="k">if</span> <span class="n">max_duration_minutes</span> <span class="o">&lt;</span> <span class="mi">1</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nc">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">max_duration_minutes must be at least 1, got </span><span class="si">{</span><span class="n">max_duration_minutes</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

    <span class="k">if</span> <span class="ow">not</span> <span class="n">hourly_prices</span><span class="p">:</span>
        <span class="k">return</span> <span class="bp">None</span>

    <span class="k">if</span> <span class="ow">not</span> <span class="nf">any</span><span class="p">(</span><span class="n">hourly_price</span><span class="p">.</span><span class="n">price</span> <span class="o">&gt;=</span> <span class="n">min_price_threshold</span> <span class="k">for</span> <span class="n">hourly_price</span> <span class="ow">in</span> <span class="n">hourly_prices</span><span class="p">):</span>
        <span class="k">return</span> <span class="bp">None</span>

    <span class="n">period_duration_minutes</span> <span class="o">=</span> <span class="mi">60</span>

    <span class="n">max_revenue</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="n">best_start_time</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="n">best_end_time</span> <span class="o">=</span> <span class="bp">None</span>

    <span class="k">for</span> <span class="n">start_hour_idx</span><span class="p">,</span> <span class="n">start_hour</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="n">hourly_prices</span><span class="p">):</span>
        <span class="c1"># Skip if period doesn't meet price threshold
</span>        <span class="k">if</span> <span class="n">start_hour</span><span class="p">.</span><span class="n">price</span> <span class="o">&lt;</span> <span class="n">min_price_threshold</span><span class="p">:</span>
            <span class="k">continue</span>

        <span class="k">for</span> <span class="n">start_offset_minutes</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">period_duration_minutes</span><span class="p">):</span>
            <span class="n">start_time</span> <span class="o">=</span> <span class="n">start_hour</span><span class="p">.</span><span class="n">period</span><span class="p">.</span><span class="n">start</span> <span class="o">+</span> <span class="nf">timedelta</span><span class="p">(</span><span class="n">minutes</span><span class="o">=</span><span class="n">start_offset_minutes</span><span class="p">)</span>

            <span class="c1"># Calculate revenue for a window of max_duration_minutes from this start
</span>            <span class="n">revenue</span> <span class="o">=</span> <span class="n">start_hour</span><span class="p">.</span><span class="n">price</span><span class="p">.</span><span class="nf">zeroed</span><span class="p">()</span>
            <span class="n">minutes_covered</span> <span class="o">=</span> <span class="mi">0</span>

            <span class="c1"># Iterate through periods that this window spans
</span>            <span class="n">current_period_idx</span> <span class="o">=</span> <span class="n">start_hour_idx</span>
            <span class="n">minutes_into_current_period</span> <span class="o">=</span> <span class="n">start_offset_minutes</span>

            <span class="k">while</span> <span class="n">minutes_covered</span> <span class="o">&lt;</span> <span class="n">max_duration_minutes</span> <span class="ow">and</span> <span class="n">current_period_idx</span> <span class="o">&lt;</span> <span class="nf">len</span><span class="p">(</span><span class="n">hourly_prices</span><span class="p">):</span>
                <span class="n">current_hourly_price</span> <span class="o">=</span> <span class="n">hourly_prices</span><span class="p">[</span><span class="n">current_period_idx</span><span class="p">]</span>

                <span class="c1"># Check threshold - stop if this period doesn't meet it
</span>                <span class="k">if</span> <span class="n">current_hourly_price</span><span class="p">.</span><span class="n">price</span> <span class="o">&lt;</span> <span class="n">min_price_threshold</span><span class="p">:</span>
                    <span class="k">break</span>

                <span class="c1"># Calculate how many minutes to take from this period
</span>                <span class="n">minutes_available_in_period</span> <span class="o">=</span> <span class="n">period_duration_minutes</span> <span class="o">-</span> <span class="n">minutes_into_current_period</span>
                <span class="n">minutes_needed</span> <span class="o">=</span> <span class="n">max_duration_minutes</span> <span class="o">-</span> <span class="n">minutes_covered</span>
                <span class="n">minutes_to_take</span> <span class="o">=</span> <span class="nf">min</span><span class="p">(</span><span class="n">minutes_available_in_period</span><span class="p">,</span> <span class="n">minutes_needed</span><span class="p">)</span>

                <span class="c1"># Add revenue
</span>                <span class="n">price_per_minute</span> <span class="o">=</span> <span class="n">current_hourly_price</span><span class="p">.</span><span class="n">price</span> <span class="o">/</span> <span class="nc">Decimal</span><span class="p">(</span><span class="n">period_duration_minutes</span><span class="p">)</span>
                <span class="n">revenue</span> <span class="o">+=</span> <span class="n">price_per_minute</span> <span class="o">*</span> <span class="nc">Decimal</span><span class="p">(</span><span class="n">minutes_to_take</span><span class="p">)</span>
                <span class="n">minutes_covered</span> <span class="o">+=</span> <span class="n">minutes_to_take</span>

                <span class="c1"># Move to next period
</span>                <span class="n">current_period_idx</span> <span class="o">+=</span> <span class="mi">1</span>
                <span class="n">minutes_into_current_period</span> <span class="o">=</span> <span class="mi">0</span>

            <span class="c1"># Check if this is a valid and better solution
</span>            <span class="k">if</span> <span class="n">minutes_covered</span> <span class="o">&gt;=</span> <span class="mi">1</span><span class="p">:</span>
                <span class="n">end_time</span> <span class="o">=</span> <span class="n">start_time</span> <span class="o">+</span> <span class="nf">timedelta</span><span class="p">(</span><span class="n">minutes</span><span class="o">=</span><span class="n">minutes_covered</span><span class="p">)</span>

                <span class="k">if</span> <span class="n">max_revenue</span> <span class="ow">is</span> <span class="bp">None</span> <span class="ow">or</span> <span class="n">revenue</span> <span class="o">&gt;</span> <span class="n">max_revenue</span><span class="p">:</span>
                    <span class="n">max_revenue</span> <span class="o">=</span> <span class="n">revenue</span>
                    <span class="n">best_start_time</span> <span class="o">=</span> <span class="n">start_time</span>
                    <span class="n">best_end_time</span> <span class="o">=</span> <span class="n">end_time</span>

    <span class="k">if</span> <span class="n">max_revenue</span> <span class="ow">is</span> <span class="bp">None</span> <span class="ow">or</span> <span class="n">best_start_time</span> <span class="ow">is</span> <span class="bp">None</span> <span class="ow">or</span> <span class="n">best_end_time</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
        <span class="k">return</span> <span class="bp">None</span>

    <span class="nf">return </span><span class="p">(</span><span class="n">max_revenue</span><span class="p">,</span> <span class="n">best_start_time</span><span class="p">,</span> <span class="n">best_end_time</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>For the following hourly prices:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="p">[</span>
    <span class="p">(</span><span class="sh">"</span><span class="s">00:00:00</span><span class="sh">"</span><span class="p">,</span> <span class="mi">100</span><span class="p">),</span>
    <span class="p">(</span><span class="sh">"</span><span class="s">01:00:00</span><span class="sh">"</span><span class="p">,</span> <span class="mi">150</span><span class="p">),</span>
    <span class="p">(</span><span class="sh">"</span><span class="s">02:00:00</span><span class="sh">"</span><span class="p">,</span> <span class="mi">200</span><span class="p">),</span>
    <span class="p">(</span><span class="sh">"</span><span class="s">03:00:00</span><span class="sh">"</span><span class="p">,</span> <span class="mi">120</span><span class="p">),</span>
<span class="p">]</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The maximum revenue for a 105 minutes window with minimum price threshold of “100” is <code class="language-plaintext highlighter-rouge">150 * 45 / 60 + 200 = 312.5</code>, starting at “01:15:00” and ending at “03:00:00”.</p>

<h3 id="estimate-heating-energy-consumption">Estimate heating energy consumption</h3>

<p>This function estimates the electrical energy consumption of a heat pump by accounting for:</p>

<ul>
  <li>Heat loss proportional to indoor/outdoor temperature difference</li>
  <li>COP (Coefficient of Performance) variation with outdoor temperature</li>
  <li>COP degradation due to frosting cycles in specific temperature and humidity ranges</li>
</ul>

<p>The model assumes linear heat loss and uses empirically-derived adjustments for
real-world heat pump performance characteristics.</p>

<p>For example, for the outdoor temperature of 3.5°C, indoor temperature of 20°C, humidity of 100% with frosting penalty, COP at 7°C of 4.0, and heat loss coefficient of 0.18 kW/°C, the estimated heating energy consumption is approximately 0.987 kWh.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
</pre></td><td class="rouge-code"><pre><span class="n">_REFERENCE_TEMPERATURE</span> <span class="o">=</span> <span class="nc">Celsius</span><span class="p">(</span><span class="mf">7.0</span><span class="p">)</span>  <span class="c1"># temperature at which COP is rated
</span><span class="n">_COP_TEMPERATURE_COEFFICIENT</span> <span class="o">=</span> <span class="mf">0.033</span>  <span class="c1"># COP change per degree Celsius
</span><span class="n">_COP_COEFFICIENT_MIN</span> <span class="o">=</span> <span class="mf">0.5</span>  <span class="c1"># minimum COP multiplier to prevent unrealistic values
</span>
<span class="c1"># Frosting cycle parameters
</span><span class="n">_FROSTING_TEMP_MIN</span> <span class="o">=</span> <span class="nc">Celsius</span><span class="p">(</span><span class="mf">0.0</span><span class="p">)</span>  <span class="c1"># lower bound for frosting conditions
</span><span class="n">_FROSTING_TEMP_MAX</span> <span class="o">=</span> <span class="nc">Celsius</span><span class="p">(</span><span class="mf">7.0</span><span class="p">)</span>  <span class="c1"># upper bound for frosting conditions
</span><span class="n">_FROSTING_TEMP_PEAK</span> <span class="o">=</span> <span class="nc">Celsius</span><span class="p">(</span><span class="mf">3.5</span><span class="p">)</span>  <span class="c1"># temperature with maximum frosting risk
</span><span class="n">_FROSTING_HUMIDITY_THRESHOLD</span> <span class="o">=</span> <span class="mf">70.0</span>  <span class="c1"># % - minimum humidity for frosting
</span><span class="n">_FROSTING_PENALTY_MAX</span> <span class="o">=</span> <span class="mf">0.15</span>  <span class="c1"># maximum COP reduction due to frosting cycles (15%)
</span>

<span class="k">def</span> <span class="nf">estimate_heating_energy_consumption</span><span class="p">(</span>
    <span class="n">t_out</span><span class="p">:</span> <span class="n">Celsius</span><span class="p">,</span>
    <span class="n">t_in</span><span class="p">:</span> <span class="n">Celsius</span><span class="p">,</span>
    <span class="n">humidity</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span>
    <span class="n">cop_at_7c</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span>
    <span class="n">h</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">EnergyKwh</span><span class="p">:</span>
    <span class="n">t_diff</span> <span class="o">=</span> <span class="n">t_in</span> <span class="o">-</span> <span class="n">t_out</span>

    <span class="k">if</span> <span class="n">t_diff</span><span class="p">.</span><span class="n">value</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
        <span class="n">heat_loss</span> <span class="o">=</span> <span class="n">h</span> <span class="o">*</span> <span class="n">t_diff</span><span class="p">.</span><span class="n">value</span>

        <span class="n">temperature_coefficient</span> <span class="o">=</span> <span class="nf">_temperature_coefficient</span><span class="p">(</span><span class="n">t_out</span><span class="p">)</span>
        <span class="n">frosting_penalty</span> <span class="o">=</span> <span class="nf">_frosting_penalty</span><span class="p">(</span><span class="n">t_out</span><span class="p">,</span> <span class="n">humidity</span><span class="p">)</span>
        <span class="n">adjusted_cop</span> <span class="o">=</span> <span class="n">cop_at_7c</span> <span class="o">*</span> <span class="n">temperature_coefficient</span> <span class="o">*</span> <span class="n">frosting_penalty</span>

        <span class="n">energy_consumption</span> <span class="o">=</span> <span class="nc">EnergyKwh</span><span class="p">(</span><span class="n">heat_loss</span> <span class="o">/</span> <span class="n">adjusted_cop</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="n">energy_consumption</span> <span class="o">=</span> <span class="n">ENERGY_KWH_ZERO</span>

    <span class="k">return</span> <span class="n">energy_consumption</span>


<span class="k">def</span> <span class="nf">_temperature_coefficient</span><span class="p">(</span>
    <span class="n">t_out</span><span class="p">:</span> <span class="n">Celsius</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="n">temp_delta</span> <span class="o">=</span> <span class="n">t_out</span> <span class="o">-</span> <span class="n">_REFERENCE_TEMPERATURE</span>
    <span class="n">coefficient</span> <span class="o">=</span> <span class="mf">1.0</span> <span class="o">+</span> <span class="p">(</span><span class="n">temp_delta</span><span class="p">.</span><span class="n">value</span> <span class="o">*</span> <span class="n">_COP_TEMPERATURE_COEFFICIENT</span><span class="p">)</span>

    <span class="k">return</span> <span class="nf">max</span><span class="p">(</span><span class="n">_COP_COEFFICIENT_MIN</span><span class="p">,</span> <span class="n">coefficient</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">_frosting_penalty</span><span class="p">(</span>
    <span class="n">t_out</span><span class="p">:</span> <span class="n">Celsius</span><span class="p">,</span>
    <span class="n">humidity</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="p">(</span><span class="n">_FROSTING_TEMP_MIN</span> <span class="o">&lt;=</span> <span class="n">t_out</span> <span class="o">&lt;=</span> <span class="n">_FROSTING_TEMP_MAX</span> <span class="ow">and</span> <span class="n">humidity</span> <span class="o">&gt;</span> <span class="n">_FROSTING_HUMIDITY_THRESHOLD</span><span class="p">):</span>
        <span class="k">return</span> <span class="mf">1.0</span>

    <span class="n">distance_from_peak</span> <span class="o">=</span> <span class="nf">abs</span><span class="p">(</span><span class="n">t_out</span><span class="p">.</span><span class="n">value</span> <span class="o">-</span> <span class="n">_FROSTING_TEMP_PEAK</span><span class="p">.</span><span class="n">value</span><span class="p">)</span>
    <span class="n">frosting_severity</span> <span class="o">=</span> <span class="p">(</span><span class="n">_FROSTING_TEMP_PEAK</span><span class="p">.</span><span class="n">value</span> <span class="o">-</span> <span class="n">distance_from_peak</span><span class="p">)</span> <span class="o">/</span> <span class="n">_FROSTING_TEMP_PEAK</span><span class="p">.</span><span class="n">value</span>

    <span class="n">humidity_factor</span> <span class="o">=</span> <span class="p">(</span><span class="n">humidity</span> <span class="o">-</span> <span class="n">_FROSTING_HUMIDITY_THRESHOLD</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="mf">100.0</span> <span class="o">-</span> <span class="n">_FROSTING_HUMIDITY_THRESHOLD</span><span class="p">)</span>

    <span class="n">penalty</span> <span class="o">=</span> <span class="n">_FROSTING_PENALTY_MAX</span> <span class="o">*</span> <span class="n">frosting_severity</span> <span class="o">*</span> <span class="n">humidity_factor</span>

    <span class="k">return</span> <span class="mf">1.0</span> <span class="o">-</span> <span class="n">penalty</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="calculate-maximum-cumulative-energy-deficit">Calculate maximum cumulative energy deficit</h3>

<p>To calculate the battery reserve SOC in the morning, I need to find the maximum cumulative energy deficit before next low tariff period.
For example, to survive this hypothetical morning the battery needs to cover 1.25 kWh deficit:</p>

<table>
  <thead>
    <tr>
      <th>hour</th>
      <th>production (kWh)</th>
      <th>consumption (kWh)</th>
      <th>cumulative deficit (kWh)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>7</td>
      <td>0.25</td>
      <td>0.5</td>
      <td>0.25</td>
    </tr>
    <tr>
      <td>8</td>
      <td>1.0</td>
      <td>2.0</td>
      <td><strong>1.25</strong></td>
    </tr>
    <tr>
      <td>9</td>
      <td>2.0</td>
      <td>1.0</td>
      <td>0.25</td>
    </tr>
    <tr>
      <td>10</td>
      <td>0.25</td>
      <td>0.5</td>
      <td>0.5</td>
    </tr>
  </tbody>
</table>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
</pre></td><td class="rouge-code"><pre><span class="k">def</span> <span class="nf">maximum_cumulative_deficit</span><span class="p">(</span>
    <span class="n">consumptions</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">HourlyConsumptionEnergy</span><span class="p">],</span> <span class="n">productions</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">HourlyProductionEnergy</span><span class="p">]</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">EnergyKwh</span><span class="p">:</span>
    <span class="n">net_energy_dict</span> <span class="o">=</span> <span class="p">{}</span>

    <span class="k">for</span> <span class="n">consumption</span> <span class="ow">in</span> <span class="n">consumptions</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">consumption</span><span class="p">.</span><span class="n">period</span> <span class="ow">in</span> <span class="n">net_energy_dict</span><span class="p">:</span>
            <span class="n">net_energy_dict</span><span class="p">[</span><span class="n">consumption</span><span class="p">.</span><span class="n">period</span><span class="p">]</span> <span class="o">-=</span> <span class="n">consumption</span><span class="p">.</span><span class="n">energy</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">net_energy_dict</span><span class="p">[</span><span class="n">consumption</span><span class="p">.</span><span class="n">period</span><span class="p">]</span> <span class="o">=</span> <span class="o">-</span><span class="n">consumption</span><span class="p">.</span><span class="n">energy</span>

    <span class="k">for</span> <span class="n">production</span> <span class="ow">in</span> <span class="n">productions</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">production</span><span class="p">.</span><span class="n">period</span> <span class="ow">in</span> <span class="n">net_energy_dict</span><span class="p">:</span>
            <span class="n">net_energy_dict</span><span class="p">[</span><span class="n">production</span><span class="p">.</span><span class="n">period</span><span class="p">]</span> <span class="o">+=</span> <span class="n">production</span><span class="p">.</span><span class="n">energy</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">net_energy_dict</span><span class="p">[</span><span class="n">production</span><span class="p">.</span><span class="n">period</span><span class="p">]</span> <span class="o">=</span> <span class="n">production</span><span class="p">.</span><span class="n">energy</span>

    <span class="n">net_energy_list</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span><span class="n">net_energy_dict</span><span class="p">.</span><span class="nf">items</span><span class="p">())</span>
    <span class="n">net_energy_list_sorted</span> <span class="o">=</span> <span class="nf">sorted</span><span class="p">(</span><span class="n">net_energy_list</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">n</span><span class="p">:</span> <span class="n">n</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">start</span><span class="p">)</span>

    <span class="n">cumulative_balance</span> <span class="o">=</span> <span class="n">ENERGY_KWH_ZERO</span>
    <span class="n">min_cumulative_balance</span> <span class="o">=</span> <span class="n">ENERGY_KWH_ZERO</span>

    <span class="k">for</span> <span class="n">net_energy</span> <span class="ow">in</span> <span class="n">net_energy_list_sorted</span><span class="p">:</span>
        <span class="n">cumulative_balance</span> <span class="o">=</span> <span class="n">cumulative_balance</span> <span class="o">+</span> <span class="n">net_energy</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>

        <span class="k">if</span> <span class="n">cumulative_balance</span> <span class="o">&lt;</span> <span class="n">min_cumulative_balance</span><span class="p">:</span>
            <span class="n">min_cumulative_balance</span> <span class="o">=</span> <span class="n">cumulative_balance</span>

    <span class="k">return</span> <span class="nf">max</span><span class="p">(</span><span class="o">-</span><span class="n">min_cumulative_balance</span><span class="p">,</span> <span class="n">ENERGY_KWH_ZERO</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="estimate-time-for-indoor-temperature-to-decay-from-start-to-end-temperature">Estimate time for indoor temperature to decay from start to end temperature</h3>

<p>This function estimates the time required for indoor temperature to decay from a starting temperature to an ending temperature, considering varying outdoor temperatures over time.
It uses Newton’s Law of Cooling with a decay rate constant derived from the building’s thermal properties.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
</pre></td><td class="rouge-code"><pre><span class="k">def</span> <span class="nf">estimate_temperature_decay_time</span><span class="p">(</span>
    <span class="n">temp_start</span><span class="p">:</span> <span class="n">Celsius</span><span class="p">,</span>
    <span class="n">temp_end</span><span class="p">:</span> <span class="n">Celsius</span><span class="p">,</span>
    <span class="n">hourly_weather</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">HourlyWeather</span><span class="p">],</span>
    <span class="n">decay_rate</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">timedelta</span><span class="p">:</span>
    <span class="k">if</span> <span class="n">decay_rate</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nc">ValueError</span><span class="p">(</span><span class="sh">"</span><span class="s">Decay rate must be positive</span><span class="sh">"</span><span class="p">)</span>

    <span class="n">temp_current</span> <span class="o">=</span> <span class="n">temp_start</span>
    <span class="n">temp_target</span> <span class="o">=</span> <span class="n">temp_end</span>

    <span class="n">total_hours</span> <span class="o">=</span> <span class="mf">0.0</span>

    <span class="k">for</span> <span class="n">weather</span> <span class="ow">in</span> <span class="n">hourly_weather</span><span class="p">:</span>
        <span class="n">temp_outdoor</span> <span class="o">=</span> <span class="n">weather</span><span class="p">.</span><span class="n">temperature</span>

        <span class="k">if</span> <span class="n">temp_current</span> <span class="o">&lt;=</span> <span class="n">temp_target</span><span class="p">:</span>
            <span class="k">break</span>

        <span class="k">if</span> <span class="n">temp_current</span> <span class="o">&lt;=</span> <span class="n">temp_outdoor</span><span class="p">:</span>
            <span class="k">break</span>

        <span class="n">temp_diff_start</span> <span class="o">=</span> <span class="n">temp_current</span> <span class="o">-</span> <span class="n">temp_outdoor</span>
        <span class="n">temp_after_hour</span> <span class="o">=</span> <span class="n">temp_outdoor</span> <span class="o">+</span> <span class="nc">Celsius</span><span class="p">(</span><span class="n">temp_diff_start</span><span class="p">.</span><span class="n">value</span> <span class="o">*</span> <span class="nf">exp</span><span class="p">(</span><span class="o">-</span><span class="n">decay_rate</span> <span class="o">*</span> <span class="mf">1.0</span><span class="p">))</span>

        <span class="k">if</span> <span class="n">temp_after_hour</span> <span class="o">&lt;=</span> <span class="n">temp_target</span><span class="p">:</span>
            <span class="n">fraction</span> <span class="o">=</span> <span class="nf">log</span><span class="p">(</span><span class="n">temp_diff_start</span> <span class="o">/</span> <span class="p">(</span><span class="n">temp_target</span> <span class="o">-</span> <span class="n">temp_outdoor</span><span class="p">))</span> <span class="o">/</span> <span class="n">decay_rate</span>
            <span class="n">total_hours</span> <span class="o">+=</span> <span class="n">fraction</span>
            <span class="k">break</span>

        <span class="n">temp_current</span> <span class="o">=</span> <span class="n">temp_after_hour</span>
        <span class="n">total_hours</span> <span class="o">+=</span> <span class="mf">1.0</span>

    <span class="k">return</span> <span class="nf">timedelta</span><span class="p">(</span><span class="n">hours</span><span class="o">=</span><span class="n">total_hours</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>For example, starting from 22.9°C to 21.3°C with constant outdoor temperature of 8.0°C and decay rate of 0.0226, the estimated time is approximately 5.03 hours.</p>

<h2 id="summary">Summary</h2>

<p>I had a lot of fun building this project and learning Python along the way.
During implementation I deliberately avoided some language features from the book like: multiple inheritance and generics, because I’m not a big fan.
I still plan to explore coroutines, async/await, and metaclasses in more depth.</p>

<p>Getting back to the point, since the initial deployment in early autumn:</p>

<ul>
  <li>The house stays warm and cozy.</li>
  <li>My family hasn’t complained about hot water.</li>
  <li>Most grid consumption now happens in low‑tariff periods: 90% of grid energy is used then (total cost 0.58 PLN/kWh), while only 10% is used during high‑tariff periods (total cost 1.06 PLN/kWh).</li>
  <li>Self‑consumption of solar energy was 59% in October and 70% in November.</li>
</ul>

<p>Don’t forget to add a ⭐️ to my project on GitHub if you find it useful!
<a href="https://github.com/mkuthan/home-assistant-appdaemon">https://github.com/mkuthan/home-assistant-appdaemon</a></p>]]></content><author><name>Marcin Kuthan</name></author><category term="DIY" /><category term="Homelab" /><category term="Python" /><category term="Software Engineering" /><summary type="html"><![CDATA[Recently I decided to learn Python seriously and studied Fluent Python: Clear, Concise, and Effective Programming by Luciano Ramalho. The hardest part was finding a project to apply the new skills. At work I mostly use Python to write small scripts and Apache Airflow DAGs. I wanted something more challenging with complex domain logic and real-world data ⚙️📊]]></summary></entry><entry><title type="html">Head First Python, Head First JavaScript – books review</title><link href="https://mkuthan.github.io/blog/2025/10/30/head-first-python-js/" rel="alternate" type="text/html" title="Head First Python, Head First JavaScript – books review" /><published>2025-10-30T00:00:00+00:00</published><updated>2025-10-30T00:00:00+00:00</updated><id>https://mkuthan.github.io/blog/2025/10/30/head-first-python-js</id><content type="html" xml:base="https://mkuthan.github.io/blog/2025/10/30/head-first-python-js/"><![CDATA[<p>A few months ago, my 13-year-old son and I were discussing his future high school options.
He’s really smart and interested in technology (programming, robotics, 3D printing, etc.) but extremely bored with traditional school subjects.
Finding a school that would fit his interests wasn’t easy, but fortunately my old friend <em>Orłoś</em> told me about <a href="https://technischools.com/">TechniSchools</a>.</p>

<p>After some deep research, we discovered that <em>TechniSchools</em> is a perfect match for him.
My son decided to apply next year, but first he needs to do some serious homework in programming and other technical subjects.
He already knows <em>Scratch</em> and a bit of <em>C</em> (<em>Arduino</em>), <em>Python</em>, and <em>MicroPython</em> (<em>Lego Mindstorms</em>), but I suggested he should learn web technologies as well (<em>HTML</em>, <em>CSS</em>, <em>JavaScript</em>).</p>

<p>He started with <a href="https://www.w3schools.com/">W3Schools</a> video tutorials, switched to VS Code with some Copilot help, and created a few simple web pages.
It was his way of learning by doing—typical for his generation, I assume.</p>

<p>But recently he asked me to recommend some books 📚 that would help him understand programming concepts better.
I was super happy 😊 because <strong>I know that reading books is the best way to learn programming fundamentals deeply</strong>.</p>

<p>I started searching for books, but most of them were… just boring 😴 (at least for a teenager).
Then I remembered the <em>Head First</em> series from O’Reilly.</p>

<ul>
  <li><a href="https://www.oreilly.com/library/view/head-first-python/9781491919521/">Head First Python</a> by Paul Barry</li>
  <li><a href="https://www.oreilly.com/library/view/head-first-javascript/9781492092515/">Head First JavaScript Programming</a> by Eric Freeman and Elisabeth Robson</li>
</ul>

<p><img src="/assets/images/2025-10-30-head-first-python-js/python_book_cover.jpg" alt="Head First Python" />
<img src="/assets/images/2025-10-30-head-first-python-js/js_book_cover.jpg" alt="Head First JavaScript" /></p>

<p>My son started with <em>Head First JavaScript</em> and I started leafing through <em>Head First Python</em>.
First, you read about learning psychology and why these books are a bit different from typical programming books: lots of pictures, funny stories, quizzes, exercises, etc.
Then you dive into the project-based learning approach, building simple applications step by step.
In the JavaScript book, you create a ship battle game; in the Python book, you build a web app for swimming club training recordings.</p>

<p>I’m really impressed by how cleverly the books smuggle in programming concepts and best practices I usually apply in my daily work as a software engineer.
For example: make small iterations, prefer readability over cleverness, or apply the YAGNI principle.</p>

<p>Dear parents, I highly recommend these books for your kids interested in programming!</p>]]></content><author><name>Marcin Kuthan</name></author><category term="Books" /><category term="Software Engineering" /><summary type="html"><![CDATA[A few months ago, my 13-year-old son and I were discussing his future high school options. He’s really smart and interested in technology (programming, robotics, 3D printing, etc.) but extremely bored with traditional school subjects. Finding a school that would fit his interests wasn’t easy, but fortunately my old friend Orłoś told me about TechniSchools.]]></summary></entry><entry><title type="html">Home Assistant solar energy management</title><link href="https://mkuthan.github.io/blog/2025/04/12/home-assistant-solar/" rel="alternate" type="text/html" title="Home Assistant solar energy management" /><published>2025-04-12T00:00:00+00:00</published><updated>2025-04-12T00:00:00+00:00</updated><id>https://mkuthan.github.io/blog/2025/04/12/home-assistant-solar</id><content type="html" xml:base="https://mkuthan.github.io/blog/2025/04/12/home-assistant-solar/"><![CDATA[<p>🌞🔋 Since I last <a href="/blog/2024/12/08/home-assistant-automations/">blogged</a> about Home Assistant automations, I have taken a big step forward in sustainable living by installing a photovoltaic (PV) system with an energy storage unit. 🌱
This exciting upgrade has unlocked incredible opportunities for automating and optimizing energy usage in my home, making it smarter and greener than ever! 🏡✨</p>

<p><img src="/assets/images/2025-04-12-home-assistant-solar/energy_distribution.gif" alt="Energy Distribution" /></p>

<h2 id="pv-installation">PV installation</h2>

<p>I have a 6.3 kWp PV system with a 10 kWh battery storage unit installed in my utility room.</p>

<p><img src="/assets/images/2025-04-12-home-assistant-solar/pv_installation.jpg" alt="PV Installation" /></p>

<p>At the moment I am buying electricity at a flat rate of 1.2 PLN/kWh (0.28 €/kWh) including all taxes and fees.
I can also sell excess energy back to the grid at dynamic hourly rates.
The selling price depends on the time of the day and the current demand for energy.
Charts below show the hourly rates for today and tomorrow, in PLN/MWh (to get the price in EUR or USD, just divide by ~4.3).</p>

<p><img src="/assets/images/2025-04-12-home-assistant-solar/price_forecast.png" alt="Price Forecast" /></p>

<h2 id="requirements">Requirements</h2>

<p>To make the most of my solar energy system, I have outlined the following key requirements for efficient energy management and automation:</p>

<p>⚡️ <strong>Prioritize self-consumption</strong> over exporting energy to the grid, as it’s the most cost-effective approach for my setup.</p>

<p>🌍 <strong>Export energy to the grid</strong> only when the current and forecasted household consumption is fully covered by on-site generation.</p>

<p>🚫 <strong>Limit energy export</strong> to the grid when hourly electricity prices are zero or negative. Exporting under such conditions provides no financial benefit and only generates unnecessary heat in the inverter.</p>

<h2 id="inverter-remote-control">Inverter remote control</h2>

<p>The most important part of the setup is the ability to control the inverter remotely.
I have a Solis hybrid inverter, which is compatible with the Solis Cloud API.
Unfortunately, the existing <a href="https://github.com/hultenvp/solis-sensor">solis-sensor</a> integration covers only the monitoring part, the control part is experimental and unstable, see <a href="https://github.com/hultenvp/solis-sensor/issues/437">#437</a>.</p>

<p>This project also provided an excellent opportunity to develop my own custom integration for Home Assistant, tailored specifically for controlling the Solis inverter. You can find the source code and detailed documentation here:</p>

<p><a href="https://github.com/mkuthan/solis-cloud-control">Solis Cloud Control Integration</a></p>

<p>At the time of writing, the integration allows for controlling the inverter in the following ways:</p>

<p><img src="/assets/images/2025-04-12-home-assistant-solar/solis_control.png" alt="Solis Control" /></p>

<h2 id="energy-production-forecasting">Energy Production Forecasting</h2>

<p>An essential component of my setup is the ability to accurately forecast energy production. By integrating <a href="https://github.com/BJReplay/ha-solcast-solar">Solcast</a> with Home Assistant, I can access detailed energy production forecasts tailored to my PV installation.</p>

<p>This integration provides valuable insights into expected solar generation, enabling better planning and optimization of energy usage and storage.</p>

<p><img src="/assets/images/2025-04-12-home-assistant-solar/production_forecast.png" alt="Production Forecast" /></p>

<h2 id="solis-inverter-modes-of-operation">Solis Inverter Modes of Operation</h2>

<p>Solis inverters support three primary modes of operation:</p>

<p>🔋 <strong>Self-Use</strong>: In this mode, the inverter prioritizes using energy generated by the PV system to power the home and charge the battery. Any excess energy is exported to the grid.</p>

<p>⚡ <strong>In-Feed Priority</strong>: This mode prioritizes selling energy to the grid. The battery will neither charge nor discharge unless “Time Charging” is enabled and properly configured.</p>

<p>🌐 <strong>Off-Grid</strong>: Designed for installations without grid power, this mode isn’t relevant to this project.</p>

<h2 id="automation-for-inverter-mode">Automation for inverter mode</h2>

<p>By default, my installation operates in <strong>Self-Use</strong> mode, which effectively handles most scenarios.
However, under specific conditions, automation switches the inverter to <strong>In-Feed Priority</strong> mode to maximize energy export. These conditions include:</p>

<ul>
  <li>The house is in “away mode,” indicating no one is home and excess energy is available.</li>
  <li>The sun is above the horizon, ensuring sufficient energy production.</li>
  <li>Hourly electricity prices are favorable and exceed the minimum price of the day.</li>
  <li>The remaining energy production forecast for the day is sufficient to fully recharge the battery, ensuring optimal energy storage for later use.</li>
  <li>Today’s temperature is high enough to minimize excessive power consumption for heating, considering the current state of charge (SOC) of the battery.</li>
  <li>The current household power consumption is low, which aligns with the current battery SOC to optimize energy usage.</li>
</ul>

<p>In all other scenarios, the inverter reverts to <strong>Self-Use</strong> mode to optimize energy usage and storage.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
</pre></td><td class="rouge-code"><pre><span class="pi">-</span> <span class="na">alias</span><span class="pi">:</span> <span class="s">Solar - mode optimization</span>
  <span class="na">id</span><span class="pi">:</span> <span class="s">solar_01</span>
  <span class="na">triggers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">trigger</span><span class="pi">:</span> <span class="s">state</span>
      <span class="na">entity_id</span><span class="pi">:</span> <span class="s">input_boolean.away_mode</span>
    <span class="pi">-</span> <span class="na">trigger</span><span class="pi">:</span> <span class="s">time_pattern</span>
      <span class="na">minutes</span><span class="pi">:</span> <span class="m">0</span>
      <span class="na">seconds</span><span class="pi">:</span> <span class="m">30</span> <span class="c1"># to get up-to-date hourly statistics</span>
    <span class="pi">-</span> <span class="na">trigger</span><span class="pi">:</span> <span class="s">time_pattern</span>
      <span class="na">minutes</span><span class="pi">:</span> <span class="m">30</span>
  <span class="na">variables</span><span class="pi">:</span>
    <span class="c1"># Battery SOC [%]</span>
    <span class="na">battery_soc</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">states('sensor.solis_remaining_battery_capacity')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">float</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">battery_reserve</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">states('sensor.inverter_control_battery_reserve_soc')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">float</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">battery_max_charge</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">states('sensor.inverter_control_battery_max_charge_soc')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">float</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">battery_threshold_low</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">[battery_reserve</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">float</span><span class="nv"> </span><span class="s">+</span><span class="nv"> </span><span class="s">10,</span><span class="nv"> </span><span class="s">battery_max_charge]</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">min</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">battery_threshold_high</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">[battery_reserve</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">float</span><span class="nv"> </span><span class="s">+</span><span class="nv"> </span><span class="s">40,</span><span class="nv"> </span><span class="s">battery_max_charge]</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">min</span><span class="nv"> </span><span class="s">}}"</span>

    <span class="c1"># Prices [PLN/kWh]</span>
    <span class="na">price</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">states('sensor.solar_electricity_price_hourly_rate')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">float</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">price_min</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">states('sensor.solar_electricity_price_hourly_rate_min')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">float</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">price_valley_threshold</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">states('input_number.solar_electricity_price_valley_threshold')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">float</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">price_threshold</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">[price_min,</span><span class="nv"> </span><span class="s">price_valley_threshold]</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">max</span><span class="nv"> </span><span class="s">}}"</span>

    <span class="c1"># PV forecast [kWh]</span>
    <span class="na">pv_forecast</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">states('sensor.solcast_pv_forecast_forecast_remaining_today')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">float</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">pv_forecast_threshold_low</span><span class="pi">:</span> <span class="m">15</span>
    <span class="na">pv_forecast_threshold_high</span><span class="pi">:</span> <span class="m">25</span>

    <span class="c1"># Temperature forecast [°C]</span>
    <span class="na">temp_forecast</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">states('sensor.weather_temperature_today')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">float</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">temp_forecast_threshold</span><span class="pi">:</span> <span class="m">5</span>

    <span class="c1"># Power consumption [W]</span>
    <span class="na">power_consumption</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">states('sensor.solar_load_power')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">float</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">power_consumption_threshold</span><span class="pi">:</span> <span class="m">500</span>

    <span class="c1"># Export power [W]</span>
    <span class="na">export_power_capped</span><span class="pi">:</span> <span class="m">1000</span>
    <span class="na">export_power_nominal</span><span class="pi">:</span> <span class="m">13200</span>
    <span class="na">export_power</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">export_power_nominal</span><span class="nv"> </span><span class="s">if</span><span class="nv"> </span><span class="s">price</span><span class="nv"> </span><span class="s">&gt;</span><span class="nv"> </span><span class="s">0.01</span><span class="nv"> </span><span class="s">else</span><span class="nv"> </span><span class="s">export_power_capped</span><span class="nv"> </span><span class="s">}}"</span>

  <span class="na">actions</span><span class="pi">:</span>
    <span class="na">choose</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">conditions</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">state</span>
            <span class="na">entity_id</span><span class="pi">:</span> <span class="s">input_boolean.away_mode</span>
            <span class="na">state</span><span class="pi">:</span> <span class="s2">"</span><span class="s">on"</span>
          <span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">state</span>
            <span class="na">entity_id</span><span class="pi">:</span> <span class="s">sun.sun</span>
            <span class="na">state</span><span class="pi">:</span> <span class="s">above_horizon</span>
          <span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">template</span>
            <span class="na">alias</span><span class="pi">:</span> <span class="s">If price is decent</span>
            <span class="na">value_template</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">price</span><span class="nv"> </span><span class="s">&gt;</span><span class="nv"> </span><span class="s">price_threshold</span><span class="nv"> </span><span class="s">}}"</span>
          <span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">template</span>
            <span class="na">alias</span><span class="pi">:</span> <span class="s">If PV forecast meets battery SOC criteria</span>
            <span class="na">value_template</span><span class="pi">:</span> <span class="pi">&gt;</span>
              <span class="s">{{ (pv_forecast &gt; pv_forecast_threshold_high and battery_soc &gt; battery_threshold_low) or </span>
                  <span class="s">(pv_forecast &gt; pv_forecast_threshold_low and battery_soc &gt; battery_threshold_high) }}</span>
          <span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">template</span>
            <span class="na">alias</span><span class="pi">:</span> <span class="s">If temperature meets battery SOC criteria</span>
            <span class="na">value_template</span><span class="pi">:</span> <span class="pi">&gt;</span>
              <span class="s">{{ (temp_forecast &gt; temp_forecast_threshold and battery_soc &gt; battery_threshold_low) or</span>
                  <span class="s">(temp_forecast &lt;= temp_forecast_threshold and battery_soc &gt; battery_threshold_high) }}</span>
          <span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">template</span>
            <span class="na">alias</span><span class="pi">:</span> <span class="s">If power consumption meets battery SOC criteria</span>
            <span class="na">value_template</span><span class="pi">:</span>
              <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">(power_consumption</span><span class="nv"> </span><span class="s">&lt;</span><span class="nv"> </span><span class="s">power_consumption_threshold</span><span class="nv"> </span><span class="s">and</span><span class="nv"> </span><span class="s">battery_soc</span><span class="nv"> </span><span class="s">&gt;</span><span class="nv"> </span><span class="s">battery_threshold_low)</span><span class="nv"> </span><span class="s">or</span>
              <span class="s">(power_consumption</span><span class="nv"> </span><span class="s">&gt;=</span><span class="nv"> </span><span class="s">power_consumption_threshold</span><span class="nv"> </span><span class="s">and</span><span class="nv"> </span><span class="s">battery_soc</span><span class="nv"> </span><span class="s">&gt;</span><span class="nv"> </span><span class="s">battery_threshold_high)</span><span class="nv"> </span><span class="s">}}"</span>
        <span class="na">sequence</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">action</span><span class="pi">:</span> <span class="s">select.select_option</span>
            <span class="na">entity_id</span><span class="pi">:</span> <span class="s">select.inverter_control_storage_mode</span>
            <span class="na">data</span><span class="pi">:</span>
              <span class="na">option</span><span class="pi">:</span> <span class="s">Feed-In Priority</span>
          <span class="pi">-</span> <span class="na">action</span><span class="pi">:</span> <span class="s">number.set_value</span>
            <span class="na">entity_id</span><span class="pi">:</span> <span class="s">number.inverter_control_max_export_power</span>
            <span class="na">data</span><span class="pi">:</span>
              <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">export_power</span><span class="nv"> </span><span class="s">}}"</span>
      <span class="na">default</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">action</span><span class="pi">:</span> <span class="s">select.select_option</span>
          <span class="na">entity_id</span><span class="pi">:</span> <span class="s">select.inverter_control_storage_mode</span>
          <span class="na">data</span><span class="pi">:</span>
            <span class="na">option</span><span class="pi">:</span> <span class="s">Self-Use</span>
        <span class="pi">-</span> <span class="na">action</span><span class="pi">:</span> <span class="s">number.set_value</span>
          <span class="na">entity_id</span><span class="pi">:</span> <span class="s">number.inverter_control_max_export_power</span>
          <span class="na">data</span><span class="pi">:</span>
            <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">export_power</span><span class="nv"> </span><span class="s">}}"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="selling-energy-during-peak-hour">Selling energy during peak hour</h2>

<p>The second automation schedules the inverter to sell energy during peak hour, when the following conditions are met:</p>

<ul>
  <li>The house is working in eco mode. I have excess energy only when there is no one at home, or we’re going to leave soon.</li>
  <li>The price is decent and maximum of the day.</li>
  <li>Energy production forecast for tomorrow is excellent.</li>
  <li>Temperature forecast for tomorrow is high enough to minimize excessive power consumption for heating.</li>
  <li>Battery is fully charged.</li>
  <li>There will be enough energy for sale considering typical nighttime consumption.</li>
</ul>

<p><img src="/assets/images/2025-04-12-home-assistant-solar/time_slots.png" alt="Time Slots" /></p>

<p>The most tricky part of the automation is to calculate the amount of energy that can be sold.
Ask LLM if you need more details about the algorithm 😜</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
</pre></td><td class="rouge-code"><pre><span class="pi">-</span> <span class="na">alias</span><span class="pi">:</span> <span class="s">Solar - schedule discharge slot</span>
  <span class="na">id</span><span class="pi">:</span> <span class="s">solar_02</span>
  <span class="na">triggers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">trigger</span><span class="pi">:</span> <span class="s">state</span>
      <span class="na">entity_id</span><span class="pi">:</span> <span class="s">input_boolean.eco_mode</span>
    <span class="pi">-</span> <span class="na">trigger</span><span class="pi">:</span> <span class="s">time</span>
      <span class="na">at</span><span class="pi">:</span> <span class="s2">"</span><span class="s">15:30:00"</span> <span class="c1"># when prices for the next day are available</span>
    <span class="pi">-</span> <span class="na">trigger</span><span class="pi">:</span> <span class="s">time</span>
      <span class="na">at</span><span class="pi">:</span> <span class="s2">"</span><span class="s">16:00:00"</span> <span class="c1"># backup call</span>
  <span class="na">variables</span><span class="pi">:</span>
    <span class="c1"># Battery SOC [%]</span>
    <span class="na">battery_soc</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">states('sensor.solis_remaining_battery_capacity')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">float</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">battery_reserve</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">states('sensor.inverter_control_battery_reserve_soc')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">float</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">battery_max_charge</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">states('sensor.inverter_control_battery_max_charge_soc')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">float</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">battery_threshold_low</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">[battery_reserve</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">float</span><span class="nv"> </span><span class="s">+</span><span class="nv"> </span><span class="s">15,</span><span class="nv"> </span><span class="s">battery_max_charge]</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">min</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">battery_threshold_high</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">[battery_max_charge</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">float</span><span class="nv"> </span><span class="s">-</span><span class="nv"> </span><span class="s">10,</span><span class="nv"> </span><span class="s">battery_threshold_low]</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">max</span><span class="nv"> </span><span class="s">}}"</span>

    <span class="na">battery_capacity</span><span class="pi">:</span> <span class="m">10000</span> <span class="c1"># Wh</span>
    <span class="na">battery_voltage</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">states('sensor.solis_battery_voltage')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">float</span><span class="nv"> </span><span class="s">}}"</span> <span class="c1"># V</span>

    <span class="c1"># PV forecast [kWh]</span>
    <span class="na">pv_forecast</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">states('sensor.solcast_pv_forecast_forecast_tomorrow')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">float</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">pv_forecast_threshold</span><span class="pi">:</span> <span class="m">25</span>

    <span class="c1"># Temperature forecast [°C]</span>
    <span class="na">temp_forecast</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">states('sensor.weather_temperature_tomorrow')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">float</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">temp_forecast_threshold</span><span class="pi">:</span> <span class="m">5</span>

    <span class="c1"># Peak price and time</span>
    <span class="na">peak_raw</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">states('sensor.solar_electricity_price_hourly_rate_max_next_raw')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">from_json</span><span class="nv"> </span><span class="s">}}"</span>

    <span class="na">peak_price</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">peak_raw.price</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">peak_price_threshold</span><span class="pi">:</span> <span class="pi">&gt;</span>
      <span class="s">{{ states('input_number.solar_electricity_price_peak_threshold') | float }}</span>

    <span class="na">peak_time_hours</span><span class="pi">:</span> <span class="m">1</span>
    <span class="na">peak_time</span><span class="pi">:</span> <span class="pi">&gt;</span>
      <span class="s">{% set from = '%02d:00' | format(peak_raw.hour) %}</span>
      <span class="s">{% set to = '%02d:00' | format((peak_raw.hour + peak_time_hours) % 24) %}</span>
      <span class="s">{{ from ~ "-" ~ to }}</span>

    <span class="c1"># Export energy [#Wh]</span>
    <span class="na">energy_to_export_threshold</span><span class="pi">:</span> <span class="m">1000</span>
    <span class="na">energy_to_export</span><span class="pi">:</span> <span class="pi">&gt;</span>
      <span class="s">{% set energy_available = (battery_soc - battery_threshold_low) / 100 * battery_capacity %}</span>

      <span class="s">{% set sunset = as_timestamp(state_attr('sun.sun', 'next_setting')) %}</span>
      <span class="s">{% set sunrise = as_timestamp(state_attr('sun.sun', 'next_rising')) %}</span>
      <span class="s">{% if sunset &gt; sunrise %}</span>
          <span class="s">{% set sunset = sunset - 86400 %}</span>
      <span class="s">{% endif %}</span>
      <span class="s">{% set night_duration = sunrise - sunset  %}</span>

      <span class="s">{% set night_avg_consumption = 300 %}</span>
      <span class="s">{% set energy_night_consumption = (night_duration / 3600) * night_avg_consumption %}</span>

      <span class="s">{{ [energy_available - energy_night_consumption, 0] | max }}</span>

    <span class="c1"># Export current [A]</span>
    <span class="na">export_current_max</span><span class="pi">:</span> <span class="m">100</span>
    <span class="na">export_current</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">[energy_to_export</span><span class="nv"> </span><span class="s">/</span><span class="nv"> </span><span class="s">battery_voltage</span><span class="nv"> </span><span class="s">/</span><span class="nv"> </span><span class="s">peak_time_hours,</span><span class="nv"> </span><span class="s">export_current_max]</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">min</span><span class="nv"> </span><span class="s">}}"</span>

  <span class="na">actions</span><span class="pi">:</span>
    <span class="na">choose</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">conditions</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">state</span>
            <span class="na">entity_id</span><span class="pi">:</span> <span class="s">input_boolean.eco_mode</span>
            <span class="na">state</span><span class="pi">:</span> <span class="s2">"</span><span class="s">on"</span>
          <span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">template</span>
            <span class="na">alias</span><span class="pi">:</span> <span class="s">If price is decent</span>
            <span class="na">value_template</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">peak_price</span><span class="nv"> </span><span class="s">&gt;</span><span class="nv"> </span><span class="s">peak_price_threshold</span><span class="nv"> </span><span class="s">}}"</span>
          <span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">template</span>
            <span class="na">alias</span><span class="pi">:</span> <span class="s">If PV forecast is good</span>
            <span class="na">value_template</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">pv_forecast</span><span class="nv"> </span><span class="s">&gt;</span><span class="nv"> </span><span class="s">pv_forecast_threshold</span><span class="nv"> </span><span class="s">}}"</span>
          <span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">template</span>
            <span class="na">alias</span><span class="pi">:</span> <span class="s">If temperature forecast is good</span>
            <span class="na">value_template</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">temp_forecast</span><span class="nv"> </span><span class="s">&gt;</span><span class="nv"> </span><span class="s">temp_forecast_threshold</span><span class="nv"> </span><span class="s">}}"</span>
          <span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">template</span>
            <span class="na">alias</span><span class="pi">:</span> <span class="s">If battery SOC is high</span>
            <span class="na">value_template</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">battery_soc</span><span class="nv"> </span><span class="s">&gt;</span><span class="nv"> </span><span class="s">battery_threshold_high</span><span class="nv"> </span><span class="s">}}"</span>
          <span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">template</span>
            <span class="na">alias</span><span class="pi">:</span> <span class="s">If there is enough energy to export</span>
            <span class="na">value_template</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">energy_to_export</span><span class="nv"> </span><span class="s">&gt;</span><span class="nv"> </span><span class="s">energy_to_export_threshold</span><span class="nv"> </span><span class="s">}}"</span>
        <span class="na">sequence</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">action</span><span class="pi">:</span> <span class="s">text.set_value</span>
            <span class="na">entity_id</span><span class="pi">:</span> <span class="s">text.inverter_control_slot1_discharge_time</span>
            <span class="na">data</span><span class="pi">:</span>
              <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">peak_time</span><span class="nv"> </span><span class="s">}}"</span>
          <span class="pi">-</span> <span class="na">action</span><span class="pi">:</span> <span class="s">number.set_value</span>
            <span class="na">entity_id</span><span class="pi">:</span> <span class="s">number.inverter_control_slot1_discharge_current</span>
            <span class="na">data</span><span class="pi">:</span>
              <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">export_current</span><span class="nv"> </span><span class="s">}}"</span>
          <span class="pi">-</span> <span class="na">action</span><span class="pi">:</span> <span class="s">switch.turn_on</span>
            <span class="na">entity_id</span><span class="pi">:</span> <span class="s">switch.inverter_control_slot1_discharge</span>
      <span class="na">default</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">action</span><span class="pi">:</span> <span class="s">switch.turn_off</span>
          <span class="na">entity_id</span><span class="pi">:</span> <span class="s">switch.inverter_control_slot1_discharge</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="expose-excess-energy-mode">Expose excess energy mode</h2>

<p>The third automation introduces an “excess energy” mode, which can be utilized by other automations to determine when surplus energy is available for consumption. For instance, a bathroom heater automation can leverage this mode to activate the electric heater only when excess energy is detected.</p>

<p>Excess power sensor is calculated as follows:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="na">template</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">sensor</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Solar Excess Power</span>
        <span class="na">unit_of_measurement</span><span class="pi">:</span> <span class="s">W</span>
        <span class="na">device_class</span><span class="pi">:</span> <span class="s">power</span>
        <span class="na">icon</span><span class="pi">:</span> <span class="s">mdi:lightning-bolt-outline</span>
        <span class="na">state</span><span class="pi">:</span> <span class="pi">&gt;</span>
          <span class="s">{% set pv = states('sensor.solar_pv_power') | float %}</span>
          <span class="s">{% set battery = states('sensor.solar_battery_power') | float %}</span>
          <span class="s">{% set load = states('sensor.solar_load_power') | float %}</span>
          <span class="s">{% set excess = pv - (load + battery) if battery &gt;= 0 else 0 %}</span>
          <span class="s">{{ [excess, 0] | max }}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Automation for setting the excess energy mode is triggered by the following conditions:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
</pre></td><td class="rouge-code"><pre><span class="pi">-</span> <span class="na">alias</span><span class="pi">:</span> <span class="s">Solar - set excess energy mode "on"</span>
  <span class="na">id</span><span class="pi">:</span> <span class="s">solar_03</span>
  <span class="na">triggers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">trigger</span><span class="pi">:</span> <span class="s">numeric_state</span>
      <span class="na">entity_id</span><span class="pi">:</span> <span class="s">sensor.solar_electricity_price_hourly_rate</span>
      <span class="na">below</span><span class="pi">:</span> <span class="s">input_number.solar_electricity_price_valley_threshold</span>
    <span class="pi">-</span> <span class="na">trigger</span><span class="pi">:</span> <span class="s">numeric_state</span>
      <span class="na">entity_id</span><span class="pi">:</span> <span class="s">sensor.solar_excess_power</span>
      <span class="na">above</span><span class="pi">:</span> <span class="m">500</span>
      <span class="na">for</span><span class="pi">:</span>
        <span class="na">minutes</span><span class="pi">:</span> <span class="m">10</span>
  <span class="na">conditions</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">numeric_state</span>
      <span class="na">entity_id</span><span class="pi">:</span> <span class="s">sensor.solar_electricity_price_hourly_rate</span>
      <span class="na">below</span><span class="pi">:</span> <span class="s">input_number.solar_electricity_price_valley_threshold</span>
  <span class="na">actions</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">action</span><span class="pi">:</span> <span class="s">input_boolean.turn_on</span>
      <span class="na">entity_id</span><span class="pi">:</span> <span class="s">input_boolean.solar_excess_energy_mode</span>

<span class="pi">-</span> <span class="na">alias</span><span class="pi">:</span> <span class="s">Solar - set excess energy mode "off"</span>
  <span class="na">id</span><span class="pi">:</span> <span class="s">solar_04</span>
  <span class="na">triggers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">trigger</span><span class="pi">:</span> <span class="s">numeric_state</span>
      <span class="na">entity_id</span><span class="pi">:</span> <span class="s">sensor.solar_electricity_price_hourly_rate</span>
      <span class="na">above</span><span class="pi">:</span> <span class="s">input_number.solar_electricity_price_valley_threshold</span>
    <span class="pi">-</span> <span class="na">trigger</span><span class="pi">:</span> <span class="s">numeric_state</span>
      <span class="na">entity_id</span><span class="pi">:</span> <span class="s">sensor.solar_excess_power</span>
      <span class="na">below</span><span class="pi">:</span> <span class="m">100</span>
      <span class="na">for</span><span class="pi">:</span>
        <span class="na">minutes</span><span class="pi">:</span> <span class="m">10</span>
  <span class="na">conditions</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">numeric_state</span>
      <span class="na">entity_id</span><span class="pi">:</span> <span class="s">sensor.solar_electricity_price_hourly_rate</span>
      <span class="na">above</span><span class="pi">:</span> <span class="s">input_number.solar_electricity_price_valley_threshold</span>
  <span class="na">actions</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">action</span><span class="pi">:</span> <span class="s">input_boolean.turn_off</span>
      <span class="na">entity_id</span><span class="pi">:</span> <span class="s">input_boolean.solar_excess_energy_mode</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="summary">Summary</h2>

<p>With a PV system of this size, I could have simply left the inverter in “Self-Use” mode, and the overall results wouldn’t differ significantly. However, I approached this entire experiment as an excellent opportunity to learn and have fun. By diving into the intricacies of energy management and automation, I gained valuable insights and hands-on experience that not only enhanced my technical skills but also made my home smarter and more efficient.</p>

<p><img src="/assets/images/2025-04-12-home-assistant-solar/energy_dashboard.png" alt="Energy Dashboard" /></p>

<p>Don’t forget to add a ⭐️ to my project on GitHub if you find it useful!
<a href="https://github.com/mkuthan/solis-cloud-control">https://github.com/mkuthan/solis-cloud-control</a></p>]]></content><author><name>Marcin Kuthan</name></author><category term="DIY" /><category term="Homelab" /><summary type="html"><![CDATA[🌞🔋 Since I last blogged about Home Assistant automations, I have taken a big step forward in sustainable living by installing a photovoltaic (PV) system with an energy storage unit. 🌱 This exciting upgrade has unlocked incredible opportunities for automating and optimizing energy usage in my home, making it smarter and greener than ever! 🏡✨]]></summary></entry><entry><title type="html">Infrastructure as Code</title><link href="https://mkuthan.github.io/blog/2025/01/19/homelab-iaac/" rel="alternate" type="text/html" title="Infrastructure as Code" /><published>2025-01-19T00:00:00+00:00</published><updated>2025-01-19T00:00:00+00:00</updated><id>https://mkuthan.github.io/blog/2025/01/19/homelab-iaac</id><content type="html" xml:base="https://mkuthan.github.io/blog/2025/01/19/homelab-iaac/"><![CDATA[<p>From the very beginning, I used an Infrastructure as Code (IaaC) approach in my homelab. However, due to privacy concerns, I couldn’t publish it as open source. Recently, I spent a lot of time separating sensitive information so that I could publish the rest as open source 😊</p>

<p>Check it out here: <a href="https://github.com/mkuthan/homelab-public">https://github.com/mkuthan/homelab-public</a></p>

<h2 id="why-iaac">Why IaaC?</h2>

<p>What is a main challenge in a homelab? For me it’s the same as in a production environment - keeping everything up to date, secure, and reliable while minimizing manual work.</p>

<p>I’m a software engineer, so I’m used to writing beautiful, testable code. In the infrastructure world, it’s not that easy.
Fortunately, IaaC tools like Terraform and Ansible help me to write infrastructure code in a way I’m used to.</p>

<h2 id="terraform">Terraform</h2>

<p>Terraform defines the following resources in my homelab:</p>

<p>🖥️ Linux containers (LXC) using the <a href="https://registry.terraform.io/providers/Telmate/proxmox/latest/docs">Telmate Proxmox</a> provider. It covers most of the container resource definitions: CPU, memory, root disk, mount points, networking, SSH keys, and nested virtualization. In the Proxmox UI, I only define replication and high availability settings.</p>

<p>☁️ Virtual Private Server (VPS) with required networking resources in Google Cloud Platform (GCP). I use this VPS for hosting Uptime Kuma to monitor my homelab services.</p>

<p>📦 Bucket on Google Cloud Storage (GCS) for storing offsite backups.</p>

<p>🔒 Tailscale access control lists (ACLs). Thanks to data providers like <code class="language-plaintext highlighter-rouge">tailscale_devices</code> or <code class="language-plaintext highlighter-rouge">tailscale_users</code> I’m able to generate ACLs on the fly.</p>

<h2 id="ansible">Ansible</h2>

<p>Ansible roles define almost all the software I use in my homelab. I couldn’t imagine to maintain all that stuff manually.
Here are some examples:</p>

<p>🛡️ Adguard DNS</p>

<p>📦 Apt Cacher NG</p>

<p>🛠️ Backup Ninja</p>

<p>🐳 Docker</p>

<p>📹 Frigate</p>

<p>📊 Grafana</p>

<p>📈 Grafana Agent</p>

<p>👴 Gramps</p>

<p>🌈 Hyperion NG</p>

<p>📸 Immich</p>

<p>🎥 Kodi</p>

<p>📂 Loki</p>

<p>📧 Mailrise</p>

<p>🐝 Mosqquitto</p>

<p>🔋 NUT</p>

<p>🌐 Omada Software Controller</p>

<p>📄 Paperless NGX</p>

<p>💾 Proxmox Backup Server</p>

<p>📈 Prometheus</p>

<p>🎵 Raspotify</p>

<p>🔄 RClone</p>

<p>🖥️ Samba</p>

<p>🔍 SearXNG</p>

<p>🎶 Shairport</p>

<p>📄 Stirling PDF</p>

<p>🔒 Tailscale</p>

<p>🚀 Traefik</p>

<p>📡 Transmission</p>

<p>📊 Uptime Kuma</p>

<p>🔐 Vaultwarden</p>

<p>🔍 Whoogle</p>

<p>📡 Zigbee2MQT</p>

<p>If you’re interested in how these services are set up in my homelab, you can explore the playbooks. Here are some examples: <a href="https://github.com/mkuthan/homelab-public/blob/main/ansible/playbooks/pve.yml">Proxmox hosts</a>,
<a href="https://github.com/mkuthan/homelab-public/blob/main/ansible/playbooks/pi.yml">Raspberry Pi</a>,
<a href="https://github.com/mkuthan/homelab-public/blob/main/ansible/playbooks/vps.yml">VPS</a>.</p>

<p>Please note that I use a dynamic Ansible inventory for all my Linux containers. You can find more details in the <a href="https://github.com/mkuthan/homelab-public/blob/main/ansible/inventory.proxmox.yml">inventory.proxmox.yml</a> file. The static inventory includes only non-virtualized hosts such as Proxmox VE, Raspberry Pi, and VPS.</p>

<h2 id="conclusion">Conclusion</h2>

<p>I hope you find my homelab setup useful and inspiring. If you have any questions, feel free to ask me on <a href="https://github.com/mkuthan/homelab-public/discussions">GitHub Discussions</a>.</p>]]></content><author><name>Marcin Kuthan</name></author><category term="Homelab" /><category term="Terraform" /><category term="Ansible" /><summary type="html"><![CDATA[From the very beginning, I used an Infrastructure as Code (IaaC) approach in my homelab. However, due to privacy concerns, I couldn’t publish it as open source. Recently, I spent a lot of time separating sensitive information so that I could publish the rest as open source 😊]]></summary></entry><entry><title type="html">Homelab upgrade 2025</title><link href="https://mkuthan.github.io/blog/2025/01/02/homelab-upgrade/" rel="alternate" type="text/html" title="Homelab upgrade 2025" /><published>2025-01-02T00:00:00+00:00</published><updated>2025-01-02T00:00:00+00:00</updated><id>https://mkuthan.github.io/blog/2025/01/02/homelab-upgrade</id><content type="html" xml:base="https://mkuthan.github.io/blog/2025/01/02/homelab-upgrade/"><![CDATA[<p>I built my homelab at the beginning of 2024, see <a href="https://mkuthan.github.io/blog/2024/06/30/homlab-hardware/">Building your ultimate Homelab</a> blog post.
I hosted many services on it, for example: Home Assistant for <a href="https://mkuthan.github.io/blog/2024/12/08/home-assistant-automations/">home automation</a>, Frigate for surveillance, Vaultwarden for password management, Paperless for document management, Omada Software Controller for <a href="https://mkuthan.github.io/blog/2024/07/29/homlab-network/">network management</a>, Prometheus, Grafana, and many more.
I wanted to make my homelab more reliable and more powerful, so I decided to configure Proxmox cluster.</p>

<h2 id="yet-another-dell-optiplex-micro">Yet another Dell Optiplex Micro</h2>

<p>To build a Proxmox cluster, I needed an additional server.
The existing Dell Optiplex Micro 3050 had served me well, so I decided to purchase a more powerful model this time.
I opted for the Dell Optiplex Micro 5070, which comes with the following specifications:</p>

<ul>
  <li>CPU Intel i5-9500T 2.2-3.7GHz, 6 cores</li>
  <li>GPU Intel® HD Graphics 630</li>
  <li>32GB RAM</li>
  <li>256 GB SSD, Samsung PM981 (NVMe, TLC)</li>
  <li>Built-in Gigabit Ethernet card</li>
  <li>USB 3.1 Gen 2 × 1 Type-C</li>
  <li>USB 3.1 Gen 1 × 5</li>
</ul>

<p>Again for hosting VMs and containers, I mounted an enterprise-grade SSD: Intel DC S3610 1.6TB.
This SSD is known for its high endurance and reliability, making it an excellent choice for a homelab environment where data integrity and performance are crucial.
Despite having 44,123 power-on hours (~5 years), this model boasts an impressive Total Bytes Written (TBW) rating of 10.7PB.
The current wear level is at 0%, indicating that the drive has plenty of life left and should continue to perform reliably for a long time. The Intel DC S3610’s high endurance is due to its use of Multi-Level Cell (MLC) NAND technology, which provides a good balance between performance, endurance, and cost.</p>

<h2 id="make-a-cluster-quorum">Make a cluster quorum</h2>

<p>I also bought a Dell Wyse 3040 thin client to use as a Proxmox QDevice to achieve cluster quorum. The Dell Wyse 3040 is equipped with a quad-core Intel Atom x5-Z8350 CPU, 2GB of RAM, and 8GB of eMMC storage.</p>

<p>Initially, I considered using a Raspberry Pi for this purpose.
However, the Dell Wyse 3040 offered several advantages that made it a better option. Firstly, the built-in eMMC storage of the Wyse 3040 is more reliable and durable compared to the SD cards typically used in Raspberry Pi devices.
SD cards are prone to wear and data corruption over time, especially under continuous read/write operations, which can be a significant drawback in a homelab environment where reliability is crucial.
Additionally, the Dell Wyse 3040 is more affordable than a Raspberry Pi when considering the total cost, including necessary accessories such as a case, power supply, and storage.</p>

<p>Overall, the Dell Wyse 3040 provides a robust and reliable solution for maintaining cluster quorum in my Proxmox setup, ensuring high availability and seamless failover capabilities.</p>

<p>Below you can see the Dell Optiplex Micro 5070 and Dell Wyse 3040 disassembled for thermal paste replacement.</p>

<p><img src="/assets/images/2025-01-02-homelab-upgrade/dell_optiplex_wyse.jpg" alt="Dell Optiplex Micro 5070 + Wyse 3040" /></p>

<h2 id="bigger-pipe">Bigger pipe</h2>

<p>A faster network is crucial for ensuring efficient data transfer and reducing latency.
It allows for quicker backups, faster VM migrations, and smoother operation of network-intensive applications.</p>

<p>So, I decided to pimp my network by hooking up both servers with some slick 2.5GbE network cards, while keeping my old 1GbE setup intact.
I slapped in some RTL8125B network cards using PCIe M.2 A+E adapters, and boom, we’re in business.</p>

<p><img src="/assets/images/2025-01-02-homelab-upgrade/network_card.jpg" alt="2.5GbE network card" /></p>

<p>This dedicated network is now the express lane for Proxmox cluster chatter and storage traffic.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="nv">$ </span>iperf <span class="nt">-s</span>
<span class="nt">------------------------------------------------------------</span>
Server listening on TCP port 5001
TCP window size:  128 KByte <span class="o">(</span>default<span class="o">)</span>
<span class="nt">------------------------------------------------------------</span>
<span class="o">[</span>  1] <span class="nb">local </span>10.0.10.30 port 5001 connected with 10.0.10.31 port 41416 <span class="o">(</span>icwnd/mss/irtt<span class="o">=</span>14/1448/99<span class="o">)</span>
<span class="o">[</span> ID] Interval       Transfer     Bandwidth
<span class="o">[</span>  1] 0.0000-10.0134 sec  2.74 GBytes  2.35 Gbits/sec
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Check out the final datacenter installed in my rack:</p>

<p><img src="/assets/images/2025-01-02-homelab-upgrade/rack.jpg" alt="Rack" /></p>

<h2 id="high-availability">High availability</h2>

<p>I chose not to use shared storage like a Synology NAS because it can become a single point of failure.
Instead, replication offers a more resilient solution for my needs. ZFS really rocks! It provides robust data integrity, efficient snapshots, and seamless replication.</p>

<p>With all hardware in place, I started to configure the Proxmox cluster.
I created a ZFS pool on both servers and defined a replication schedule to ensure that data is consistently mirrored between the servers.
This setup allows for high availability and data redundancy, ensuring that my VMs can be quickly restored or migrated in case of hardware failure.</p>

<p><img src="/assets/images/2025-01-02-homelab-upgrade/proxmox_replication.png" alt="Proxmox replication" /></p>

<p>With replicated volumes, I can easily migrate VMs between servers and have a backup in case of hardware failure. Replication uses the 2.5GbE network for better performance.
Thanks to the QDevice installed on the Dell Wyse 3040, Proxmox can automatically start VMs on the second server in case of the first server failure.
I tested the failover when I pulled old Optiplex Micro 3050 for BIOS update.
The VMs were automatically migrated to the second server and started without any issues!</p>

<p><img src="/assets/images/2025-01-02-homelab-upgrade/proxmox_ha.png" alt="Proxmox High Availability" /></p>

<h2 id="external-storage">External storage</h2>

<p>In my homelab, Frigate stores video recordings on an external USB drive.
This drive is connected to one of the servers and shared via NFS to the second server.
From the Frigate container’s perspective, the drive is mounted as a local directory, which allows for easy migration of Frigate between servers.</p>

<p>Mounts in <code class="language-plaintext highlighter-rouge">/etc/fstab</code> on the server with the external drives:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>UUID=... /mnt/usb1 ext4 defaults,nofail,x-systemd.device-timeout=10s 0 0
UUID=... /mnt/usb2 ext4 defaults,nofail,x-systemd.device-timeout=10s 0 0
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Mounts in <code class="language-plaintext highlighter-rouge">/etc/fstab</code> on the server with the NFS share:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>10.0.10.31:/mnt/usb1 /mnt/usb1 nfs4 defaults,rw,hard,rsize=1048576,wsize=1048576,timeo=300,retrans=2 0 0
10.0.10.31:/mnt/usb2 /mnt/usb2 nfs4 defaults,rw,hard,rsize=1048576,wsize=1048576,timeo=300,retrans=2 0 0
</pre></td></tr></tbody></table></code></pre></div></div>

<p>If the server with the local drive fails, I need to manually connect the drive to the second server and change the mount point from NFS to local.
This manual intervention ensures that Frigate can continue to access the video recordings without interruption.</p>

<p>This setup provides a balance between flexibility and reliability, allowing me to handle server failures without requiring automated high availability for the external storage.</p>

<h2 id="backup">Backup</h2>

<p>I migrated from VZDump to Proxmox Backup Server for several reasons. Firstly, Proxmox Backup Server offers deduplication, which significantly reduces the storage space required for backups.
This is particularly beneficial in a homelab environment where storage efficiency is crucial.
Additionally, Proxmox Backup Server provides faster backup and restore operations compared to VZDump, thanks to its optimized data handling and compression techniques.</p>

<p><img src="/assets/images/2025-01-02-homelab-upgrade/proxmox_backup_server.png" alt="Proxmox Backup Server" /></p>

<p>Proxmox Backup Server runs as an LXC container and utilizes a dedicated ZFS pool for storing backups.
This pool is replicated to the second server using Proxmox’s replication feature, ensuring that backup data is always available and protected against hardware failures.</p>

<p>To avoid a chicken-and-egg problem, VZDump is configured to back up the Proxmox Backup Server itself.
This ensures that even if the Backup Server encounters issues, I can still restore it from VZDump backups.</p>

<p><img src="/assets/images/2025-01-02-homelab-upgrade/proxmox_backup.png" alt="Proxmox Backup" /></p>

<h2 id="power-management">Power management</h2>

<p>Besides serving as the QDevice for Proxmox quorum, the Dell Wyse 3040 plays another crucial role in my homelab.
It monitors the UPS connected via USB and runs the Linux NUT (Network UPS Tools) server.
This setup ensures that both Optiplex Micro servers, configured as NUT clients, can be gracefully shut down in case of a power failure, preventing data loss and hardware damage.</p>

<h2 id="zigbee-coordinator">Zigbee coordinator</h2>

<p>Initially, I used a Sonoff ZBDongle-E USB stick connected to my server.
However, this setup wasn’t ideal for a clustered environment since only one server could access the USB device at a time.
To overcome this limitation, I upgraded to an SLZB-06 Zigbee CC2652P coordinator.
This device connects via Ethernet, allowing the Zigbee2Mqtt container to access it regardless of which server it’s running on.
I strategically placed the coordinator in the center of my home to ensure optimal coverage for all Zigbee devices.
The device is powered by PoE, simplifying installation, and features a built-in web interface for easy configuration and monitoring.</p>

<p><img src="/assets/images/2025-01-02-homelab-upgrade/slzb-06.png" alt="Zigbee coordinator" /></p>

<h2 id="summary">Summary</h2>

<p>Upgrading my homelab has been an incredibly rewarding experience. Starting with a single server allowed me to grasp the fundamentals before scaling up to a highly available cluster.
My current setup boasts 10 high-performance cores, 64GB of RAM, and 800GB of replicated storage on enterprise-grade SSDs.
This configuration ensures that even if one server fails, the other can seamlessly take over with minimal or no downtime.</p>

<p>The total cost of this homelab upgrade was approximately 500 USD, which included the Dell Optiplex Micro 5070, Dell Wyse 3040, Intel DC S3610 1.6TB SSD, two RTL8125B network cards, and a new Zigbee coordinator.
Despite the enhancements, the power consumption of my homelab only increased by 30W, from 100W to 130W, keeping it efficient and manageable.</p>

<p><img src="/assets/images/2025-01-02-homelab-upgrade/cluster_summary.png" alt="Proxmox cluster" /></p>]]></content><author><name>Marcin Kuthan</name></author><category term="DIY" /><category term="Homelab" /><summary type="html"><![CDATA[I built my homelab at the beginning of 2024, see Building your ultimate Homelab blog post. I hosted many services on it, for example: Home Assistant for home automation, Frigate for surveillance, Vaultwarden for password management, Paperless for document management, Omada Software Controller for network management, Prometheus, Grafana, and many more. I wanted to make my homelab more reliable and more powerful, so I decided to configure Proxmox cluster.]]></summary></entry><entry><title type="html">Home Assistant automations</title><link href="https://mkuthan.github.io/blog/2024/12/08/home-assistant-automations/" rel="alternate" type="text/html" title="Home Assistant automations" /><published>2024-12-08T00:00:00+00:00</published><updated>2024-12-08T00:00:00+00:00</updated><id>https://mkuthan.github.io/blog/2024/12/08/home-assistant-automations</id><content type="html" xml:base="https://mkuthan.github.io/blog/2024/12/08/home-assistant-automations/"><![CDATA[<p>In this blog post, I will share how I use <a href="https://www.home-assistant.io/">Home Assistant</a> to automate my home in a pragmatic way.
Pragmatic means that I’m not trying to automate everything, but only things that make sense to me.
For example, for lights automation in the house, I use PIR or microwave <a href="https://en.wikipedia.org/wiki/Motion_detector">motion detectors</a> connected directly to lights, instead of fancy Zigbee switches.</p>

<h2 id="modes">Modes</h2>

<p>I introduced 3 modes to control how the house behaves.
The modes improve readability and maintainability of my automations.
They are implemented as simple <a href="https://www.home-assistant.io/integrations/input_boolean/">boolean flags</a>.</p>

<p><img src="/assets/images/2024-12-08-home-assistant-automations/input_boolean_modes.png" alt="Modes" /></p>

<h3 id="away-mode">Away Mode</h3>

<ul>
  <li>When I arm all alarm partitions, automation enables “Away Mode”. This mode controls how the house behaves when I’m not at home.</li>
  <li>When I disarm all alarm partitions, automation disables “Away Mode”. It means I’m back home.</li>
</ul>

<h3 id="night-mode">Night Mode</h3>

<ul>
  <li>Time based automation enables “Night Mode” when it’s time to sleep and disables it when it’s time to wake up. This mode controls how the house behaves when I’m sleeping.</li>
</ul>

<h3 id="eco-mode">Eco Mode</h3>

<ul>
  <li>Manual switch that controls heating and cooling systems. When I’m going to be away for a long time, I enable “Eco Mode” to save energy.</li>
</ul>

<h2 id="automations">Automations</h2>

<p>I organized automations in Home Assistant using <a href="https://www.home-assistant.io/docs/configuration/packages/">packages</a>.
It helps me to keep configuration clean and organized.</p>

<p><img src="/assets/images/2024-12-08-home-assistant-automations/automation_packages.png" alt="Packages" /></p>

<p>When I’m writing this blog post, I have over 50 automations configured in Home Assistant and the number is growing.
I will not list all of them here, but I will give you some examples.</p>

<h3 id="cctv">CCTV</h3>

<ul>
  <li>Automation enables CCTV detections when I leave home, and disables it when I come back. All cameras belong to the same group, so I can enable/disable them all at once.</li>
  <li>Even if I’m at home, automation enables CCTV detections when “Night Mode” is active.</li>
  <li>If CCTV recognizes a person, automation sends a notification to my phones with the screenshot of detected entity.</li>
  <li>From the notification I’m able to open CCTV live feed using <a href="https://companion.home-assistant.io/docs/notifications/actionable-notifications/">actionable notifications</a>.</li>
  <li>I can also snooze detections for 5 minutes to avoid getting notifications when I’ve already knew who is at the yard. This automation uses <a href="https://www.home-assistant.io/integrations/timer/">timer</a> for reliable snoozing.</li>
</ul>

<p><img src="/assets/images/2024-12-08-home-assistant-automations/cctv.jpg" alt="CCTV" /></p>

<h3 id="heating">Heating</h3>

<ul>
  <li>My heat pump uses heating curve to adjust heating power based on outside temperature. This is out of scope for my Home Assistant automations, but I have some automations to control house main thermostat. It enables me to adjust heating curve a bit based on my needs.</li>
  <li>Automation decreases heating temperature when “Eco Mode” is active.</li>
  <li>Automation turns off domestic hot water heating when “Eco Mode” is enabled.</li>
  <li>When I’m at home, automation increases heating one hour before “Night Mode” ends. It’s nice to wake up in warm house.</li>
  <li>Higher temperature back to normal 3 hours later.</li>
  <li>When I’m at home, automation increases heating 2 hours before “Night Mode” starts. It’s nice to take a shower in warm bathroom.</li>
  <li>Higher temperature back to normal 1 hour after “Night Mode” starts, it helps with drying floor and towels after shower.</li>
</ul>

<h3 id="facade-lights">Facade Lights</h3>

<ul>
  <li>Automation turns on facade lights in afternoon/evening when the <a href="https://www.home-assistant.io/integrations/sun/">Sun</a> is 4 degrees below the horizon. “Night Mode” turns them off.</li>
  <li>To help me wake me up in the morning, automation turns on facade lights when “Night Mode” ends. When the Sun is 1 degree below the horizon, lights are turned off.</li>
  <li>If I’m not at home, facade lights automation is off.</li>
</ul>

<p><img src="/assets/images/2024-12-08-home-assistant-automations/facade_lights.jpg" alt="Facade Lights" /></p>

<h3 id="water">Water</h3>

<ul>
  <li>Automation closes main water valve when I leave home, and opens it when I come back.</li>
  <li>The water valve remains open when dishwasher is running, and closes when it’s done.</li>
</ul>

<h3 id="dishwasher">Dishwasher</h3>

<ul>
  <li>When dishwasher is done, automation sends a notification to my son’s phone, unloading the dishwasher is his job.</li>
  <li>“Away Mode” disables dishwasher notifications.</li>
</ul>

<h3 id="waste-collection">Waste Collection</h3>

<ul>
  <li>Based on calendar, automation sends a notification day before waste collection to my phones. I can take out the trash in the evening.</li>
  <li>“Away Mode” disables waste collection notifications.</li>
</ul>

<p><img src="/assets/images/2024-12-08-home-assistant-automations/waste_collection_calendar.png" alt="Waste Collection" /></p>

<h3 id="power">Power</h3>

<ul>
  <li>When I’m out of home, automation disables electric sockets on the terrace.</li>
  <li>When UPS is running on battery, automation sends a notification about power outage to my phones.</li>
  <li>When UPS battery is below 20%, <a href="https://networkupstools.org/">NUT</a> shuts down my servers gracefully.</li>
  <li>From time to time, I switch off Zigbee controlled socket in my <a href="/blog/2024/06/30/homlab-hardware/">rack</a> to simulate power outage and test UPS. I’m going to automate this test in the future.</li>
</ul>

<h3 id="windows">Windows</h3>

<ul>
  <li>When I’m leaving home, automation sends a notification if any window is open.</li>
  <li>When I’m out of home, automation sends a notification if any window opens.</li>
</ul>

<h3 id="garage-gate">Garage Gate</h3>

<ul>
  <li>When alarm garage partition is armed, automation disables garage gate switch and enables it again when alarm is disarmed.</li>
  <li>Because my Zigbee garage switch is not a momentary switch, automation turns it off after 5 seconds.</li>
</ul>

<h3 id="fire">Fire</h3>

<ul>
  <li>When smoke detectors are triggered, besides the alarm sound, automation sends a notification to my phones.</li>
</ul>

<h2 id="summary">Summary</h2>

<p>I have a lot of fun with Home Assistant but I’m always ask my wife and son if they are happy with the automations 😀</p>

<p><img src="/assets/images/2024-12-08-home-assistant-automations/approved_by_wife.jpg" alt="Approved by Wife" /></p>]]></content><author><name>Marcin Kuthan</name></author><category term="DIY" /><category term="Homelab" /><summary type="html"><![CDATA[In this blog post, I will share how I use Home Assistant to automate my home in a pragmatic way. Pragmatic means that I’m not trying to automate everything, but only things that make sense to me. For example, for lights automation in the house, I use PIR or microwave motion detectors connected directly to lights, instead of fancy Zigbee switches.]]></summary></entry><entry><title type="html">Flink Forward 2024</title><link href="https://mkuthan.github.io/blog/2024/10/24/flink-forward/" rel="alternate" type="text/html" title="Flink Forward 2024" /><published>2024-10-24T00:00:00+00:00</published><updated>2024-10-24T00:00:00+00:00</updated><id>https://mkuthan.github.io/blog/2024/10/24/flink-forward</id><content type="html" xml:base="https://mkuthan.github.io/blog/2024/10/24/flink-forward/"><![CDATA[<p>This week, I attended Flink Forward in Berlin, Germany.
The event celebrated the 10th anniversary of Apache Flink.
Below, you can find my overall impressions of the conference and notes from several interesting sessions.
If an aspect was particularly appealing, I included a reference to supplementary materials.</p>

<p><img src="/assets/images/2024-10-24-flink-forward/intro.jpg" alt="Intro" /></p>

<p class="notice--info">I don’t use Flink on a daily basis, but I hoped to gain some inspiration that I could apply to my real-time data pipelines running on GCP Dataflow.</p>

<h2 id="keynotes">Keynotes</h2>

<ul>
  <li>Flink 2.0 <a href="https://www.ververica.com/blog/embracing-the-future-apache-flink-2.0">announced</a> during the first day of the conference, perfect timing</li>
  <li>Stephan Ewen with Feng Wang presented 15 years of the project history
Apache Flink, which emerged around 2014, originally started as the Stratosphere project at German universities in 2009</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/keynotes1.jpg" alt="Keynotes" /></p>

<ul>
  <li>Kafka fails short in real-time streaming analytics</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/keynotes2.jpg" alt="Keynotes" /></p>

<ul>
  <li>Truly unified batch and streaming with <a href="https://www.ververica.com/blog/apache-paimon-the-streaming-lakehouse">Apache Paimon: Streaming Lakehouse</a>.</li>
  <li>Fluss: Streaming storage for next-gen data analytics, it’s going to be open-sourced soon.
Apologies for the low quality picture.</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/keynotes3.jpg" alt="Keynotes" /></p>

<h2 id="revealing-the-secrets-of-apache-flink-20">Revealing the secrets of Apache Flink 2.0</h2>

<ul>
  <li>Disaggregated state storage: Goodbye <a href="http://rocksdb.org/">RocksDB</a></li>
  <li>External shuffle service: Welcome <a href="https://celeborn.apache.org">Apache Celeborn</a></li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/disaggregated_state_storage.jpg" alt="Disaggregated state storage" /></p>

<ul>
  <li>Streaming Lakehouse: An enabler for unified batch and streaming in an innovative way</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/streaming_lakehouse.jpg" alt="Streaming lakehouse" /></p>

<ul>
  <li>Breaking changes: While it’s unfortunate that the <em>Scala API</em> is deprecated, there’s a new extension available: <a href="https://github.com/flink-extended/flink-scala-api">Flink Scala API</a>.</li>
  <li>PMC members mentioned that they’re going to modernize the Java Streaming API soon. It’s the oldest and hardest-to-maintain part of the Flink API.</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/breaking_changes.jpg" alt="Breaking changes" /></p>

<h2 id="flink-autoscaling-a-year-in-review---performance-challenges-and-innovations">Flink autoscaling: A year in review - performance, challenges and innovations</h2>

<ul>
  <li>Autoscaling example, simplified but self-explanatory.</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/autoscaling_example.jpg" alt="Autoscaling example" /></p>

<ul>
  <li>Challenges: Unfortunately, the speaker struggled with time management and couldn’t delve into the details.
The key lesson for me: don’t scale up if there is no effect of scaling.
Dataflow engineering team - can you hear me?</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/autoscaling_challenges.jpg" alt="Autoscaling challenges" /></p>

<ul>
  <li>Memory management in Flink, for me looks like a configuration and tuning nightmare.
Flink autoscaling should help, see: <a href="https://cwiki.apache.org/confluence/display/FLINK/FLIP-271%3A+Autoscaling">FLIP-271</a>.</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/memory_model.jpg" alt="Memory model" /></p>

<h2 id="scaling-flink-in-the-real-world-insights-from-running-flink-for-five-years-at-stripe">Scaling Flink in the real world: Insights from running Flink for five years at Stripe</h2>

<ul>
  <li>The best session of the first day, in my opinion!</li>
  <li>I’m sure that Ben Augarten from Stripe knows how to manage Flink clusters and jobs at scale.</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/stripe_intro.jpg" alt="Stripe" /></p>

<ul>
  <li>With tight SLOs, there isn’t time for manual operations.
If a job fails, roll back using the previously saved job graph.
How do you decide if a job fails in a generic way? You should listen to the session.</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/stripe_rollbacks.jpg" alt="Stripe" /></p>

<ul>
  <li>Use a proxy in front of the Kafka cluster to prevent jobs from getting stuck if a Kafka partition leader becomes unavailable.
See: <a href="https://www.confluent.io/events/kafka-summit-london-2022/6-nines-how-stripe-keeps-kafka-highly-available-across-the-globe/">How Stripe keeps Kafka highly available across the globe</a></li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/stripe_kafka.jpg" alt="Stripe" /></p>

<ul>
  <li>Shared Zookeepers and shared Flink clusters can lead to issues with noisy neighbors and the propagation of failures. Extra operational costs are worth it to support system stability and performance.</li>
</ul>

<h2 id="visually-diagnosing-operator-state-problems">Visually diagnosing operator state problems</h2>

<ul>
  <li>How to track the flow of data and identify where things go wrong?</li>
  <li>Can you inspect each late data record and figure out why it was late?</li>
  <li>Do you want to know what your state is before and after each step in your job?</li>
  <li>See also <a href="https://datorios.com/blog/the-murky-waters-of-debugging-in-apache-flink/">The Murky Waters of Debugging in Apache Flink: Is it a Black Box?</a></li>
  <li>Excellent logo, isn’t it?</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/flink_xray.jpg" alt="Datorios" /></p>

<h2 id="zero-interference-and-resource-congestion-in-flink-clusters-with-kafka-data-sources">Zero interference and resource congestion in Flink clusters with Kafka data sources</h2>

<ul>
  <li>Another session focused on current Kafka limitations</li>
  <li>Mitigation strategies for noisy neighbors in Kafka: quotas and cluster mirroring</li>
  <li>Introducing WarpStream: <a href="https://www.confluent.io/blog/confluent-acquires-warpstream/">Confluent has acquired WarpStream</a></li>
  <li>Stateless, leaderless brokers</li>
  <li>Using object storage for state management: expect higher latency, but it should be acceptable for most use cases</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/warpstream.jpg" alt="WarpStream" /></p>

<h2 id="from-apache-flink-to-restate---event-processing-for-analytics-and-transactions">From Apache Flink to Restate - Event processing for analytics and Transactions</h2>

<ul>
  <li>A new business idea from one of the Flink founders: shift the focus from analytical to transactional processing.</li>
  <li>Apply resilience and consistency lessons learned from building Flink to distributed transactional, RPC-based applications.</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/restate_intro.jpg" alt="Restate Intro" /></p>

<ul>
  <li>In simple terms, it resembles an orchestrated <a href="https://blog.bytebytego.com/p/the-saga-pattern">Saga</a> pattern</li>
  <li>Durable and reliable async/await</li>
  <li>See <a href="https://restate.dev/blog/why-we-built-restate/">Why we built Restate</a></li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/restate_durable_execution.jpg" alt="Restate Durable Execution" /></p>

<h2 id="enabling-flinks-cloud-native-future-introducing-forst-db-in-flink-20">Enabling Flink’s Cloud-Native Future: Introducing ForSt DB in Flink 2.0</h2>

<ul>
  <li>The problem, local state doesn’t fit cloud native architecture.</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/forst1.jpg" alt="Large state" /></p>

<ul>
  <li>ForSt (for streaming) DB architecture</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/forst2.jpg" alt="ForSt DB architecture" /></p>

<ul>
  <li>Performance dropped 100x when RocksDB was replaced with object store as is.</li>
  <li>The new async API requires changes in <strong>all</strong> Flink operators!</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/forst3.jpg" alt="State async API" /></p>

<ul>
  <li>Asynchronous improves performance but introduces new challenge: ordering</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/forst4.jpg" alt="Ordering" /></p>

<ul>
  <li>Slower than local RocksDB, but performance looks promising</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/forst5.jpg" alt="Benchmark" /></p>

<h2 id="building-copilots-with-flink-sql-llms-and-vector-databases">Building Copilots with Flink SQL, LLMs and vector databases</h2>

<ul>
  <li>The most entertaining session of the conference, in my opinion</li>
  <li>How to adopt real-time analysis for non-technical users?</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/genai1.jpg" alt="Real-time analysis adoption" /></p>

<ul>
  <li>Steffen Hoellinger invited us to conduct a POC together with <a href="https://airy.co/">Airy</a></li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/genai3.jpg" alt="Copilot architecture" /></p>

<ul>
  <li>Hmm, some technical knowledge is still required 😀</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/genai2.jpg" alt="Sample session" /></p>

<ul>
  <li>Key lesson: context is much more important than model</li>
  <li>Keep small workspaces to avoid hallucinations</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/genai4.jpg" alt="Context vs Model" /></p>

<ul>
  <li>Flink SQL ML models, see: <a href="https://cwiki.apache.org/confluence/display/FLINK/FLIP-437%3A+Support+ML+Models+in+Flink+SQL">FLIP-437</a></li>
</ul>

<h2 id="materialized-table---making-your-data-pipeline-easier">Materialized Table - Making Your Data Pipeline Easier</h2>

<ul>
  <li>The most eye opening session</li>
  <li>Batch, incremental and real-time unification</li>
  <li>Backfill</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/materialized_table.jpg" alt="Materialized Table" /></p>

<ul>
  <li>Freshness vs Cost</li>
  <li>Apply <code class="language-plaintext highlighter-rouge">SET FRESHNESS = INTERVAL 1 HOUR</code> and framework will do the rest</li>
  <li>Support for most SQL queries (without <code class="language-plaintext highlighter-rouge">ORDER BY</code> cause)</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/freshness_vs_cost.jpg" alt="Freshness vs Cost" /></p>

<ul>
  <li>Cool demo, materialized view freshness changed, Flink jobs rescheduled and BI dashboard updated in-place.
Yet another scenario for backfill.</li>
  <li>Community version coming soon, see <a href="https://cwiki.apache.org/confluence/display/FLINK/FLIP-435%3A+Introduce+a+New+Materialized+Table+for+Simplifying+Data+Pipelines">FLIP-435</a>.</li>
</ul>

<h2 id="event-tracing">Event tracing</h2>

<ul>
  <li>Session based on IoT vehicle data in Mercedes-Benz.</li>
  <li>Apply <a href="https://opentelemetry.io/">OpenTelemetry</a> for real-time data pipelines</li>
  <li>Tracing events sampling to avoid negative performance impact</li>
  <li>Kafka sources: extract tracing from headers</li>
  <li>Flink steps: attach spans to all events</li>
  <li>Kafka sinks: add tracing to headers</li>
  <li>In-summary: a lot of extra work</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/telemetry.jpg" alt="Telemetry" /></p>

<h2 id="summary">Summary</h2>

<p>Attending the conference was a valuable experience, offering deep insights into the latest developments in Apache Flink.
Here are my key takeaways:</p>

<ul>
  <li>Listening to sessions about the challenges of Flink deployment and operations from the trenches made me appreciate the simplicity of <a href="https://cloud.google.com/products/dataflow">Dataflow</a> even more.</li>
  <li>I now believe in the potential of truly unified batch and streaming processing.
FLIP-435 and the streaming lakehouse give hope that <code class="language-plaintext highlighter-rouge">SET FRESHNESS</code> could switch processing modes from batch, through incremental, to real-time.</li>
  <li>For high adoption of real-time analytics, consider using GenAI to hide the underlying data pipelines complexity.</li>
  <li>My general impression is that Kafka’s limitations in the cloud-native era have been confirmed.</li>
</ul>

<p><img src="/assets/images/2024-10-24-flink-forward/venue.jpg" alt="Summary" /></p>]]></content><author><name>Marcin Kuthan</name></author><category term="Conferences" /><category term="Apache Flink" /><category term="Apache Kafka" /><summary type="html"><![CDATA[This week, I attended Flink Forward in Berlin, Germany. The event celebrated the 10th anniversary of Apache Flink. Below, you can find my overall impressions of the conference and notes from several interesting sessions. If an aspect was particularly appealing, I included a reference to supplementary materials.]]></summary></entry><entry><title type="html">Foundations of scalable systems – book review</title><link href="https://mkuthan.github.io/blog/2024/10/07/foundations-of-scalable-systems/" rel="alternate" type="text/html" title="Foundations of scalable systems – book review" /><published>2024-10-07T00:00:00+00:00</published><updated>2024-10-07T00:00:00+00:00</updated><id>https://mkuthan.github.io/blog/2024/10/07/foundations-of-scalable-systems</id><content type="html" xml:base="https://mkuthan.github.io/blog/2024/10/07/foundations-of-scalable-systems/"><![CDATA[<p><a href="https://www.oreilly.com/library/view/foundations-of-scalable/9781098106058/">Foundations of scalable systems</a> written by <a href="https://www.linkedin.com/in/gortonator/">Ian Gorton</a>, the book with my highest rate of 5 ⭐️
I highly recommend that every software engineer grasps the distributed system principles outlined in this book.</p>

<p><img src="/assets/images/2024-10-07-foundations-of-scalable-systems/bookcover.jpeg" alt="Foundations of scalable systems" /></p>

<p>You can say that this is a book about the basics, but these basics concern the construction of distributed systems, which isn’t an easy subject.
For less experienced engineers, this book reduces the area of unknown unknowns, turning them into known unknowns.
A lot of references allow them to deepen their knowledge as needed.
Experienced engineers can treat this book as a refresher and identify areas where they need to supplement their knowledge.</p>

<p>My general observations during the lecture:</p>

<p><strong>Clear and Structured Approach</strong>: The book explains complex principles in a clear and organized manner.</p>

<p><strong>Wide Knowledge Coverage</strong>: It provides a broad overview rather than deep dives into specific topics.</p>

<p><strong>Excellent References</strong>: The book includes many references to whitepapers, books, articles, and websites for further reading.</p>]]></content><author><name>Marcin Kuthan</name></author><category term="Books" /><category term="Software Engineering" /><category term="Architecture" /><summary type="html"><![CDATA[Foundations of scalable systems written by Ian Gorton, the book with my highest rate of 5 ⭐️ I highly recommend that every software engineer grasps the distributed system principles outlined in this book.]]></summary></entry><entry><title type="html">Building evolutionary architectures – book review</title><link href="https://mkuthan.github.io/blog/2024/09/26/building-evolutionary-architecture/" rel="alternate" type="text/html" title="Building evolutionary architectures – book review" /><published>2024-09-26T00:00:00+00:00</published><updated>2024-09-26T00:00:00+00:00</updated><id>https://mkuthan.github.io/blog/2024/09/26/building-evolutionary-architecture</id><content type="html" xml:base="https://mkuthan.github.io/blog/2024/09/26/building-evolutionary-architecture/"><![CDATA[<p>September this year is dedicated to reviewing software architecture books.
This time, I’ve read <a href="https://www.oreilly.com/library/view/building-evolutionary-architectures/9781492097532/">Building Evolutionary Architectures</a> written by <a href="https://nealford.com">Neal Ford</a>.
I appreciate the effort put into compiling this book.
However, I found it to be more of a collection of existing books, articles and talks rather than offering new, original insights.</p>

<p><img src="/assets/images/2024-09-26-building-evolutionary-architecture/bookcover.jpeg" alt="Building Evolutionary Architectures" /></p>

<p>Below is my brief summary of the book:</p>

<ul>
  <li><strong>Rapid Business Evolution</strong>: The pace of business change is accelerating, necessitating that software systems keep up. Proper architecture is crucial for enabling software evolution.</li>
  <li><strong>Embracing Incremental Changes</strong>: Software engineers can’t predict unknown unknowns. By embracing incremental changes to test hypotheses, similar to evolutionary processes, failed experiments are discarded while successful ones are retained.</li>
  <li><strong>Short Release Cycles</strong>: Release cycles must be short. There is no time for manual, repetitive tasks; therefore, everything must be automated.</li>
</ul>]]></content><author><name>Marcin Kuthan</name></author><category term="Books" /><category term="Software Engineering" /><category term="Architecture" /><summary type="html"><![CDATA[September this year is dedicated to reviewing software architecture books. This time, I’ve read Building Evolutionary Architectures written by Neal Ford. I appreciate the effort put into compiling this book. However, I found it to be more of a collection of existing books, articles and talks rather than offering new, original insights.]]></summary></entry><entry><title type="html">Fundamentals of software architecture – book review</title><link href="https://mkuthan.github.io/blog/2024/09/12/fundamentals-of-software-architecture/" rel="alternate" type="text/html" title="Fundamentals of software architecture – book review" /><published>2024-09-12T00:00:00+00:00</published><updated>2024-09-12T00:00:00+00:00</updated><id>https://mkuthan.github.io/blog/2024/09/12/fundamentals-of-software-architecture</id><content type="html" xml:base="https://mkuthan.github.io/blog/2024/09/12/fundamentals-of-software-architecture/"><![CDATA[<p>Recently I’ve read <a href="https://www.oreilly.com/library/view/fundamentals-of-software/9781492043447/">Fundamentals of Software Architecture</a>
written by <a href="https://www.linkedin.com/in/markrichards3/">Mark Richards</a>
and <a href="https://nealford.com">Neal Ford</a>.
I found this book valuable, even though my company doesn’t have a formal architect role.
At Allegro, the most experienced senior software engineers take on the responsibilities of a software architect in addition to their regular development duties.</p>

<p><img src="/assets/images/2024-09-12-fundamentals-of-software-architecture/bookcover.jpg" alt="Fundamentals of Software Architecture" /></p>

<p>At the end of the book, there is a self-assessment section, which I partially summarized by writing my answers in this blog post.</p>

<h2 id="what-are-the-four-dimensions-that-define-software-architecture">What are the four dimensions that define software architecture</h2>

<ol>
  <li>Architecture Characteristics: These define the success criteria of a system, such as performance, scalability, and security. They’re orthogonal to the system’s functionality.</li>
  <li>Structure: This refers to the type of architecture style or styles used in the system, such as microservices, layered, or microkernel architectures.</li>
  <li>Architecture Decisions: These are the rules and guidelines that dictate how a system should be constructed. They include choices about technologies, frameworks, and design patterns.</li>
  <li>Design Principles: These are guidelines that help development teams make decisions about how to implement the system. They’re not hard-and-fast rules but rather best practices to follow.</li>
</ol>

<h2 id="whats-the-difference-between-an-architecture-decision-and-a-design-principle">What’s the difference between an architecture decision and a design principle</h2>

<p>Architecture decisions are specific choices that define the architecture, while design principles are broader guidelines that influence those choices.</p>

<h2 id="list-the-eight-core-expectations-of-a-software-architect">List the eight core expectations of a software architect</h2>

<ol>
  <li>Make architecture decisions</li>
  <li>Continually analyze the architecture</li>
  <li>Keep current with latest trends</li>
  <li>Ensure compliance with decisions</li>
  <li>Diverse exposure and experience</li>
  <li>Have business domain knowledge</li>
  <li>Possess interpersonal skills</li>
  <li>Understand and navigate politics</li>
</ol>

<h2 id="whats-the-first-law-of-software-architecture">What’s the first law of software architecture</h2>

<p>Everything is a tradeoff</p>

<h2 id="list-the-three-levels-of-knowledge-in-the-knowledge-triangle">List the three levels of knowledge in the knowledge triangle</h2>

<ol>
  <li>Stuff you know</li>
  <li>Stuff you know you don’t know</li>
  <li>Stuff you don’t know you don’t know</li>
</ol>

<h2 id="what-are-the-ways-of-remaining-hands-on-as-an-architect">What are the ways of remaining hands-on as an architect</h2>

<ul>
  <li>Coding regularly</li>
  <li>Side projects</li>
  <li>Pair programming</li>
  <li>Technical reading</li>
  <li>Training and courses</li>
  <li>Code reviews</li>
  <li>Prototyping and experimentation</li>
  <li>Mentoring</li>
</ul>

<h2 id="whats-meant-by-the-term-connascence">What’s meant by the term connascence</h2>

<p>Describe the degree to which different parts of a system are interdependent</p>

<h2 id="whats-the-difference-between-static-and-dynamic-connascence">What’s the difference between static and dynamic connascence</h2>

<ul>
  <li>Static connascence occurs at the source code level, and developers can identify it by examining the code itself.</li>
  <li>Dynamic connascence is related to the runtime behavior of the system and developers can only identify it during execution, such as the order of calls.</li>
</ul>

<h2 id="whats-the-strongest-form-of-connascence">What’s the strongest form of connascence</h2>

<p>Connascence of Identity: This occurs when many components must reference the same entity, meaning any change to the identity of that entity requires changes across all components that reference it.
For example, if the format or structure of the entity identifier is changed from a numeric to an alphanumeric code, you need to update every module that references this entity.</p>

<h2 id="whats-the-weakest-form-of-connascence">What’s the weakest form of connascence</h2>

<p>Connascence of Name, this occurs when many components must agree on the name of an entity. For example, if the name of a method changes, all references to that method must also be updated.
Usually straightforward to manage and refactor with IDE.</p>

<h2 id="which-is-preferred-static-or-dynamic-connascence">Which is preferred static or dynamic connascence</h2>

<p>Static connascence refers to dependencies that the compiler can check at compile time, such as type checking and method signatures.
Dynamic connascence, on the other hand, involves dependencies that are only checked at runtime, such as dynamic method calls or reflection.</p>

<p>In a code base, it’s preferable to use static connascence over dynamic connascence.</p>

<h2 id="give-an-example-of-an-operational-characteristic">Give an example of an operational characteristic</h2>

<ul>
  <li>Availability</li>
  <li>Continuity (disaster recovery capability)</li>
  <li>Performance</li>
  <li>Recoverability</li>
  <li>Reliability/safety</li>
  <li>Robustness</li>
  <li>Scalability</li>
</ul>

<h2 id="give-an-example-of-a-structural-characteristic">Give an example of a structural characteristic</h2>

<ul>
  <li>Configurability</li>
  <li>Extensibility</li>
  <li>Installability</li>
  <li>Leverageability (ability to reuse common components)</li>
  <li>Localization</li>
  <li>Maintainability</li>
  <li>Portability</li>
  <li>Upgradeability</li>
</ul>

<h2 id="give-an-example-of-a-cross-cutting-characteristic">Give an example of a cross-cutting characteristic</h2>

<ul>
  <li>Accessibility</li>
  <li>Archivability (will the data need to be archived or deleted after a period of time)</li>
  <li>Authentication</li>
  <li>Authorization</li>
  <li>Legal</li>
  <li>Privacy</li>
  <li>Security</li>
  <li>Supportability</li>
  <li>Usability/achievability (level of training required for users to achieve their goals with the application)</li>
</ul>

<h2 id="why-its-a-good-practice-to-limit-the-number-of-characteristics-an-architecture-should-support">Why it’s a good practice to limit the number of characteristics an architecture should support</h2>

<ul>
  <li>Avoiding complexity: Keep the architecture simpler and more understandable.</li>
  <li>Resource allocation: Ensure that resources are effectively allocated to the most critical aspects of the system.</li>
  <li>Trade-off management: Ensure that the system meets its most important goals without being overburdened by conflicting requirements.</li>
</ul>

<h2 id="whats-an-architectural-quantum">What’s an architectural quantum</h2>

<p>It refers to the smallest unit of an architecture that can be independently deployed and tested, encompassing all the necessary components to fulfill a specific business function.</p>

<h2 id="whats-the-difference-between-technical-partitioning-and-domain-partitioning">What’s the difference between technical partitioning and domain partitioning</h2>

<p>Technical partitioning focuses on technical roles, while domain partitioning focuses on business functionality. Domain partitioning often makes it easier to manage changes in business requirements, whereas technical partitioning can complicate changes due to inter-layer dependencies.</p>

<h2 id="under-what-circumstances-would-technical-partitioning-be-a-better-choice-over-domain-partitioning">Under what circumstances would technical partitioning be a better choice over domain partitioning</h2>

<ul>
  <li>Small or simple applications</li>
  <li>Homogeneous teams, for example: front-end developers, back-end developers</li>
  <li>Legacy systems with technical partitioning</li>
  <li>Standardized processes, for example: compliance with certain regulations or industry standards</li>
  <li>Performance optimization</li>
</ul>

<h2 id="list-the-eight-fallacies-of-distributed-computing">List the eight fallacies of distributed computing</h2>

<ol>
  <li>The network is reliable</li>
  <li>Latency is zero</li>
  <li>Bandwidth is infinite</li>
  <li>The network is secure</li>
  <li>Topology doesn’t change</li>
  <li>There is one administrator</li>
  <li>Transport cost is zero</li>
  <li>The network is homogeneous</li>
</ol>

<h2 id="whats-stamp-coupling">What’s stamp coupling</h2>

<p>Stamp coupling, also known as data-structured coupling, occurs when modules share a composite data structure and use only parts of it.
This can lead to issues where changes in the unused parts of the data structure might affect the module that doesn’t need those parts.</p>

<h2 id="whats-the-difference-between-an-open-layer-and-a-closed-layer">What’s the difference between an open layer and a closed layer</h2>

<p>An open layer permits requests to bypass it and directly access any layer below it.
A closed layer requires that all requests pass through it before reaching any lower layer.</p>

<h2 id="whats-the-architecture-sinkhole-anti-pattern">What’s the architecture sinkhole anti-pattern</h2>

<p>The architecture sinkhole anti-pattern occurs when requests pass through multiple layers of an architecture without any significant processing or logic being applied at each layer.
Essentially, the layers act as mere pass-throughs, adding unnecessary complexity and overhead without providing any real value.</p>

<h2 id="name-the-four-types-of-filters-and-their-purpose-in-pipeline-architecture">Name the four types of filters and their purpose in pipeline architecture</h2>

<ul>
  <li>Input filters: Transform raw data into a form that’s suitable for later processing stages.</li>
  <li>Transform filters: Enrich the data or apply business logic.</li>
  <li>Output filter: Convert the data into the final format and write it to the destination.</li>
  <li>Error filters: Handle exceptions and errors that may occur during processing.</li>
</ul>

<h2 id="in-what-way-does-the-pipeline-architecture-support-modularity">In what way does the pipeline architecture support modularity</h2>

<ul>
  <li>Separation of Concerns: Each step in the pipeline has a specific task, such as handling input, performing transformations, managing output, or handling errors.</li>
  <li>Reusability: Steps can be designed as reusable modules that can be plugged into different pipelines.</li>
  <li>Scalability: Individual steps can optimize, replace, or replicate without affecting other parts of the pipeline.</li>
  <li>Testing and debugging: Each step allows independent testing and debugging, making it easier to identify and fix issues.</li>
</ul>

<h2 id="whats-domainarchitecture-isomorphism">What’s domain/architecture isomorphism</h2>

<p>Principle where the structure of the software architecture closely mirrors the structure of the problem domain it’s designed to address.
For example, in an operating system, the micro-kernel architecture’s minimal core will handle only the most fundamental aspects of the system (like process communication and basic I/O), while other services (like file systems and device drivers) will be handled by external modules.</p>

<h2 id="whats-a-primary-difference-between-broker-and-mediator-topologies">What’s a primary difference between broker and mediator topologies</h2>

<ul>
  <li>In a broker topology, individual components or services communicate with each other through a message broker.
Services are loosely coupled because they don’t need to know each other’s location or protocol.</li>
  <li>In a mediator topology, a mediator orchestrates and manages the communication between services.
This approach is suitable for complex interactions that require coordination, transformation, and aggregation of many services.</li>
</ul>

<h2 id="whats-a-primary-aspect-fo-space-based-architecture-that-differentiates-in-from-other-architecture-styles">What’s a primary aspect fo space-based architecture that differentiates in from other architecture styles</h2>

<p>It splits both the processing and the storage across many servers.
The Space Based Architecture pattern is designed to offer high scalability, fault-tolerance, and low-latency data access by storing data in memory across many nodes.</p>

<h2 id="name-the-four-components-that-make-up-the-virtualized-middleware-within-a-space-based-architecture">Name the four components that make up the virtualized middleware within a space-based architecture</h2>

<ul>
  <li>Message grid: Manages input request and session information</li>
  <li>Data grid: Manage the data replication between processing units when data updates occur</li>
  <li>Processing grid: Manages distributed request processing when there are many processing units, each handling a part of the application</li>
  <li>Deployment manager: Manages the dynamic startup and shutdown of processing units based on load conditions</li>
</ul>

<h2 id="whats-a-difference-between-replicated-cache-and-distributed-cache">What’s a difference between replicated cache and distributed cache</h2>

<p>Replicated cache:</p>

<ul>
  <li>Same data replicated to all nodes</li>
  <li>High fault tolerance, all nodes have the same data</li>
  <li>Low latency for read operations because each node has a full copy of the data</li>
  <li>Useful when read operations significantly outweigh write operations</li>
</ul>

<p>Distributed cache:</p>

<ul>
  <li>Stores different pieces of data on different cache nodes</li>
  <li>Lower compared to replicated cache</li>
  <li>Can be higher for read operations, since data might not be present on the local node</li>
  <li>Suitable for large datasets and when read and write operations are balanced</li>
</ul>]]></content><author><name>Marcin Kuthan</name></author><category term="Books" /><category term="Software Engineering" /><category term="Architecture" /><summary type="html"><![CDATA[Recently I’ve read Fundamentals of Software Architecture written by Mark Richards and Neal Ford. I found this book valuable, even though my company doesn’t have a formal architect role. At Allegro, the most experienced senior software engineers take on the responsibilities of a software architect in addition to their regular development duties.]]></summary></entry></feed>