<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>InfluxData Blog - Ben Tasker</title>
    <description>Posts by Ben Tasker on the InfluxData Blog</description>
    <link>https://www.influxdata.com/blog/author/ben-tasker/</link>
    <language>en-us</language>
    <lastBuildDate>Tue, 18 Feb 2025 07:00:00 +0000</lastBuildDate>
    <pubDate>Tue, 18 Feb 2025 07:00:00 +0000</pubDate>
    <ttl>1800</ttl>
    <item>
      <title>Improving Our Solar Battery Savings</title>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally posted on &lt;a href="https://www.bentasker.co.uk/posts/blog/house-stuff/improving-the-energy-savings-delivered-by-our-solar-battery.html"&gt;www.bentasker.co.uk&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This project was built using a previous version of InfluxDB. &lt;a href="https://www.influxdata.com/products/influxdb-overview/"&gt;InfluxDB 3&lt;/a&gt; moved away from Flux and a built-in task engine. Users can use external tools, like Python-based &lt;a href="https://www.quix.io/kapacitor-alternative"&gt;Quix&lt;/a&gt;, to create tasks in InfluxDB 3.&lt;/em&gt;
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/4124856f485b4e879db963faf1ec1f6b/357bb6ac111da8ff362322a80c6744ff/unnamed.jpg" alt="" /&gt;
Last month, I analyzed the performance of our solar install. I found that, while our solar battery was generating daily savings, it would likely &lt;a href="https://www.bentasker.co.uk/posts/blog/house-stuff/calculating-the-break-even-period-of-our-solar-battery.html#adjusted_break_even_stats"&gt;never save enough to offset its purchase cost&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There were a number of factors involved, but one of the bigger ones was the battery’s not always being sufficiently charged, with our average max daily charge level &lt;a href="https://www.bentasker.co.uk/posts/blog/house-stuff/calculating-the-break-even-period-of-our-solar-battery.html#charge_level"&gt;being 72%&lt;/a&gt;. The battery gets a full charge on sunny days, but British weather being what it is, there are plenty of days dragging that average down.&lt;/p&gt;

&lt;p&gt;As I noted at the time, solar energy isn’t the only source from which the battery can be charged: there’s also the option of charging from the grid (ideally when prices are low).&lt;/p&gt;

&lt;p&gt;So, in the month since, that’s exactly what I’ve done.&lt;/p&gt;

&lt;p&gt;In this post, I will describe how I’ve configured things, analyze the impact of the change, and  talk about some changes that my electricity supplier (&lt;a href="https://octopus.energy/"&gt;Octopus&lt;/a&gt;) has recently made. Energy consumption monitoring is a &lt;a href="https://www.influxdata.com/what-is-time-series-data/"&gt;time series&lt;/a&gt; use case so I write the metrics that I collect into InfluxDB. It’s designed specifically for time series, which allows me to easily visualize data in real-time as well as look at historic data.&lt;/p&gt;

&lt;h2 id="configuration"&gt;Configuration&lt;/h2&gt;

&lt;p&gt;It  was my intention to build automation that consumes pricing information and dynamically controls &lt;em&gt;when&lt;/em&gt; the battery should charge. There’s a wealth of information out there on &lt;a href="https://www.youtube.com/watch?v=vmeEd2ljXtQ"&gt;how to control a Solis Inverter from HomeAssistant&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, as soon as I tried to set this up, I ran into a fundamental issue: all the setup guides and tools rely on older data sticks.&lt;/p&gt;

&lt;p&gt;My inverter (a Solis &lt;code class="language-markup"&gt;RHI-3.6K-48ES-EG&lt;/code&gt;) came with a third-generation stick (model &lt;code class="language-markup"&gt;S3-WIFI-ST&lt;/code&gt;), which, unlike older counterparts, does not expose Modbus locally. As a result, tools like &lt;a href="https://github.com/NosIreland/solismod"&gt;solismod&lt;/a&gt; and &lt;code class="language-markup"&gt;pysolarman&lt;/code&gt; &lt;a href="https://github.com/jmccrohan/pysolarmanv5/issues/8#issuecomment-1172739268"&gt;cannot work&lt;/a&gt; with them.&lt;/p&gt;

&lt;p&gt;There is actually a guide to &lt;a href="https://github.com/Jumpy07/Solis---SolisCloud-and-Home-Assistant"&gt;getting things working using a rs485 splitter&lt;/a&gt;, but I’ve not tried it yet (in fact, I only found it while double-checking the links above as part of drafting this post).&lt;/p&gt;

&lt;p&gt;However, Soliscloud has an interface that allows you to set the registers on your inverter manually. It’s not enabled by default, so access &lt;a href="https://solis-service.solisinverters.com/en/support/solutions/articles/44002373796-inverter-remote-control-application"&gt;needs to be requested&lt;/a&gt;. Once granted, there’s no API access (update: &lt;a href="https://www.bentasker.co.uk/posts/blog/house-stuff/controlling-charge-schedules-with-homeassistant-and-soliscloud.html"&gt;there is now&lt;/a&gt;), but it does what it says on the tin.&lt;/p&gt;

&lt;p&gt;The navigation interface is quite simplistic:
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/4371180c7df340ae9f06e8fe21b30600/fb17eecb0ea0c7c04f5f8a17f7788239/unnamed.png" alt="Screenshot of the top level menu of the Soliscloud Remote Control interface" /&gt;
Solis still seems to be working on improving it, as several things have moved around/changed since I first started playing with the interface.&lt;/p&gt;

&lt;p&gt;The default mode is &lt;code class="language-markup"&gt;Self-Consumption&lt;/code&gt; (basically, it prioritizes household demand over export revenue). Within that menu there are some additional options:
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/10eac808cf084a26907c116cc097d4f4/074e4b534b2e6365d9efad2be5c74fc9/unnamed.png" alt="Screenshot of the Self-Use menu of the Soliscloud Remote Control interface. There are multiple buttons including Time of Use Switch and Charge and Discharge" /&gt;
The &lt;code class="language-markup"&gt;Charge and Discharge&lt;/code&gt; button opens a side pane that allows charge times and rates to be configured.
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/69dcd3f7cff44bcb89f8b6e14f08eb39/232638dd1d0733370482dfdf16e9544f/unnamed.png" alt="Screenshot of the Self-Use menu of the Soliscloud Remote Control interface" /&gt;
It’s worth noting here that, to a certain extent, it doesn’t really matter &lt;em&gt;what&lt;/em&gt; you enter as a charge rate (as long as it’s not too low): the Battery Management System (BMS) will not allow the battery to charge faster than it’s capable of. Still, it is better to err on the side of caution in case that ever changes.&lt;/p&gt;

&lt;p&gt;The changes made here, though, will have &lt;em&gt;absolutely no impact&lt;/em&gt; until the schedule is enabled.&lt;/p&gt;

&lt;p&gt;On the Self-Use menu is a button labeled &lt;code class="language-markup"&gt;Time of Use Switch&lt;/code&gt;. Anything ending in &lt;code class="language-markup"&gt;Switch&lt;/code&gt; is essentially a boolean—you turn it on or off. In this case, the switch defines whether or not the Charge/Discharge schedule is observed.&lt;/p&gt;

&lt;h2 id="adjusting-time-of-use"&gt;Adjusting time of use&lt;/h2&gt;

&lt;p&gt;In my previous post, one of the mitigations that I suggested was adjusting settings so that energy would only be drawn from the battery when most valuable (i.e., at peak times). The idea was that any energy remaining after peak would be carried forward into the next day’s peak rather than trickling away overnight.&lt;/p&gt;

&lt;p&gt;I did look into doing this, but unfortunately, the inverter’s settings don’t quite work that way:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;If you set the &lt;code class="language-markup"&gt;Discharge Current&lt;/code&gt; field to &lt;code class="language-markup"&gt;0&lt;/code&gt; and specify a time range, the battery will not discharge during that time (good!)&lt;/li&gt;
  &lt;li&gt;However, it will also consider it a discharge period and will, therefore, not charge the battery (boooo!)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, although you &lt;em&gt;can&lt;/em&gt; technically set the battery to only discharge at certain times, it probably won’t have any charge to discharge in the first place.&lt;/p&gt;

&lt;p&gt;In fiddling around to get this working, I learned a few things about the Solis Inverter’s scheduling:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;If you set a charge period, the battery will charge at that rate, taking energy from the grid to top-up whatever the panels are delivering.&lt;/li&gt;
  &lt;li&gt;If you set a &lt;em&gt;discharge&lt;/em&gt; period, the battery will constantly discharge at the specified rate, exporting energy to the grid if there’s insufficient household demand to consume it (this took me by surprise, resulting in me accidentally dumping a battery load to the grid shortly before peak time started).&lt;/li&gt;
  &lt;li&gt;There isn’t a way (other than setting a discharge period), within &lt;code class="language-markup"&gt;Self-Use&lt;/code&gt;, to tell the inverter not to charge the battery from the panels.&lt;/li&gt;
  &lt;li&gt;If you switch to &lt;code class="language-markup"&gt;Grid Feedback Priority&lt;/code&gt; the meaning of the schedule changes. The battery will not be charged from the panels &lt;em&gt;unless&lt;/em&gt; there’s a charge period defined.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In effect, &lt;code class="language-markup"&gt;Self-Use&lt;/code&gt; mode means the tooling only really allows you to influence how the battery interacts with the grid: When should it charge from it, and when should it discharge to it? This is actually quite logical, given that the mode is supposed to try and ensure that consumption primarily uses locally generated energy, but it can be a little restrictive.&lt;/p&gt;

&lt;h2 id="routine-schedule"&gt;Routine schedule&lt;/h2&gt;

&lt;p&gt;With no ability to &lt;em&gt;dynamically&lt;/em&gt; shift the charging schedule based on price, I needed to choose a time range that would generally see low prices.&lt;/p&gt;

&lt;p&gt;The obvious choice for this, of course, was in the small hours of the night, so I set an appropriate schedule:
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/317ca69052674d0a8d07ab6507b8c6a3/da61b00bcdc30efe4bda9efea71b54ea/unnamed.png" alt="Screenshot of the time-of-use pane. I have set the inverter to charge the battery between 2am and 4am" /&gt;
The next night, the battery charged as it should. Except, of course, that charge was long gone by the time we reached peak (in fact, it was gone before I even got out of bed).&lt;/p&gt;

&lt;p&gt;I messed around with settings for a bit but ultimately found that there isn’t a way to configure things so that the charge is maintained until peak (at least, not if we want the panels to top it up).&lt;/p&gt;

&lt;p&gt;Helpfully, it’s not just the morning that prices are lower: there are a good few hours in the afternoon (where Solar farms are presumably picking up the slack) with lower pricing, too:
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/42249613d5274c7e836211c79ae7850e/29f395cd9e13c10b4e72cfa250145e42/unnamed.png" alt="Screenshot of an Octopus pricing graph, the afternoon is much cheaper than 4pm onwards" /&gt;
So, I updated the configuration so that we charge the battery from 12 pm to just before peak time starts:
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/7b5e2cc0f42e48f6a6805969a135cd03/a20b4094591e084d05a637982c27675a/unnamed.png" alt="Screenshot of battery schedule, it'll charge between 1200 and 1545" /&gt;
This carries an additional benefit: the battery will have had an entire morning of Solar charging, so we’ll only use the grid for whatever’s necessary to top it up (of course, if import prices drop below export value, we’ll need to re-assess that).&lt;/p&gt;

&lt;h2 id="impact-on-charge-levels"&gt;Impact on charge levels&lt;/h2&gt;

&lt;p&gt;The change didn’t have as significant an impact on the average charge level as I’d been expecting:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code class="language-javascript"&gt;from(bucket: "Systemstats/rp_720d")
|&amp;gt; range(start: 2023-08-14T00:00:00Z)
|&amp;gt; filter(fn: (r) =&amp;gt; r["_measurement"] == "solar_inverter")
|&amp;gt; filter(fn: (r) =&amp;gt; r["_field"] == "batteryPowerPerc")
|&amp;gt; keep(columns: ["_time", "_field", "_value"])
|&amp;gt; aggregateWindow(every: 1d, fn: max, createEmpty: false)
|&amp;gt; mean()&lt;/code&gt;&lt;/pre&gt;

&lt;figure&gt;&lt;code&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/83bf0654b35e4454a0c9854dc2589145/57db9d104c325f2b5a9e2036a171b50f/unnamed.png" alt="Screenshot of battery schedule, it'll charge between 1200 and 1545" /&gt;&lt;/code&gt;&lt;/figure&gt;

&lt;p&gt;However, on closer inspection, this average is pulled down by a handful of particularly overcast days on which the only charge that the battery got was from the grid.&lt;/p&gt;

&lt;p&gt;If we instead look at the proportion of days where the battery was charged to at least 65% (i.e., enough to see us through even a busier peak):&lt;/p&gt;

&lt;pre class=""&gt;&lt;code class="language-javascript"&gt;from(bucket: "Systemstats/rp_720d")
|&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)
|&amp;gt; filter(fn: (r) =&amp;gt; r["_measurement"] == "solar_inverter")
|&amp;gt; filter(fn: (r) =&amp;gt; r["_field"] == "batteryPowerPerc")
|&amp;gt; keep(columns: ["_time", "_field", "_value"])
|&amp;gt; aggregateWindow(every: 1d, fn: max, createEmpty: false)
|&amp;gt; map(fn: (r) =&amp;gt; ({r with 
       _field: if r._value &amp;gt; 65.0 then "full" else "partial",
       _value: 1
       }))
|&amp;gt; sum()&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The difference is quite pronounced:
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/708ec3821f374658ba2a09c40d29abb3/bee6283432fe768c39cb906bf5d8b18c/unnamed.png" alt="Screenshot of pie charts showing how many days the battery failed to reach 65% charge before the change was made and after." /&gt;
Prior to the change, nearly &lt;code class="language-markup"&gt;31%&lt;/code&gt; of days didn’t result in the battery reaching a &lt;code class="language-markup"&gt;65%&lt;/code&gt; charge level (yay, British weather…). Since the change was made, that number has dropped to just &lt;code class="language-markup"&gt;6%&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The difference is even more pronounced if we use a 45% charge threshold: partial charges account for &lt;code class="language-markup"&gt;20%&lt;/code&gt; before and &lt;code class="language-markup"&gt;1%&lt;/code&gt; after the change.&lt;/p&gt;

&lt;h2 id="impact-on-savings"&gt;Impact on savings&lt;/h2&gt;

&lt;p&gt;The introduction of the afternoon charge schedule led to a fairly significant change in daily savings rates (the change was made at the red line):
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/71311730611440108dbe664a8c7844e2/8860d14e0e4b386c507abb088284cc44/unnamed.png" alt="Screenshot of daily battery savings. After the change, the daily savings tend to be higher, but there are more negatives" /&gt;
(If you’re wondering about those earlier multi-pound savings, they’re explained &lt;a href="https://www.bentasker.co.uk/posts/blog/house-stuff/calculating-the-break-even-period-of-our-solar-battery.html#skewed"&gt;here&lt;/a&gt; and, unfortunately, are not repeatable).&lt;/p&gt;

&lt;p&gt;The change had two outcomes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Savings, when realized, tended to be higher than before.&lt;/li&gt;
  &lt;li&gt;Negative savings have occurred more frequently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Negative savings occur when the total financial value (i.e., the cost of import from the grid or lost export cost) of the power that we store in the battery is higher than the equivalent import value of the energy when it’s discharged.&lt;/p&gt;

&lt;p&gt;In the time covered, one of the primary determining factors seems to be whether or not we use the oven in the evening: If we cook &lt;a href="https://www.bentasker.co.uk/posts/blog/house-stuff/is-an-air-fryer-more-energy-efficient-than-an-oven.html"&gt;with the air fryer&lt;/a&gt; (or a gas hob), we don’t draw enough energy at peak time. The battery’s charge is then released gradually over the course of the night when prices are lower, leading to negative savings.&lt;/p&gt;

&lt;p&gt;So, just as in my last analysis, there is a perverse incentive to use &lt;em&gt;less&lt;/em&gt; energy-efficient appliances in the evening to help drive the numbers up.&lt;/p&gt;

&lt;p&gt;Of course, no one actually &lt;em&gt;benefits&lt;/em&gt; from improving those figures in that way: using extra energy unnecessarily is still a &lt;em&gt;net loss&lt;/em&gt;. This is an example of the sort of thinking that ultimately leads to trying to &lt;a href="https://www.theguardian.com/us-news/2023/aug/31/bitcoin-mining-plan-pennsylvania-tire-burning"&gt;burn tires in order to improve the efficiency of burning coal to generate Cryptocurrency&lt;/a&gt;.
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/15ebdc358ed0465796c0b1b6c54f6e46/3254858a4e8077ea04bcfa8e74bdf93e/unnamed.jpg" alt="A crypto-bro sits holding a Bitcoin amongst fires made of coal and tyres. Generated using Bings AI image creator, which probably explains why he has a third hand" /&gt;
&lt;em&gt;Utterly&lt;/em&gt; crazy.&lt;/p&gt;

&lt;p&gt;Anyway, despite those days with negative savings, the average daily saving has increased to &lt;code class="language-markup"&gt;£0.152&lt;/code&gt; (from &lt;code class="language-markup"&gt;£0.125&lt;/code&gt;). Although that change is only a few pence, it still represents a &lt;code class="language-markup"&gt;21%&lt;/code&gt; increase in the average daily saving.&lt;/p&gt;

&lt;p&gt;At that rate, the battery will have paid itself off in… yeah, let’s not go there.&lt;/p&gt;

&lt;p&gt;If we can eliminate negative-savings days (don’t you just love a magic wand?), then the average savings becomes &lt;code class="language-markup"&gt;0.29&lt;/code&gt; a day. That roughly halves the break-even time (but still leaves it many times the battery’s rated lifetime).&lt;/p&gt;

&lt;h2 id="getting-free-electricity"&gt;Getting free electricity&lt;/h2&gt;

&lt;p&gt;Our energy tariff is &lt;a href="https://octopus.energy/smart/agile/"&gt;Octopus Agile&lt;/a&gt;, which sometimes sees prices drop very low (or even become negative). Unfortunately, that’s not really happened since I updated the charge schedule.&lt;/p&gt;

&lt;p&gt;However, we’re now enrolled in a new Octopus scheme: &lt;a href="https://octopus.energy/power-ups/"&gt;Power-Ups&lt;/a&gt;.
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/e95048d6ccc7412fa4ce4156699f9400/7a2492ace5150b33b3a6bc757cac8553/unnamed.png" alt="Screenshot of the Octopus page on powerups, showing the areas that it's currently active in" /&gt;
With power-ups, we occasionally get periods where our electricity is free &lt;em&gt;despite&lt;/em&gt; the Agile price not being &lt;code class="language-markup"&gt;0&lt;/code&gt;. The scheme aims to show local network operators that excess renewable energy can be consumed and discourage them from switching off green generation when surpluses are expected.&lt;/p&gt;

&lt;p&gt;With the electricity being free, it obviously makes sense to charge the battery from the grid rather than from solar, helping drive up that day’s savings.&lt;/p&gt;

&lt;p&gt;In order to account for these in my stats, I created a new field—&lt;code class="language-markup"&gt;override_price&lt;/code&gt;—which allows me to override the reported agile unit cost with a simple curl.&lt;/p&gt;

&lt;pre class=""&gt;&lt;code class="language-c"&gt;curl \
-d 'octopus_pricing,tariff_direction=import,charge_type=usage-charge,payment_method=None,reason=misc/solar#6 override_price=0.0 1692365400000000000' \
"http://192.168.3.96:8086/write?db=Systemstats"`&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This sets the cost to &lt;code class="language-markup"&gt;0.0p/kWh&lt;/code&gt; for that 30-minute slot.&lt;/p&gt;

&lt;p&gt;The following query allows us to look at when there have been power-ups and how much time they covered:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code class="language-sql"&gt;SELECT
   count("override_price") * 30 as "minutes"  
FROM "Systemstats"."autogen"."octopus_pricing" 
WHERE $timeFilter 
GROUP BY time(1d) fill(null)&lt;/code&gt;&lt;/pre&gt;

&lt;figure&gt;&lt;code&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/edcf73721ce24d2b89a317dd6b8906fd/f7512d66797c0f37dd9053b082fd6e25/unnamed.png" alt="Screenshot of graph showing when powerups have occurred. There have been 4 since the scheme started on Aug 14" /&gt;&lt;/code&gt;&lt;/figure&gt;

&lt;p&gt;As you might expect, the impact that power-up periods have had on battery savings has been pretty positive (relevant days are marked with red dots):
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/87efc2b577e243aebad29258591e348a/7d1e63a63076a10bab7dba22ad0fa0c0/unnamed.png" alt="Screenshot of graph showing battery saving values with red dots marking the days where power-ups were present. Savings are pretty good on each of the days" /&gt;
A number of factors have played into savings performance so far, but the primary one is striking a balance between solar and grid charging.&lt;/p&gt;

&lt;p&gt;The maximum rate at which the battery can charge is limited, so it’s simply not possible to take the battery from empty to full in 2 hours. This means that it’s important to let the battery charge from solar during the morning because otherwise, there likely won’t be enough charge to cover the peak.&lt;/p&gt;

&lt;p&gt;Conversely, it also doesn’t make sense to put too much solar energy into the battery because then we won’t be able to fully utilize the free grid power.&lt;/p&gt;

&lt;p&gt;To complicate matters, the battery’s maximum charge rate is influenced by things like temperature, so there is an unavoidable element of guesswork in scheduling charges.&lt;/p&gt;

&lt;p&gt;I found that the key lies in not relying solely on the Soliscloud schedules and instead switching things around manually:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Set a schedule to charge from the grid during the power-up (so far, that’s been 1400 - 1600).&lt;/li&gt;
  &lt;li&gt;In the morning, let the battery charge from Solar until it’s at about 47% (on some days, we reached that by 1030).&lt;/li&gt;
  &lt;li&gt;Toggle off the &lt;code class="language-markup"&gt;Time of Use Switch&lt;/code&gt; and then switch the inverter to &lt;code class="language-markup"&gt;Feedback Grid Priority&lt;/code&gt; mode (so that it’ll start exporting rather than charging the battery)&lt;/li&gt;
  &lt;li&gt;Just before the start of the power-up, switch back to &lt;code class="language-markup"&gt;Self-Use&lt;/code&gt; and toggle &lt;code class="language-markup"&gt;Time of Use&lt;/code&gt; back on so that the battery charges from the grid.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach, of course, relies on me being available at the right times to switch things around. But, it also leads to much higher export volumes (and therefore revenue).
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/0c0b4938b4bf4ba1af3b648238ba7e5b/2cb3863379920c27229f319dd9693423/unnamed.png" alt="Screenshot of graph showing system export volume. Power up days are marked with red dots and are generally much higher" /&gt;
In effect, each exported &lt;code class="language-markup"&gt;kWh&lt;/code&gt; becomes worth double what it would otherwise have been.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Rather than storing &lt;code class="language-markup"&gt;1 kWh&lt;/code&gt;, we export it (export revenue makes balance &lt;code class="language-markup"&gt;+15p&lt;/code&gt;).&lt;/li&gt;
  &lt;li&gt;During the power-up period, we replace it using free grid power stored in the battery (balance remains &lt;code class="language-markup"&gt;+15p&lt;/code&gt;).&lt;/li&gt;
  &lt;li&gt;We later discharge and consume it at the prevailing import price (assuming &lt;code class="language-markup"&gt;30p/kWh&lt;/code&gt;, the balance becomes &lt;code class="language-markup"&gt;+45p&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The power-up allows us to claim the export value, avoiding loss of opportunity cost (which, as &lt;a href="https://www.bentasker.co.uk/posts/blog/house-stuff/calculating-the-break-even-period-of-our-solar-battery.html#electricity"&gt;I wrote last time&lt;/a&gt;, is a major contributor to the low savings rate), and still realize a full kWh’s worth of savings during peak times.&lt;/p&gt;

&lt;p&gt;Although that level of gain couldn’t exist without the battery, it isn’t fully accounted for in the battery savings figures—to see the true benefit of the approach, we need to look at the total system savings figure instead:
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/360121d4e41644d8963f6fd9c93d3122/4997c75418f3d0b2165a6e4439342972/unnamed.png" alt="Screenshot of graph showing system savings. Power up days are marked with red dots and are generally much higher" /&gt;
As a rule, the days with power-ups outperformed their adjacent days. The exception is the 18th of August: not only was the weather crap, but it was also my first spin at configuring the inverter to take advantage of a power-up, and I ballsed it up a bit.&lt;/p&gt;

&lt;p&gt;It’s worth noting, too, that the power-ups also delivered &lt;em&gt;non-solar&lt;/em&gt; savings that aren’t accounted for in these graphs: we shifted some of our load to the power-up window, and so we were able to run the washing machine, etc., for free.&lt;/p&gt;

&lt;p&gt;The result is that our consumption costs were reduced by at least sixty pence a day:
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/a07139acc2874ef78a4e8ebc1ea6f8b2/9e15be2ddff65b658ae54773d6428288/unnamed.png" alt="Screenshot of graph showing savings derived through Octopus power ups" /&gt;
The grid, for its part, gets an additional supply of green energy (our exports) at times of lower supply, without needing to spin down green generation capacity.&lt;/p&gt;

&lt;h2 id="further-improvements"&gt;Further improvements&lt;/h2&gt;

&lt;p&gt;A couple of days ago, I made an additional change to the schedule.&lt;/p&gt;

&lt;p&gt;Although much lower than the evening peak, there is a pricing peak each morning (coinciding with people getting ready for work, etc.).
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/c8e23b2fe4e24b7098a76b70066adde8/fc950d0412244edd1c4862ca0f5098c5/unnamed.png" alt="Screenshot of graph showing octopus pricing, there's a small peak each morning before prices drop again for the afternoon" /&gt;
At that time in the morning, our panels are active but far from operating at their best, so it seems sensible to take a small overnight charge to help shave the morning peak.
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/8a3cc9f505994e258ec5e96f9dff7e5b/6e68f08ce662c5b3f8d3966d6e03e104/unnamed.png" alt="Screenshot of battery schedule, it is now configured to charge 1200-1545 and 0315-0545" /&gt;
The savings, if any, will likely be a few pence at most, but that’s still a reasonable percentage improvement in the battery’s daily value. It will also likely be higher in winter when our panels start generating later in the day.&lt;/p&gt;

&lt;h2 id="the-outcome"&gt;The outcome&lt;/h2&gt;

&lt;p&gt;Battery break-even is still &lt;a href="https://www.bentasker.co.uk/posts/blog/house-stuff/calculating-the-break-even-period-of-our-solar-battery.html#adjusted_break_even_stats"&gt;decades away&lt;/a&gt;. But given that there is no way to unpurchase the battery, any additional savings that the battery can generate are still welcome, not least because it helps lower our energy bills. If I were being smart, I could &lt;em&gt;probably&lt;/em&gt; find a way to invest those savings so that their growth could contribute further to achieving break-even.&lt;/p&gt;

&lt;p&gt;Because our day-to-day activities vary, charging the battery (whether from the grid or solar) carries an element of risk: the battery’s ability to unlock savings is quite reliant on whether or not we demand sufficient energy at peak times.&lt;/p&gt;

&lt;p&gt;That could potentially be mitigated by limiting battery discharge to certain periods, but it’s not something that’s currently possible with this inverter (or at least, not without significant trade-offs).&lt;/p&gt;

&lt;p&gt;Somewhat unsurprisingly, the best way to unlock additional battery savings is to use the battery more. Ensuring the battery has &lt;em&gt;at least&lt;/em&gt; enough charge to survive peak usage has helped increase savings by about &lt;code class="language-markup"&gt;20%&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The introduction of Octopus Power-Ups has also proven beneficial, allowing the system to derive a reasonable amount of additional value. The benefit will, very likely, be less during winter as we won’t be able to export anywhere near the same amount of energy. Conversely though, our batteries will likely be in more need of a grid charge.&lt;/p&gt;

&lt;p&gt;My work on this, clearly, isn’t done.&lt;/p&gt;

&lt;p&gt;First, I need to see what benefit (if any) we gain from shaving that morning peak.&lt;/p&gt;

&lt;p&gt;Once I’ve got meaningful stats on that, the next step is likely going to be adding a &lt;a href="https://github.com/Jumpy07/Solis---SolisCloud-and-Home-Assistant"&gt;rs485 switch&lt;/a&gt; into the mix so that I can potentially start automating.&lt;/p&gt;

&lt;p&gt;With automation in place, I’ll be more able to:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Avoid running up negative savings: On days when we routinely don’t use the oven, the automation can disable (or perhaps just reduce) the scheduled grid charge.&lt;/li&gt;
  &lt;li&gt;Ensure a sensible charge level on cloudy days: Weather forecasting can be used to define whether we’re likely to need a prolonged grid charge to cope with the peak.&lt;/li&gt;
  &lt;li&gt;Automate switching between Self-Use and Grid-Priority, allowing finer control over when the battery charges and discharges.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I don’t believe we’ll ever reach the point where buying the battery &lt;em&gt;won’t&lt;/em&gt; have been a financial mistake, but given that we’ve got it, I think there’s still some scope for additional savings—particularly as the rest of the system is over-performing enough to offset the cost of the battery relatively quickly.&lt;/p&gt;
</description>
      <pubDate>Tue, 18 Feb 2025 07:00:00 +0000</pubDate>
      <link>https://www.influxdata.com/blog/improving-solar-energy-savings/</link>
      <guid isPermaLink="true">https://www.influxdata.com/blog/improving-solar-energy-savings/</guid>
      <category>Developer</category>
      <author>Ben Tasker (InfluxData)</author>
    </item>
    <item>
      <title>Is an Air Fryer More Energy Efficient Than an Oven</title>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally posted on &lt;a href="https://www.bentasker.co.uk/posts/blog/house-stuff/is-an-air-fryer-more-energy-efficient-than-an-oven.html#is_it_worth_investing_for_this_winter"&gt;www.bentasker.co.uk&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This project was built using a previous version of InfluxDB. &lt;a href="https://www.influxdata.com/products/influxdb-overview/"&gt;InfluxDB 3&lt;/a&gt; moved away from Flux and a built-in task engine. Users can use external tools, like Python-based &lt;a href="https://www.quix.io/kapacitor-alternative"&gt;Quix&lt;/a&gt;, to create tasks in InfluxDB 3.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At first glance, the question I’m asking in this post almost seems redundant. An air fryer heats a much smaller area and uses a smaller heating element, so of course, it &lt;em&gt;should&lt;/em&gt; be more energy efficient.
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/7e9c84e2050f499da266af90ebf443b6/98df2824a6bd1cae67f85b9cc95ea8dd/unnamed.png" alt="" /&gt;
However, that’s not guaranteed to be the case. Although an oven uses a larger heating element, because it’s better insulated, it’s possible that it might do a better job of keeping heat in and so have to consume less energy replacing lost heat. If, due to these losses, the air fryer’s element is on for a longer duration of the cook, it’s plausible that an air fryer might end up consuming more energy than the oven.&lt;/p&gt;

&lt;p&gt;If (as seems likely) the air fryer is more energy efficient, the question becomes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;How much more efficient?&lt;/li&gt;
  &lt;li&gt;When does it amortize (i.e., at what point do the energy savings outweigh the initial purchase cost)?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That second question will also help answer whether it’s worth investing in an air fryer to try to counter high energy prices. To see how they compare, I cook myself some chips and compare the resulting energy usage using:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A Rangemaster Professional oven (3.2 kWh)&lt;/li&gt;
  &lt;li&gt;An air fryer (&lt;a href="https://www.amazon.co.uk/dp/B08NF6XPRT"&gt;1700w 5.5L air fryer&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id="the-cook"&gt;The cook&lt;/h2&gt;

&lt;p&gt;Both the oven and the air fryer cooked the same thing: Co-op’s brand of frozen oven chips. The appliances are situated in the same kitchen, but at opposite ends (so the air fryer does not benefit from heat radiating from the oven warming the air it draws in).&lt;/p&gt;

&lt;p&gt;Although they’re cooking the same food, there are some differences:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The oven requires 25 minutes, and the air fryer takes 15&lt;/li&gt;
  &lt;li&gt;The oven needs to be set to 180°c (per the packaging), while the air fryer manual says to use 200°c&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unlike the oven, the air fryer doesn’t need to warm-up first, cutting 15-20 minutes off the run time (depending on how long it takes you to come back and put food into the oven).&lt;/p&gt;

&lt;p&gt;The oven started first. When the air fryer finished, the oven was turned off and the food was removed from both.&lt;/p&gt;

&lt;p&gt;As with my previous forays into &lt;a href="https://www.bentasker.co.uk/categories/energy-consumption.html"&gt;measuring energy consumption&lt;/a&gt;, I collected and wrote energy usage readings using &lt;a href="https://www.bentasker.co.uk/categories/influxdb.html"&gt;InfluxDB&lt;/a&gt; so I could query statistics back out with &lt;a href="https://www.bentasker.co.uk/categories/flux.html"&gt;Flux&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;InfluxDB is ideal for monitoring energy consumption because it’s purpose-built to handle time series data. Time series data is the type of data I’m collecting from both the oven and air fryer. InfluxDB ingests the data in real-time so I can see my visualizations immediately.&lt;/p&gt;

&lt;h2 id="retrieving-results"&gt;Retrieving results&lt;/h2&gt;

&lt;p&gt;Because of how it’s wired in (into its own circuit, with no convenient access to the wiring for a clamp meter), calculating the oven’s usage requires a slightly more complex query than is normally required for individually monitored appliances.&lt;/p&gt;

&lt;p&gt;The way usage is calculated is:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Retrieve usage for all individually monitored devices.&lt;/li&gt;
  &lt;li&gt;Retrieve usage for power meter.&lt;/li&gt;
  &lt;li&gt;Subtract the sum of device usage from the power meter (find details on how I collect this &lt;a href="https://www.bentasker.co.uk/posts/blog/house-stuff/739-monitoring-our-electricity-usage-with-influxdb.html"&gt;here&lt;/a&gt;).&lt;/li&gt;
  &lt;li&gt;Subtract a known base load figure (to account for items not monitored) to arrive at the Oven’s usage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is a small margin for error involved in the subtraction of the base load, but it is quite small: I’ve been doing &lt;em&gt;a lot&lt;/em&gt; of work recently to understand our base load (to find where it can be reduced).&lt;/p&gt;

&lt;p&gt;The air fryer was plugged into a Tapo P110 Smart Socket, so its usage is easy to &lt;a href="https://www.bentasker.co.uk/posts/blog/house-stuff/capturing-energy-usage-info-with-tapo-kasa-and-influxdb.html"&gt;collect and query&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I wrote a Flux query to retrieve and combine usage data for each of the appliances into a single output stream:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code class=""&gt;// Start/Stop times
start = 2022-09-04T16:40:00Z 
stop = 2022-09-04T17:30:00Z 

// Group into 30s  time windows 
// helps ensure meter and appliance readings appear in the same window
period = 30s

// get appliance usage
known = from(bucket: "Systemstats")
  |&amp;gt; range(start: start, stop: stop)
  |&amp;gt; filter(fn: (r) =&amp;gt; r._measurement == "power_watts" and 
                       r._field == "consumption")
  |&amp;gt; filter(fn: (r) =&amp;gt; r.host != "power-meter")
  |&amp;gt; aggregateWindow(every: period, fn: mean)
  // Combine all appliances into a single table
  |&amp;gt; group()
  // Calculate the combined consumption per window period
  |&amp;gt; aggregateWindow(every: period, fn: sum, createEmpty: true)

// Get total usage measured at the meter
pm = from(bucket: "Systemstats")
  |&amp;gt; range(start: start, stop: stop)
  |&amp;gt; filter(fn: (r) =&amp;gt; r._measurement == "power_watts" and 
                       r._field == "consumption")
  |&amp;gt; filter(fn: (r) =&amp;gt; r.host == "power-meter")
  |&amp;gt; aggregateWindow(every: period, fn: mean)

// Join the two
oven = join(tables: {t1: pm, t2: known}, on: ["_time"])
  // subtract appliances + base load to get oven usage
  |&amp;gt; map(fn: (r) =&amp;gt; ({
      _time: r._time,
      _field: "Oven",
      _value: r._value_t1 - r._value_t2 - 400.0
  })) 

// Get the air fryer's usage over the same period
airfry = from(bucket: "Systemstats")
  |&amp;gt; range(start: start, stop: stop)
  |&amp;gt; filter(fn: (r) =&amp;gt; r._measurement == "power_watts" and r._field == "consumption")
  |&amp;gt; filter(fn: (r) =&amp;gt; r.host == "air fryer")
  |&amp;gt; aggregateWindow(every: period, fn: mean)

// Union the tables so we can display a graph with 2 series
union(tables: [oven, airfry])&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This gives us the following graph.
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/1a29b8d38f864e5e8b695ab9eadf2a30/b85bdad2b707c2f4f391d4fe88df925a/unnamed.png" alt="" /&gt;
It’s already fairly apparent which appliance is going to be responsible for most usage.&lt;/p&gt;

&lt;p&gt;All subsequent statistics in this post are retrieved by appending to the &lt;code class="language-markup"&gt;union()&lt;/code&gt; statement in the query above.&lt;/p&gt;

&lt;h2 id="mean-usage"&gt;Mean usage&lt;/h2&gt;

&lt;p&gt;If we look at the mean draw across the cook, we see that the oven’s average usage is significantly higher:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code class=""&gt;start = 2022-09-04T16:40:00Z 
stop = 2022-09-04T17:30:00Z 

...

// Union the tables so we can display a graph with 2 series
union(tables: [oven, airfry])
|&amp;gt; mean()
|&amp;gt; group()&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/bb63d8c0870c4fcead5400ad6ead4ea8/7c6bbedae8adc653212195a54d51718e/unnamed.png" alt="" /&gt;&lt;/p&gt;

&lt;h2 id="total-usage"&gt;Total usage&lt;/h2&gt;

&lt;p&gt;We know the average draw, but let’s look at the total power consumption for each appliance.&lt;/p&gt;

&lt;p&gt;Calculating consumption from draw is fairly easy: If we draw &lt;code class="language-markup"&gt;n&lt;/code&gt; watts for an hour, then the usage is &lt;code class="language-markup"&gt;n Wh&lt;/code&gt; (with &lt;code class="language-markup"&gt;1000 Wh&lt;/code&gt; being &lt;code class="language-markup"&gt;1 kWh&lt;/code&gt;). If we draw n watts for 1/&lt;code class="language-markup"&gt;x&lt;/code&gt;th of an hour, then we need to divide &lt;code class="language-markup"&gt;n&lt;/code&gt; by &lt;code class="language-markup"&gt;x&lt;/code&gt; to have the calculated usage represent that fraction of time. For example:&lt;/p&gt;

&lt;p&gt;&lt;code class="language-markup"&gt;A 30-second 3kW draw consumes (3000 / (60 * 2)): 25-watt hours.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Our data is windowed into 30-second periods (&lt;code class="language-markup"&gt;1/120&lt;/code&gt;th of an hour), so each window consumes &lt;code class="language-markup"&gt;1/120&lt;/code&gt;th of the calculated usage.&lt;/p&gt;

&lt;p&gt;So, to implement this, we add a simple conversion to the end of the query, taking the draw for that period and dividing it by 120:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code class=""&gt;// Union the tables so we can display a graph with 2 series
union(tables: [oven, airfry])
  |&amp;gt; map(fn: (r) =&amp;gt; ({ r with
      // data is grouped into 30s windows, so if we're pulling 2kW 
      // in that window we're consuming
      //
      // usage / (mins-in-hour \* 2)
      // 2000 / 120
      //
      _value: r._value / 120.0
  }))
  // Calculate totals
  |&amp;gt; sum()
  |&amp;gt; group()&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Giving the following results:
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/f263cbadef784e74ba0481e56b66c93d/a07fc222467222a7ae2332f9666d2a67/unnamed.png" alt="" /&gt;
The oven consumed more than double the energy demanded by the air fryer. Unsurprisingly, the proportions here are very similar to the proportion between the two appliance’s mean draw (the two figures being quite well correlated).&lt;/p&gt;

&lt;p&gt;However, the oven did need to warm-up first. What happens if we only look at the time spent cooking (i.e., when food was in there)?&lt;/p&gt;

&lt;p&gt;To do that, we adjust the start time at the head of the query so that the query starts after the oven has completed its warm-up:&lt;/p&gt;

&lt;p&gt;&lt;code class="language-markup"&gt;start = 2022-09-04T16:59:00Z&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The result is a large reduction in the reported consumption:
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/45939878fafd4afaa28a346901aed4cc/91c3bed0d18d5028f3efed992e929a38/unnamed.png" alt="" /&gt;
The oven has a significant edge, managing to outperform the air fryer (which, even if it is able to do so while cooking, had to warm-up during that time).&lt;/p&gt;

&lt;h2 id="warmups"&gt;Warmups&lt;/h2&gt;

&lt;p&gt;Of course, you may be thinking that this statistic isn’t particularly useful: you can’t reliably cook food without warming the oven.&lt;/p&gt;

&lt;p&gt;However, the oven’s post-warm-up consumption is lower than that of the air fryer, which suggests that the oven might be able to outperform the air fryer when cooking over longer periods.&lt;/p&gt;

&lt;p&gt;To understand the usage, we start by looking a little closer at the warmups:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;How long does warm-up take?&lt;/li&gt;
  &lt;li&gt;What power is consumed during that time?&lt;/li&gt;
  &lt;li&gt;What’s the consumption with that subtracted?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because both devices have thermostatic control, we can easily tell when they reached temperature: there’s a corresponding drop in power consumption when the thermostat turns the heating element off.&lt;/p&gt;

&lt;p&gt;You can see the warm-up periods highlighted in the graph below (red: oven, purple: air fryer).
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/18a073025ede4f668f39887bf333177d/66f0438c7340b62d5441fd8820374c44/unnamed.png" alt="" /&gt;
The warm-up timings were:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Oven: (17:45 - 17:53) 8 minutes&lt;/li&gt;
  &lt;li&gt;Air fryer: (18:08 - 18:15) 7 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, in practice, the oven didn’t cook anything for longer than 8 minutes because I wandered off to do other things and took a little too long to come back. Although that’s representative of real usage (or, certainly, representative of my usage), we won’t penalize the oven for my mistake when we do our calculations.&lt;/p&gt;

&lt;p&gt;By changing the &lt;code class="language-markup"&gt;start&lt;/code&gt; and &lt;code class="language-markup"&gt;stop&lt;/code&gt; values on the query, we get consumption for each of the warm-up windows before taking the usage from the warm-up periods and subtracting them from each of the total usage figures:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code&gt;| Device    | Warm Up Usage | Remaining Usage |
|---------------------------------------------|
| Oven      | 137 Wh        | 206 Wh          |
| Air fryer | 96.4 Wh       | 96.6 Wh         |
-----------------------------------------------&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Although interestingly, they don’t tell us much on their own. We’ll use these figures later.&lt;/p&gt;

&lt;h2 id="heating-element-activity"&gt;Heating element activity&lt;/h2&gt;

&lt;p&gt;One thing that stands out in the graph is how long the air fryer’s element stays active after the initial warm-up.&lt;/p&gt;

&lt;p&gt;It’s a horrible visualization, but look at how much longer the air fryer element is active (the purple boxes) per instance than the oven (red boxes):
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/602d4449b8474e69b592f6650a852440/43ff510b8935716f80eb2a2fc248fb7d/unnamed.png" alt="" /&gt;
The oven mainly has 30-second bursts (though there are a couple of 1-minute periods mixed in). Conversely, although the air fryer has a couple of 30s bursts, and there’s a 3- minute period where the element is constantly active.&lt;/p&gt;

&lt;p&gt;This would seem to support the idea that the Oven is able to maintain temperature much more easily than the air fryer.&lt;/p&gt;

&lt;p&gt;We can check how long the element in each device spent turned on:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code&gt;**union**(tables: [oven, airfry])
  // Set a state to group by later
**|&amp;gt; map**(fn: (**r**) =&amp;gt; ({r with
    state: **if** **r**._value &amp;gt; 1100 then
        "ON"
    else
      "OFF"
  }))

  // Calculate the total stateduration
**|&amp;gt; elapsed**(unit: 1s)
**|&amp;gt; filter**(fn: (**r**) =&amp;gt; **r**.state == "ON")
**|&amp;gt; sum**(column: "elapsed")
**|&amp;gt; group**()&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;By changing the start and end times, we can see how long the element was active during each cooking stage.&lt;/p&gt;

&lt;pre class=""&gt;&lt;code&gt;| Device    | Total   | After Oven Warmup | After Air Fryer Warmup |
|------------------------------------------------------------------|
| Oven      | 22 mins | 14 Mins           | 1 Minute               |
| air fryer | 12 mins | 12 Mins           | 6 Minutes              |
--------------------------------------------------------------------&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;50% of the air fryer’s activity occurred &lt;em&gt;after&lt;/em&gt; warmup, while 63% of the oven’s heating time occurred post-warmup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But&lt;/strong&gt; the oven was also on for longer (making its post-warmup time disproportionately larger than the air fryers), so we need to make the comparison proportional by taking the total cook time into account:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code&gt;| Device    | Cook Time (exc warmup) | Element Time (exc warmup) | Element Time |
|-------------------------------------------------------------------------------|
| Oven      | 32 Mins                | 14 Mins                   | 43.75%       |
| air fryer | 10 Mins                | 6 Mins                    | 60.00%       |
---------------------------------------------------------------------------------&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The air fryer spent &lt;em&gt;significantly&lt;/em&gt; more of its non-warmup time with the element sucking power. So, it’s certainly possible that the oven could prove to be more energy-efficient after a period.&lt;/p&gt;

&lt;h2 id="can-the-oven-be-more-efficient"&gt;Can the oven be more efficient?&lt;/h2&gt;

&lt;p&gt;We can work out roughly where the break-even falls. If we reduce the post-warmup average consumption to a per-minute average, we can compare them to predict how many minutes of cooking time it would take for the oven to outperform the air fryer.&lt;/p&gt;

&lt;p&gt;We collected the figures we need &lt;a href="https://www.bentasker.co.uk/posts/blog/house-stuff/is-an-air-fryer-more-energy-efficient-than-an-oven.html#post_warmup_usage_figured"&gt;above&lt;/a&gt;:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code&gt;| Device    | Cook Time (exc warmup) | Power Consumed (exc warmup) | Power consumed (Warmup) |
|--------------------------------------------------------------------------------------------|
| Oven      | 32 Mins                | 206 Wh                      | 137 Wh                  |
| air fryer | 10 Mins                | 96.6 Wh                     | 96.6 Wh                 |
----------------------------------------------------------------------------------------------&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;To calculate the average Wh consumed per minute, we do:&lt;/p&gt;

&lt;p&gt;&lt;code class="language-markup"&gt;Consumed / time = average minutely consumption&lt;/code&gt;
Giving the following figures:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Oven: &lt;code class="language-markup"&gt;206 Wh&lt;/code&gt; / &lt;code class="language-markup"&gt;32 mins&lt;/code&gt; = &lt;code class="language-markup"&gt;6.4375 Wh/min&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Air fryer: &lt;code class="language-markup"&gt;96.6 Wh&lt;/code&gt; / &lt;code class="language-markup"&gt;10 mins&lt;/code&gt; = &lt;code class="language-markup"&gt;9.66 Wh/min&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The oven is &lt;em&gt;significantly&lt;/em&gt; more efficient per minute.&lt;/p&gt;

&lt;p&gt;But before that efficiency advantage can translate into an overall energy saving, the oven must cancel out the additional energy it consumed during its warmup period.&lt;/p&gt;

&lt;p&gt;To calculate when it’ll reach that point, we calculate the difference in warmup consumption and divide it by the difference in per-minute consumption.&lt;/p&gt;

&lt;pre class=""&gt;&lt;code&gt;    137 - 96.6 = 40.4 Wh
-------------------------------   = 12.54 minutes
   9.66 - 6.4375 = 3.2225 Wh&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So, all things being equal, the oven would break even after ~13 minutes.&lt;/p&gt;

&lt;p&gt;But not all is equal: The air fryer also requires less cooking time (10 minutes less for chips), so the oven consumes an extra &lt;em&gt;10 minutes&lt;/em&gt; of energy.&lt;/p&gt;

&lt;p&gt;Adding this into our calculation moves the break-even point quite considerably:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code&gt;    (137 - 96.6)
  + (10 * 6.4375) = 104.757 Wh
--------------------------------   = 32.51 minutes
   9.66 - 6.4375 = 3.2225 Wh&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At nearly 33 minutes, this is more than double the time the air fryer requires to cook the chips.&lt;/p&gt;

&lt;h2 id="spuds"&gt;Spuds&lt;/h2&gt;

&lt;p&gt;Fifteen minutes is quite a short cooking time, though—do these results hold for something that requires longer?&lt;/p&gt;

&lt;p&gt;If we take the timings from two baked potato recipes (&lt;a href="https://www.bbc.co.uk/food/recipes/theperfectbakedpotat_67837"&gt;oven&lt;/a&gt; and &lt;a href="https://www.bbcgoodfood.com/recipes/air-fryer-baked-potatoes"&gt;air fryer&lt;/a&gt;), we should be able to work out which is more energy-efficient:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Oven potatoes require 75-90 mins (we’ll split the difference and call it 83).&lt;/li&gt;
  &lt;li&gt;Fryer potatoes require 50 mins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The oven potatoes need 33 mins more, so assuming that warmup and per-min consumption doesn’t change:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code&gt;    (137 - 96.6) 
  + (33 * 6.4375) = 252.84 Wh
--------------------------------   = 78 minutes
   9.66 - 6.4375 = 3.2225 Wh&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The break-even is at 78 minutes, which (again) is longer than the air fryer needs.&lt;/p&gt;

&lt;p&gt;Technically, the break-even will be even further out than that because the oven recipe requires a higher temperature than our chips, so the warm-up and maintenance consumption would be higher.&lt;/p&gt;

&lt;p&gt;To summarize, an oven uses less energy per minute post-warmup than an air fryer, but because it needs substantially less time to cook food, it still uses less energy overall.&lt;/p&gt;

&lt;h2 id="purchase-break-even"&gt;Purchase break-even&lt;/h2&gt;

&lt;p&gt;Given the energy crisis looming in the UK, it’d be remiss not to also examine the financial side of this.&lt;/p&gt;

&lt;p&gt;If you’re reading this, it’s possible you haven’t yet bought an air fryer and are considering doing so to reduce your energy bills. Since you &lt;em&gt;probably&lt;/em&gt; already have an oven, the upfront cost comparison is £0 vs. &lt;code class="language-markup"&gt;$airfryer_cost&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To determine whether it’s worth buying an air fryer, we need to consider how long it will take for the energy savings to break even with the purchase cost (known as amortization).&lt;/p&gt;

&lt;p&gt;If we consider the following:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The air fryer I tested costs &lt;code class="language-markup"&gt;£72.99&lt;/code&gt; (although you can get an Air Fryer for around £40).&lt;/li&gt;
  &lt;li&gt;Including warm-ups (let’s be realistic, you’re never going to consistently leap on the oven as soon as it’s warmed up), the air fryer consumed &lt;code class="language-markup"&gt;150 Wh&lt;/code&gt; less than the oven when cooking chips.&lt;/li&gt;
  &lt;li&gt;In October, the cost of electricity will (for many) rise to &lt;code class="language-markup"&gt;£0.52/kWh&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We can conclude that:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The air fryer saved (&lt;code class="language-markup"&gt;52/1000 * 150&lt;/code&gt;) &lt;code class="language-markup"&gt;£0.078&lt;/code&gt; in energy when cooking my chips&lt;/li&gt;
  &lt;li&gt;At £0.078 per cook, break-even would happen after (&lt;code class="language-markup"&gt;72.99 / 0.078&lt;/code&gt;) 936 similar cooks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At one cook a day, the energy savings would take &lt;em&gt;two and a half years&lt;/em&gt; to offset the £72.99 purchase price. Assuming a £40.00 air fryer can deliver the same energy savings, it would still take nearly 18 months to break even.&lt;/p&gt;

&lt;p&gt;To put that into perspective:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;If I cook chips once daily for 31 days, I would reduce my energy bills by just &lt;code class="language-markup"&gt;4.65 kWh&lt;/code&gt; / &lt;code class="language-markup"&gt;£2.41&lt;/code&gt; a month.&lt;/li&gt;
  &lt;li&gt;If I spread the cost of the air fryer, interest-free, over 18 months, I would pay &lt;code class="language-markup"&gt;£4.05&lt;/code&gt;/month&lt;/li&gt;
  &lt;li&gt;Despite the energy saving, I’d therefore lose &lt;code class="language-markup"&gt;£1.64&lt;/code&gt; a month&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s (hopefully) unlikely that you’re going to be living off chips alone, so let’s also project usage for baked potatoes:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code&gt;     Oven: 137 + (83 * 6.4375) = 671.31 Wh
  -  air fryer: 96.6 + (50 * 9.66) = 579.6 Wh
    -------------------------------------------
                  91.71 Wh&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The saving on baked potatoes is actually &lt;em&gt;lower&lt;/em&gt;, and saves (&lt;code class="language-markup"&gt;52/1000 * 91.71&lt;/code&gt;) &lt;code class="language-markup"&gt;£0.0477&lt;/code&gt; per cook (giving a 4 year amortization period).&lt;/p&gt;

&lt;p&gt;This means that, even if you manage to find an air fryer at half the price I paid, the break-even is still &lt;em&gt;years&lt;/em&gt; away.&lt;/p&gt;

&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;Despite needing to achieve a higher temperature, the air fryer used significantly less power cooking my chips.&lt;/p&gt;

&lt;p&gt;The fact that it doesn’t need to be warmed up first contributes quite significantly to this: not just because it’s not spending time heating rather than cooking, but also because it removes the opportunity for a human to get side-tracked and forget to put the food in (guilty, your honor).&lt;/p&gt;

&lt;p&gt;The oven consumes significantly less energy maintaining temperature than the air fryer (likely because of better insulation). However, the air fryer’s faster cooking time mitigates this, lowering overall energy consumption.&lt;/p&gt;

&lt;p&gt;So, to answer our questions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is an air fryer more energy efficient than an oven?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, but &lt;em&gt;only&lt;/em&gt; if you use it properly and adjust cooking times.&lt;/p&gt;

&lt;p&gt;If you air-fry food for the same  time as you would in an oven, it may take as little as 13 minutes for the oven to become the less expensive option (and your food will probably be horribly burnt).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When will I break even?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even at the insane prices we’re all going to be paying, the savings per meal are &lt;em&gt;very&lt;/em&gt; small. It’ll likely take years for the energy savings to break even with the up-front cost of purchasing an air fryer. In some cases, it may take longer than the useful life of the air fryer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I invest in an air fryer to avoid high energy prices?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The long amortization period means that if you’re concerned about how you will afford energy bills this winter, purchasing an air fryer isn’t a good way to address it. The air fryer will reduce your energy consumption, but only &lt;em&gt;very slightly&lt;/em&gt;, and the up-front purchase cost will make you worse off overall.&lt;/p&gt;

&lt;p&gt;Unless you can find a deeply discounted air fryer (or be gifted one, or pay off the loan interest-free over an extremely extended period), it’s unlikely to be the right decision from a purely financial point of view.&lt;/p&gt;

&lt;p&gt;Obviously, if you &lt;em&gt;already&lt;/em&gt; have an air fryer, then you incurred the capital cost, so using it where you can will slightly reduce your energy bill, with each saving bringing the break-even point slightly closer. And they do make some &lt;em&gt;very&lt;/em&gt; nice chips.&lt;/p&gt;

&lt;p&gt;To start monitoring the energy of your appliances, sign up for a &lt;a href="https://cloud2.influxdata.com/signup"&gt;free cloud account&lt;/a&gt; now. To add InfluxDB to larger projects, &lt;a href="https://www.influxdata.com/contact-sales/"&gt;contact our sales team&lt;/a&gt; for a custom POC.&lt;/p&gt;

&lt;h2 id="update-cooking-at-180"&gt;Update: cooking at 180&lt;/h2&gt;

&lt;p&gt;After I published this post, a Twitter user raised an &lt;a href="https://x.com/joetrev/status/1572662942465671171"&gt;interesting point&lt;/a&gt;.
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/fc367d043f6545588eb40bf47f378713/5bafd39cf891f242ad488ceb49da72af/unnamed.png" alt="" /&gt;
The oven’s manual doesn’t provide any guidance, but I could have tried cooking in both at 180c, demanding lower consumption from the air fryer and increasing its overall advantage. I expected that it wouldn’t make much material difference to the conclusion of this post, but I thought I’d give it a try nonetheless.&lt;/p&gt;

&lt;p&gt;This evening, I cooked some chips using the following:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Temperature: 180c&lt;/li&gt;
  &lt;li&gt;Time: 15 minutes (as before)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The chips came out cooked, but, honestly, they weren’t that great—they hadn’t crisped very much and had that unpleasant wet oven-cook feel to them. They either needed longer or the manual’s recommended temperature. But what we’re interested in is the power consumption.&lt;/p&gt;

&lt;p&gt;I graphed consumption using the same query as before:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-markup"&gt;period = 30s
from(bucket: "Systemstats")
  |&amp;gt; range(start: 2022-09-24T16:45:00Z, stop: 2022-09-24T17:05:00Z)
  |&amp;gt; filter(fn: (r) =&amp;gt; r._measurement == "power_watts" and r._field == "consumption")
  |&amp;gt; filter(fn: (r) =&amp;gt; r.host == "air-fryer")
  |&amp;gt; aggregateWindow(every: period, fn: mean)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Giving the following graph:
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/504e5783bb2d4b8da2ae5acdc032ca33/9203dc81aceba56ea466cbdc29e02d90/unnamed.png" alt="" /&gt;
Using the same queries as in the earlier part of this post, we can see that the total consumption for this cook was &lt;code class="language-markup"&gt;174 Wh&lt;/code&gt;, just &lt;code class="language-markup"&gt;19 Wh&lt;/code&gt; less than the cook at 200 Celsius. So it saved &lt;code class="language-markup"&gt;169 Wh&lt;/code&gt; when compared to the oven cook.&lt;/p&gt;

&lt;p&gt;If we re-run the break-even calculations using this figure, we can see that&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;At 180c it saves (&lt;code class="language-markup"&gt;52/1000 * 169&lt;/code&gt;) &lt;code class="language-markup"&gt;£0.088&lt;/code&gt; in energy.&lt;/li&gt;
  &lt;li&gt;At £0.088 per cook, break even will take (&lt;code class="language-markup"&gt;72.99 / 0.088&lt;/code&gt;) 829 similar cooks.&lt;/li&gt;
  &lt;li&gt;At one cook a day, break-even is around two years, 3 months, and 8 days.&lt;/li&gt;
  &lt;li&gt;A £40 fryer achieving the same savings would take 1 year, 2 months, and 27 days to break even.&lt;/li&gt;
  &lt;li&gt;31 days of chips once a day would reduce energy bills by just &lt;code class="language-markup"&gt;5.24 kWh&lt;/code&gt; / &lt;code class="language-markup"&gt;£2.72&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Assuming interest-free repayment of the purchase at &lt;code class="language-markup"&gt;£4.05&lt;/code&gt;/month, we’d be &lt;code class="language-markup"&gt;£1.33&lt;/code&gt; worse off a month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There’s also the issue of the quality of the chips we get. Realistically, you’re only likely to eat them like that once, and then on subsequent cooks, you’ll either increase the temperature (back to 200) or the time.&lt;/p&gt;

&lt;p&gt;So, let’s work out the impact of adding time. W. We’re interested in the average power usage while the air fryer maintains temperature (the bulk of energy usage is in the initial warm-up).&lt;/p&gt;

&lt;p&gt;The graph shows that the warm-up finished at 17:53:30, the cook finished at 18:02:30, and the maintenance period was 9 minutes.&lt;/p&gt;

&lt;p&gt;During that time it consumed 76.6 Wh, so the average consumption per minute was (&lt;code class="language-markup"&gt;76.6/9&lt;/code&gt;) &lt;code class="language-markup"&gt;8.5Wh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Assuming the chips required an additional 5 minutes (in practice, that’s probably still not enough), we’d likely consume another &lt;code class="language-markup"&gt;42.5 Wh&lt;/code&gt;. The advantage gained by dropping to 180c was only &lt;code class="language-markup"&gt;19 Wh&lt;/code&gt;, so we’re now worse off by around &lt;code class="language-markup"&gt;23.5 Wh&lt;/code&gt; (although still ahead of the oven).&lt;/p&gt;

&lt;p&gt;So, although cooking at 180c does reduce the energy consumption a little&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;It doesn’t make a material difference to the conclusion: it’s still a poor solution to energy prices &lt;em&gt;unless&lt;/em&gt; you already own one&lt;/li&gt;
  &lt;li&gt;You’ll save around &lt;code class="language-markup"&gt;£0.009&lt;/code&gt; versus cooking at 200c&lt;/li&gt;
  &lt;li&gt;The chips you get from those cooks won’t be nearly as good as those done at 200c.&lt;/li&gt;
  &lt;li&gt;Adjusting the cook time to account for the lower temperature results in higher energy usage than the shorter, high-temperature cook&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One additional concern I’ve developed since originally writing the post is realistic usage patterns.&lt;/p&gt;

&lt;p&gt;The air fryer will save you a little money if you use it instead of the oven. However, because it makes such good chips, it’s easy to slip into the habit of using it &lt;em&gt;as well&lt;/em&gt; as the oven, increasing your overall power consumption. Having a dual-zone air fryer can help with this a little, but it becomes quite hard to put chips in the oven knowing how much better they’ll be from the fryer.&lt;/p&gt;
</description>
      <pubDate>Wed, 27 Nov 2024 07:00:00 +0000</pubDate>
      <link>https://www.influxdata.com/blog/air-fryer-vs-oven-energy-consumption-influxdb/</link>
      <guid isPermaLink="true">https://www.influxdata.com/blog/air-fryer-vs-oven-energy-consumption-influxdb/</guid>
      <category>Developer</category>
      <author>Ben Tasker (InfluxData)</author>
    </item>
    <item>
      <title>Building My Own Streaming TV Station</title>
      <description>&lt;p&gt;Recently a colleague mentioned having seen a YouTuber set up their own cable TV channel at home.&lt;/p&gt;

&lt;p&gt;This struck me as quite a good idea: I sometimes want to have the TV on in the background (the background noise helps the dogs settle) but often struggle to pick something from the myriad of available options (which include a ripped copy of most of our DVDs).&lt;/p&gt;

&lt;p&gt;Live TV and Prime Video aren’t really viable options: the aim is to have some background entertainment, not to get distracted by annoying ads for stuff that I’m never going to buy.&lt;/p&gt;

&lt;p&gt;Having an ad-free channel that only plays stuff that I like would make it a lot easier to just slap something on; it’s not like I’m usually intending to watch it.&lt;/p&gt;

&lt;p&gt;We’ve got a range of devices in the house, so I wanted my implementation to use commonly supported protocols (HLS and/or RTMP) and, ideally, to be able to run on my Kubernetes cluster.&lt;/p&gt;

&lt;p&gt;In this post, I’ll talk about some of the more interesting aspects of the container image that I built in order to run my own streaming TV station using my collection of ripped DVDs.&lt;/p&gt;

&lt;h2 id="overview"&gt;Overview&lt;/h2&gt;

&lt;p&gt;The system is simply a docker container which randomly selects media from a local library, making it available to players via RTMP and HLS.&lt;/p&gt;

&lt;p&gt;Just like a TV channel, it plays its own schedule.&lt;/p&gt;

&lt;p&gt;Inevitably, I got a little carried away, so the feature set also includes&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Allow and Block lists for media: constraining &lt;em&gt;what&lt;/em&gt; the channel can stream&lt;/li&gt;
  &lt;li&gt;Ability to set a broadcast window: constraining &lt;em&gt;when&lt;/em&gt; the channel can stream&lt;/li&gt;
  &lt;li&gt;Dynamic broadcast triggering: reducing use of compute if no one’s actually watching&lt;/li&gt;
  &lt;li&gt;Writing play history and other stats into InfluxDB&lt;/li&gt;
  &lt;li&gt;An API endpoint to skip to the next episode (in case of playback issues or “I’ve seen this”)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I chose to use InfluxDB rather than (for example) writing a textual log because writing into a time-series database allows easier exploration and analysis of the play history. It also allows the history to easily be correlated against [&lt;a href="https://projects.bentasker.co.uk/gils_projects/issue/project-management-only/home-tv-station/15.html/?utm_source=website&amp;amp;utm_campaign=building_streaming_tv_station_influxdb&amp;amp;utm_content=blog"&gt;performance and usage stats&lt;/a&gt;] (for example, to identify whether certain TV series tend to result in an increased frame-drop rate).&lt;/p&gt;

&lt;p&gt;Other InfluxDB features (like support for bucket &lt;a href="https://docs.influxdata.com/influxdb/v2/reference/internals/data-retention/?utm_source=website&amp;amp;utm_medium=direct&amp;amp;utm_campaign=building_streaming_tv_station_influxdb&amp;amp;utm_content=blog"&gt;retention periods&lt;/a&gt; also allow easy management of that data’s life cycle (for example, by expiring stuff out when it’s old enough that it’s not likely to be of interest).&lt;/p&gt;

&lt;h2 id="running"&gt;Running&lt;/h2&gt;

&lt;p&gt;Invocation via docker is simple:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/2dc592b405b175bdc38c78af6170a856.js
"&gt;

&lt;/script&gt;

&lt;p&gt;Inevitably, the YAML to run on Kubernetes is &lt;a href="https://github.com/bentasker/Home-TV-Station/blob/main/example/tvstation.yml/?utm_source=website&amp;amp;utm_campaign=building_streaming_tv_station_influxdb&amp;amp;utm_content=blog"&gt;a little more verbose&lt;/a&gt; but still pretty simple as K8s config goes:&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/88c582c63c1f46cb954015e57cfa3e3e/640d41b0f3b451606faf1351dbfb4c23/unnamed.png" width="450" height="auto" alt="YAMAL" /&gt;&lt;/p&gt;

&lt;h2 id="rtmp--hls"&gt;RTMP &amp;amp; HLS&lt;/h2&gt;

&lt;p&gt;Thanks to some of my previous jobs, I’ve got &lt;em&gt;quite a bit&lt;/em&gt; of prior experience with video delivery, including RTMP.&lt;/p&gt;

&lt;p&gt;In fact, in a previous role, I owned and operated a private fork of &lt;a href="https://github.com/arut/nginx-rtmp-module/?utm_source=website&amp;amp;utm_campaign=building_streaming_tv_station_influxdb&amp;amp;utm_content=blog"&gt;nginx-rtmp&lt;/a&gt;, that had to be heavily customized (in other words, I did some unspeakable things to it) in pursuit of &lt;em&gt;extremely&lt;/em&gt; diverse customer needs.&lt;/p&gt;

&lt;p&gt;This experience is relevant because one of the things that you get from owning, operating and supporting a custom RTMP application (particularly at CDN scale), is the &lt;em&gt;wisdom&lt;/em&gt; to know that it’s something that you only ever want to do &lt;em&gt;once&lt;/em&gt; (if that…).&lt;/p&gt;

&lt;p&gt;So, although &lt;code class="language-markup"&gt;nginx-rtmp&lt;/code&gt; sits at the core of this build, I’ve not customized it &lt;em&gt;at all&lt;/em&gt; and have instead built tooling around it.&lt;/p&gt;

&lt;p&gt;Although I’ve built &lt;a href="https://www.bentasker.co.uk/categories/hls-stream-creator.html"&gt;HLS tooling&lt;/a&gt; in the past, there was no need for that this time: &lt;code class="language-markup"&gt;nginx-rtmp&lt;/code&gt; has HLS support built into it (it supports DASH too, but I’m not currently using it).&lt;/p&gt;

&lt;h2 id="video-processing"&gt;Video Processing&lt;/h2&gt;

&lt;p&gt;The project involves reading in arbitrary video files, so, obviously, I reached for the Swiss army knife of video conversion: &lt;a href="https://ffmpeg.org/"&gt;FFmpeg&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using &lt;code class="language-markup"&gt;ffmpeg&lt;/code&gt; to &lt;a href="https://snippets.bentasker.co.uk/page-1706300952-Publish-file-to-RTMP-Server-%28FFMPEG%29-BASH.html"&gt;stream files to a RTMP server&lt;/a&gt; is a well-solved problem:&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/6OQIJtlbYatjsJj1iDFMD1/38c33d44061ae6a708152297863a0118/2.jpg" alt="image 2" /&gt;
The crucial flag in this command is &lt;code class="language-markup"&gt;-re&lt;/code&gt; , it instructs &lt;code class="language-markup"&gt;ffmpeg&lt;/code&gt; to read the file at its native frame rate, ensuring that the file is streamed in real time rather than all at once.&lt;/p&gt;

&lt;h2 id="publishing-script"&gt;Publishing Script&lt;/h2&gt;

&lt;p&gt;I wrote the automated publishing in BASH.&lt;/p&gt;

&lt;p&gt;At the job with the RTMP stack, I had a co-worker who would occasionally jokingly leave a code review comment on shell scripts: “Line count suggests this should have had a &lt;code class="language-markup"&gt;.py&lt;/code&gt;  extension instead.”&lt;/p&gt;

&lt;p&gt;I imagine that the publishing script would attract a similar review, and…. he’d not be wrong. I started to regret choosing BASH at about the time that I realised I needed to iterate through lines in a file and then do the equivalent of &lt;code class="language-markup"&gt;'|'.join()&lt;/code&gt; on them.&lt;/p&gt;

&lt;p&gt;But, I was already invested… Maybe I’ll rewrite it at some point.&lt;/p&gt;

&lt;h2 id="video-selection"&gt;Video Selection&lt;/h2&gt;

&lt;p&gt;The publishing script chooses a random series followed by a random episode, filtering each with a regular expression constructed from the blocklist.&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/67614269b915a16bfb16c7ae1ceca22e.js
"&gt;

&lt;/script&gt;

&lt;p&gt;The filter is applied at both stages so that I can include filters that target episodes as well as series.&lt;/p&gt;

&lt;p&gt;For example, if I wanted to ensure that specials aren’t included, I might filter out any file containing the string &lt;code class="language-markup"&gt;s00e&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id="dynamic-publishing-trigger"&gt;Dynamic Publishing Trigger&lt;/h2&gt;

&lt;p&gt;The first build of the container was pretty simple:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Launch &lt;code class="language-markup"&gt;nginx-rtmp&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Loop, publishing random episodes into &lt;code class="language-markup"&gt;nginx-rtmp&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result was an IPTV channel that was always on.&lt;/p&gt;

&lt;p&gt;But…. It was &lt;em&gt;always&lt;/em&gt; on. Even if I wasn’t watching, &lt;code class="language-markup"&gt;ffmpeg&lt;/code&gt; was dutifully whizzing away, tying up precious cores and sapping electricity:&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/4dX50ksyNHSeRSxq95uYKj/bb86a2480de5c19fe4659ecb9834cd02/3.png" alt="image 3" /&gt;&lt;/p&gt;

&lt;p&gt;So, I decided to change it so that the publishing script only starts a new episode &lt;a href="https://projects.bentasker.co.uk/gils_projects/issue/project-management-only/home-tv-station/11.html"&gt;if a player is connected&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code class="language-markup"&gt;nginx-rtmp&lt;/code&gt; module has a range of event hooks, including a &lt;code class="language-markup"&gt;play&lt;/code&gt; event. Each hook places  a simple HTTP request to an arbitrary endpoint.&lt;/p&gt;

&lt;p&gt;So, with a single line of config:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/40d3b6a8295d012a3799c53bcd2cf995.js
"&gt;

&lt;/script&gt;

&lt;p&gt;Nginx sends details of the play request onto the destination:&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/lx67xE5U9Tej0UN9wYaKd/ffdcefc2d5c9c7c05ac84582b0aadfcc/4.png" alt="image 4" /&gt;&lt;/p&gt;

&lt;p&gt;I &lt;a href="https://projects.bentasker.co.uk/gils_projects/issue/project-management-only/home-tv-station/11.html#comment7570"&gt;cobbled together&lt;/a&gt; a small control server (in Python—I wasn’t repeating the mistake I made with the entry point), allowing for a somewhat janky but functional flow:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Player connects to &lt;code class="language-markup"&gt;nginx-rtmp&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class="language-markup"&gt;nginx-rtmp&lt;/code&gt; sends &lt;code class="language-markup"&gt;play&lt;/code&gt; event to control server&lt;/li&gt;
  &lt;li&gt;Control server writes &lt;code class="language-markup"&gt;play&lt;/code&gt; (and player count) to control files&lt;/li&gt;
  &lt;li&gt;Publishing script detects a change in state and starts &lt;code class="language-markup"&gt;ffmpeg&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Playback starts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There’s a similar workflow if a player later disconnects:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Player disconnects&lt;/li&gt;
  &lt;li&gt;&lt;code class="language-markup"&gt;nginx-rtmp&lt;/code&gt; sends &lt;code class="language-markup"&gt;play_done&lt;/code&gt; event to control server&lt;/li&gt;
  &lt;li&gt;Control server subtracts 1 from player count&lt;/li&gt;
  &lt;li&gt;If player count is &amp;lt; 1, writes &lt;code class="language-markup"&gt;stop&lt;/code&gt; state&lt;/li&gt;
  &lt;li&gt;&lt;code class="language-markup"&gt;ffmpeg&lt;/code&gt; continues current stream but does not start next while state is &lt;code class="language-markup"&gt;stop&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I decided not to stop &lt;code class="language-markup"&gt;ffmpeg&lt;/code&gt; on disconnect because I might want to reconnect (perhaps as a result of changing rooms).&lt;/p&gt;

&lt;p&gt;In the logs, it looks like this:&lt;/p&gt;

&lt;video controls="" autoplay="" name="media"&gt;&lt;source src="https://videos.bentasker.co.uk/2024/20240807_tv_dynamic_startup/tv_channel_dynamic_startup.mp4?t=3b18907b5c2c16746fe5f384afb60b76ba4fbf636ed765253b87f91000aba855&amp;amp;e=1724658079" type="video/mp4" /&gt;&lt;/video&gt;

&lt;p&gt;Technically, this process extends video startup times, but to help ensure smooth playback, RTMP players tend to sit and fill a buffer first, so the additional delay is not all that noticeable.&lt;/p&gt;

&lt;p&gt;Introducing this delivered a couple of benefits, including:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;CPU &amp;amp; electricity aren’t wasted processing something that no one is watching&lt;/li&gt;
  &lt;li&gt;I don’t miss the beginning of episodes because the system won’t start the stream til someone’s watching&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one real drawback is that it only works with RTMP streaming. HLS is served over a standard HTTP connection, so there’s no simple equivalent (there &lt;em&gt;are&lt;/em&gt; ways around it, but they’ve all got tradeoffs that I didn’t want to have to make).&lt;/p&gt;

&lt;h2 id="broadcast-window"&gt;Broadcast Window&lt;/h2&gt;

&lt;p&gt;If I’m honest, implementing this was &lt;em&gt;much&lt;/em&gt; more about nostalgia than efficiency.&lt;/p&gt;

&lt;p&gt;The broadcast window states that the system should only broadcast video between set hours:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/3f1c29c169ab03cafc049388c6865e78.js
"&gt;
&lt;/script&gt;

&lt;p&gt;Outside of those times, rather than starting a new episode, players will receive an old BBC test card.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/1jn7lf5grrbJz5hW1hvpSF/1417027d6f6497dfa314b0e41e00d102/5.png" alt="image 5" /&gt;&lt;/p&gt;

&lt;p&gt;It does differ a little from the original, though, in that the card is streamed in silence. Nostalgia only goes so far, and I didn’t &lt;em&gt;really&lt;/em&gt; fancy reliving the experience of being woken by the beep kicking in.&lt;/p&gt;

&lt;h2 id="avoiding-freezes-between-episodes"&gt;Avoiding Freezes Between Episodes&lt;/h2&gt;

&lt;p&gt;The system’s design means that &lt;code class="language-markup"&gt;ffmpeg&lt;/code&gt;  has to consume a range of arbitrary video files. Having been ripped over the course of quite a few years, there’s quite a lot of variation between them, including:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Video and audio codecs&lt;/li&gt;
  &lt;li&gt;Framerate&lt;/li&gt;
  &lt;li&gt;Resolution&lt;/li&gt;
  &lt;li&gt;Pixel Format (rarer, but still)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code class="language-markup"&gt;nginx-rtmp&lt;/code&gt; pretty much just forwards what’s being piped into it, so when an episode changes, the downstream player might end up receiving frames that are completely different to those that came before.&lt;/p&gt;

&lt;p&gt;Some players (like &lt;code class="language-markup"&gt;ffplay&lt;/code&gt;) handle this ok. Most, though, end up freezing in response.&lt;/p&gt;

&lt;p&gt;To avoid these unexpected changes, I adjusted the &lt;code class="language-markup"&gt;ffmpeg&lt;/code&gt; invocation so that it would normalize it’s output:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/9f3ca86a3eca67182df067b9fda17f7f.js
"&gt;
&lt;/script&gt;

&lt;p&gt;In order to ensure a common resolution, a &lt;a href="https://trac.ffmpeg.org/wiki/Scaling"&gt;scale filter&lt;/a&gt; is used to target 720p. If the input file has a lower resolution, then letterboxing is used.&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/99125db0f1f679d1a0b18d2593a57370.js
"&gt;

&lt;/script&gt;

&lt;p&gt;With that change made, the transition between episodes became a lot more reliable.&lt;/p&gt;

&lt;p&gt;I also implemented an optional mechanism that could be &lt;a href="https://projects.bentasker.co.uk/gils_projects/issue/project-management-only/home-tv-station/5.html#comment7585"&gt;used to redirect players&lt;/a&gt; away from the stream (and back again) at episode change-over. However, Kodi’s Simple IPTV Player plugin doesn’t seem to like that very much.&lt;/p&gt;

&lt;h2 id="play-history-and-stats"&gt;Play History and Stats&lt;/h2&gt;

&lt;p&gt;We’ve amassed a fair collection of episodes over the years, enough that I sometimes may not recognise what we’re watching (or perhaps I’m just getting old).&lt;/p&gt;

&lt;p&gt;Originally, I implemented a small API endpoint which could be called to check what’s currently playing.&lt;/p&gt;

&lt;p&gt;But what if I’m absorbed and forget to check until after it’s finished? Then I’ll never know what that great show was!&lt;/p&gt;

&lt;p&gt;Instead, I decided to have the container write a play history into InfluxDB.&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/cc84b8913acf28269e76cdb2b413b6b4.js
"&gt;
&lt;/script&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/4W3KNHnCmuznsAAFoqCdif/a391e548e9c2e6ce5e7a3b44004744fe/7.png" alt="image 6" /&gt;&lt;/p&gt;

&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;At its heart, this was just a fun project to play around with and keep myself out of trouble for a couple of days.&lt;/p&gt;

&lt;p&gt;But, it also meets its target use case quite well, even if it doesn’t always succeed in just being in the background: the other evening I sat down, flicked it on and was greeted by an episode of &lt;a href="https://en.m.wikipedia.org/wiki/Bucky_O%27Hare"&gt;Bucky O’Hare&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Best of all, there are no ad breaks. Admittedly, I did consider adding one so that I could sue Musk for “witholding billlions of dollars in advertising revenue” (that’s &lt;a href="https://www.theregister.com/2024/08/06/twitter_rumble_wfa_musk/"&gt;how it works now&lt;/a&gt;, right?).&lt;/p&gt;

&lt;p&gt;The pod’s resource demands vary a bit depending on the episode being streamed: processing higher resolution videos obviously demands more resources than lower res ones. I’ve currently got the pod capped at 2 cores and that’s been fine for the majority of media.&lt;/p&gt;

&lt;p&gt;Not that I’m going to be adding &lt;em&gt;that&lt;/em&gt; many players, but the majority of the compute expense is in the initial publishing—the actual stream delivery is incredibly cheap (I’d likely hit network contention before CPU became an issue on that front).&lt;/p&gt;
</description>
      <pubDate>Tue, 08 Oct 2024 08:00:00 +0000</pubDate>
      <link>https://www.influxdata.com/blog/building-streaming-tv-station-influxDB/</link>
      <guid isPermaLink="true">https://www.influxdata.com/blog/building-streaming-tv-station-influxDB/</guid>
      <category>Developer</category>
      <author>Ben Tasker (InfluxData)</author>
    </item>
    <item>
      <title>Deploying InfluxDB and Telegraf to Monitor Kubernetes</title>
      <description>&lt;p&gt;I run a small Kubernetes cluster at home, which I originally set up as somewhere to experiment.&lt;/p&gt;

&lt;p&gt;Because it started as a playground, I never bothered to set up monitoring. However, as time passed, I’ve ended up dropping more production-esque workloads onto it, so I decided I should probably put some observability in place.&lt;/p&gt;

&lt;p&gt;Not having visibility into the cluster was actually a little odd, considering that &lt;a href="https://www.bentasker.co.uk/posts/blog/house-stuff/monitoring-a-fishtank-with-influxdb-and-grafana.html"&gt;even my fish tank can page me&lt;/a&gt;. I don’t need (or want) the cluster to be able to generate pages, but I do still want the underlying metrics, if only for capacity planning.&lt;/p&gt;

&lt;p&gt;In this post I talk about deploying and automatically pre-configuring an &lt;a href="https://github.com/influxdata/influxdb"&gt;InfluxDB&lt;/a&gt; OSS 2.x instance (with optional EDR) and &lt;a href="https://github.com/influxdata/telegraf"&gt;Telegraf&lt;/a&gt; to collect metrics for visualization with Grafana.&lt;/p&gt;

&lt;h2 id="the-plan"&gt;The plan&lt;/h2&gt;

&lt;p&gt;First, a quick overview of the plan:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Create a dedicated namespace (called &lt;code class="language-markup"&gt;monitoring&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;Deploy and preconfigure an InfluxDB OSS 2.x instance to write metrics into&lt;/li&gt;
  &lt;li&gt;Deploy Telegraf as a DaemonSet to run on every node using both the &lt;a href="https://github.com/influxdata/telegraf/blob/master/plugins/inputs/kubernetes/README.md"&gt;kubernetes&lt;/a&gt; and &lt;a href="https://github.com/influxdata/telegraf/tree/master/plugins/inputs/kube_inventory"&gt;kube_inventory&lt;/a&gt; plugins&lt;/li&gt;
  &lt;li&gt;Graph out metrics in Grafana (I already had a Grafana instance—see &lt;a href="https://www.bentasker.co.uk/posts/documentation/general/running-grafana-in-a-kubernetes-cluster.html"&gt;here&lt;/a&gt; if you need to roll one out)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because this is a home lab, I’m going to commit an ops-sin and deploy InfluxDB into the same cluster that I’m using it to monitor.&lt;/p&gt;

&lt;p&gt;If you’re doing this in production, you should ensure that metrics are available during an outage by deploying InfluxDB elsewhere, using &lt;a href="https://cloud2.influxdata.com/"&gt;InfluxDB Cloud&lt;/a&gt;, or configuring &lt;a href="https://www.influxdata.com/products/influxdb-edge-data-replication/"&gt;Edge Data Replication (EDR)&lt;/a&gt; so that the data is replicated out of the cluster.&lt;/p&gt;

&lt;p&gt;In the steps below, I’ve made it easy to configure EDR (no changes are needed if you don’t want it; just don’t create that secret).&lt;/p&gt;

&lt;p&gt;Throughout this doc, I’m going to include snippets of YAML. The assumption is that you’ll append them to a file (mine’s called &lt;code class="language-markup"&gt;influxdb-kubemonitoring.yml&lt;/code&gt;) ready for feeding into &lt;code class="language-markup"&gt;kubectl&lt;/code&gt; at various points.&lt;/p&gt;

&lt;p&gt;For those in a hurry, there’s also a copy of the manifests below in my &lt;a href="https://github.com/bentasker/article_scripts/tree/main/monitoring-k8s-with-telegraf-and-influxdb"&gt;article_scripts repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id="why-influxdb"&gt;Why InfluxDB?&lt;/h2&gt;

&lt;p&gt;Using InfluxDB and Telegraf offers a number of benefits:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Easy off-site replication (via &lt;a href="https://www.influxdata.com/products/influxdb-edge-data-replication/"&gt;EDR&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;Ease of setup: Telegraf has everything rolled in&lt;/li&gt;
  &lt;li&gt;Metrics can be queried with InfluxQL (and/or &lt;a href="https://docs.influxdata.com/influxdb/cloud-serverless/query-data/sql/"&gt;SQL&lt;/a&gt; if replicated into v3)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because Telegraf can buffer data and InfluxDB can accept writes into the past, if something &lt;em&gt;were&lt;/em&gt; to happen to the cluster, there’s the potential to gain retrospective visibility into its state when later analysing the cause of an incident.&lt;/p&gt;

&lt;p&gt;And, of course, there’s familiarity: all of my metrics go into InfluxDB, so it makes most sense to use the solution I’m most comfortable with.&lt;/p&gt;

&lt;h2 id="namespace"&gt;Namespace&lt;/h2&gt;

&lt;p&gt;Everything will be deployed into a namespace called &lt;code class="language-markup"&gt;monitoring&lt;/code&gt;, so first we need to create that:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/ffe9685d9092f9be0ce27862349b9fbe.js
"&gt;

&lt;/script&gt;

&lt;h2 id="secret-creation"&gt;Secret creation&lt;/h2&gt;

&lt;p&gt;We’re going to need to define a secret containing several items, including:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Admin credentials for the InfluxDB instance&lt;/li&gt;
  &lt;li&gt;A token to create in the InfluxDB instance&lt;/li&gt;
  &lt;li&gt;The name of the InfluxDB org&lt;/li&gt;
  &lt;li&gt;Credentials for two non-privileged users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These will be used to create the accounts within InfluxDB.&lt;/p&gt;

&lt;p&gt;I use &lt;a href="https://snippets.bentasker.co.uk/page-1705141202-Password-Generator-BASH.html"&gt;gen_passwd&lt;/a&gt; to generate passwords but use whatever suits you.&lt;/p&gt;

&lt;p&gt;In order to create a random token, I used:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/cb4b6474a94658614f72ed5e6f7ccecf.js
"&gt;

&lt;/script&gt;

&lt;p&gt;Create a secret called &lt;code class="language-markup"&gt;influxdb-info&lt;/code&gt;:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/e0363c6867c002f07b6b5f52643c0a81
.js
"&gt;

&lt;/script&gt;

&lt;h2 id="storage"&gt;Storage&lt;/h2&gt;

&lt;p&gt;InfluxDB needs some persistent storage so our metrics remain available even after a pod is rolled.&lt;/p&gt;

&lt;p&gt;Generally, I use NFS to access my NAS, so I defined a NFS backed physical volume and an associated claim:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/01e7cfbd23503983eb24e1ad8bf62b6f.js"&gt;

&lt;/script&gt;

&lt;h2 id="optional-edge-data-replication"&gt;Optional: Edge Data Replication&lt;/h2&gt;

&lt;p&gt;As we’re deploying InfluxDB into the monitored cluster, to help ensure continuity, we might want to replicate the data onward (to InfluxDB cloud or another OSS instance) so that it also exists outside the cluster.&lt;/p&gt;

&lt;p&gt;OSS 2.X has a feature called &lt;a href="https://www.influxdata.com/products/influxdb-edge-data-replication/"&gt;Edge Data Replication&lt;/a&gt;—this involves defining a remote (i.e., where we want to replicate to) and then specifying which bucket should be replicated.&lt;/p&gt;

&lt;p&gt;To do this, you will need:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The URL of your remote instance&lt;/li&gt;
  &lt;li&gt;An authentication token for the remote instance&lt;/li&gt;
  &lt;li&gt;The org ID to use with the remote instance&lt;/li&gt;
  &lt;li&gt;The ID of the remote bucket you want to write into&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you decide not to configure EDR and want to add it later, you can always &lt;a href="https://docs.influxdata.com/influxdb/v2/write-data/replication/replicate-data/"&gt;enable it manually&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To enable EDR, we’re going to define a secret to hold the information necessary to configure it:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/ea183d7b28d55a133c4a853f9e1f1264
.js"&gt;

&lt;/script&gt;

&lt;p&gt;The manifests we create later in this post will reference these values (they’re optional, so nothing should error if the secret isn’t defined).&lt;/p&gt;

&lt;h2 id="preconfiguring-influxdb-auth"&gt;Preconfiguring InfluxDB Auth&lt;/h2&gt;

&lt;p&gt;The InfluxDB container supports pre-configuration of credentials via environment variables, so we’re going to pass in references to the secret that we created earlier.&lt;/p&gt;

&lt;p&gt;However, there is still a catch: Although the image allows us to pass a known token value (via &lt;code class="language-markup"&gt;DOCKER_INFLUXDB_INIT_ADMIN_TOKEN&lt;/code&gt;) it’s used to create the &lt;em&gt;operator token&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The operator token is an &lt;em&gt;extremely&lt;/em&gt; privileged credential, and not something that we really want to be passing into Grafana and Telegraf.&lt;/p&gt;

&lt;p&gt;We need to pre-populate some less privileged credentials, but neither the image nor InfluxDB provides a means to create a (non-operator) &lt;em&gt;token&lt;/em&gt; with a known value. We could write a script to call the API and mint a pair of tokens before writing them into a k8s secret, but we’d then need to give &lt;em&gt;something&lt;/em&gt; the ability to update secrets, which is somewhat less than ideal.&lt;/p&gt;

&lt;p&gt;Instead, we can create a username/password pair that can then be used with the &lt;a href="https://docs.influxdata.com/influxdb/v2/api/v1-compatibility/"&gt;v1 API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The InfluxDB image can run arbitrary init scripts (but only triggers them if Influx’s data files exist, so there’s no risk of accidental re-runs).&lt;/p&gt;

&lt;p&gt;We’re going to define a ConfigMap to store a small script that will create a DBRP policy and a pair of non-privileged users (one read-only, one write-only). The script also configures EDR if we’ve chosen to enable it.&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/7c68c7b38c7a43c4d60ce0382960b596.js"&gt;

&lt;/script&gt;

&lt;h2 id="deploying-influxdb"&gt;Deploying InfluxDB&lt;/h2&gt;

&lt;p&gt;Next, we need to deploy InfluxDB itself.&lt;/p&gt;

&lt;p&gt;Things to note about this definition are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Variables &lt;code class="language-markup"&gt;DOCKER_INFLUXDB_INIT_BUCKET&lt;/code&gt; and &lt;code class="language-markup"&gt;DOCKER_INFLUXDB_INIT_RETENTION&lt;/code&gt; tell InfluxDB to create a bucket called &lt;code class="language-markup"&gt;telegraf&lt;/code&gt; with a retention policy of 90 days.&lt;/li&gt;
  &lt;li&gt;My NFS share is configured to squash permissions, so I included a &lt;code class="language-markup"&gt;securityContext&lt;/code&gt; to use the appropriate UID.&lt;/li&gt;
  &lt;li&gt;We make the various values from our secret available via the environment variable.&lt;/li&gt;
  &lt;li&gt;As well as the PVC, we mount the configmap containing the init script into the container.&lt;/li&gt;
&lt;/ul&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/1a3ac287d4c9855174d971a30976d5cd
.js"&gt;

&lt;/script&gt;

&lt;p&gt;The Deployment will need to be fronted by a service, so we define that next, passing port 8086 through:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/ed31eeabf4d01c258e6b4ad1f510058e
.js"&gt;

&lt;/script&gt;

&lt;p&gt;If we apply our work so far:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/78a35cd2ae2421a6d57bb852832de869
.js"&gt;

&lt;/script&gt;

&lt;p&gt;We should now be able to retrieve service details:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/2d70f53821c814dc5b272932658c997e
.js"&gt;

&lt;/script&gt;

&lt;p&gt;It should now also be possible to use the cluster IP to run a query against InfluxDB (replace &lt;code class="language-markup"&gt;$INFLUX_TOKEN&lt;/code&gt; with the token you generated when first defining the secret).&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/514508a938b0150264d661f1afbee160
.js"&gt;

&lt;/script&gt;

&lt;p&gt;We &lt;em&gt;should&lt;/em&gt; also be able to use the read-only creds with the v1 query API:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/8d46e588da91e5159c705422d7fe0dd0
.js"&gt;

&lt;/script&gt;

&lt;h2 id="deploying-telegraf"&gt;Deploying Telegraf&lt;/h2&gt;

&lt;p&gt;Deploying Telegraf into Kubernetes is really easy. However, authorizing it to fetch information from the Kubernetes APIs involves interacting with the k8s auth model, which is a bit more complex.&lt;/p&gt;

&lt;p&gt;First, we’re going to define a service account called telegraf and then authorize that to talk to both kubelet and cluster APIs.&lt;/p&gt;

&lt;p&gt;Define the service account:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/d0824b684b09dc0c14746b837ef1263c
.js"&gt;

&lt;/script&gt;

&lt;p&gt;Define a pair of cluster roles:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/82aff4a546cd7393048cb8e320fdd903
.js"&gt;

&lt;/script&gt;

&lt;p&gt;Note: Technically, you could probably combine those, but as they give access to different things, I prefer to keep them separate.&lt;/p&gt;

&lt;p&gt;Finally, create the role bindings to link both back to the Service Account:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/a4e401e11e7ccaf53c3d8f591fe298e6
.js"&gt;

&lt;/script&gt;

&lt;p&gt;With that unpleasantness out of the way, we’re ready to configure and deploy Telegraf.&lt;/p&gt;

&lt;p&gt;As the Telegraf config is simple, we drop it into a ConfigMap.&lt;/p&gt;

&lt;p&gt;Note: There’s no need to replace the variables by hand; we’ll be setting them in the container config.&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/80c37b5674be0baaadce8ba39f7bf154
.js"&gt;

&lt;/script&gt;

&lt;p&gt;Then, we’re ready to define our DaemonSet.&lt;/p&gt;

&lt;p&gt;There are a couple of important points here:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;We’re exposing the node IP via env var &lt;code class="language-markup"&gt;HOSTIP&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;We’re setting the env var &lt;code class="language-markup"&gt;HOSTNAME&lt;/code&gt; to the node Hostname via &lt;code class="language-markup"&gt;spec.nodeName&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;We’re passing in credentials from the secret we created earlier&lt;/li&gt;
  &lt;li&gt;We’re mapping directories from the host into the container&lt;/li&gt;
  &lt;li&gt;We’re mounting our configmap as config&lt;/li&gt;
  &lt;li&gt;We set &lt;code class="language-markup"&gt;MONITOR_HOST&lt;/code&gt; to use the name of the service we created for InfluxDB (&lt;code class="language-markup"&gt;http://influxdb:8086&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/8fc31496307c6f93d51d6c4fb29a096f
.js"&gt;

&lt;/script&gt;

&lt;p&gt;If we apply the updated config:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/b5ed34a6790c180c9ee58695ae6c17d0
.js"&gt;

&lt;/script&gt;

&lt;p&gt;Telegraf should spring to life and start collecting metrics from the Kubernetes APIs.&lt;/p&gt;

&lt;p&gt;We can check by tailing Telegraf’s logs:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/53b2932a96734ec3138279efd607d1cf
.js"&gt;

&lt;/script&gt;

&lt;p&gt;If it’s not logging errors, then everything is working.&lt;/p&gt;

&lt;h2 id="checking-for-metrics"&gt;Checking for metrics&lt;/h2&gt;

&lt;p&gt;We can confirm that metrics are arriving in InfluxDB by logging into its web interface.&lt;/p&gt;

&lt;p&gt;Assuming that you deployed into the cluster, grab the cluster IP by running the following:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/7e7a9ba81e17503fb45ff95583ad5d69
.js"&gt;

&lt;/script&gt;

&lt;p&gt;Then, visit &lt;code class="language-markup"&gt;http://[cluster ip]:8086&lt;/code&gt; in a browser.&lt;/p&gt;

&lt;p&gt;You should be able to log in with the credentials you created at the beginning of this process.&lt;/p&gt;

&lt;p&gt;If you browse to the Data Explorer, you should see a bunch of Kubernetes-related measurements.
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/7a6843ab06ba450581186bd83a03c507/b355fc1057b41ee8b3525b8d2303e4e0/unnamed.png" alt="" /&gt;&lt;/p&gt;

&lt;h2 id="dashboarding"&gt;Dashboarding&lt;/h2&gt;

&lt;p&gt;With metrics coming into InfluxDB, we now just need a dashboard to help visualize things. I do all of my dashboarding in Grafana so that I only have to go to one place to visualize metrics from a wide variety of sources.&lt;/p&gt;

&lt;p&gt;We’ll need to tell Grafana how to speak to our new InfluxDB instance, so:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Burger Menu&lt;/li&gt;
  &lt;li&gt;&lt;code class="language-markup"&gt;Connections&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class="language-markup"&gt;Connect Data&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Search for &lt;code class="language-markup"&gt;InfluxDB&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When adding the new InfluxDB datasource in Grafana, you’ll need to provide the following:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Language: InfluxQL (because we’re using the v1 API)&lt;/li&gt;
  &lt;li&gt;&lt;code class="language-markup"&gt;URL&lt;/code&gt;: If InfluxDB and Grafana are running in the same cluster, you can simply use the InfluxDB service name. Otherwise you’ll need to provide an IP or domain name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/501350ee032348dbae87453654925d01/20096d16806a72e36a11ce108315d487/unnamed.png" alt="" /&gt;
Use the following settings in Grafana:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Database: &lt;code class="language-markup"&gt;telegraf&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;User: &lt;code class="language-markup"&gt;kubestatsro&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Password: the password we defined earlier&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the datasource has been created, we can start to pull stats out with queries like:&lt;/p&gt;

&lt;script src="https://gist.github.com/JessicaWachtel/b7b4fe39c0c9cf68a5aa2c0df6d4d0d5
.js"&gt;

&lt;/script&gt;

&lt;p&gt;Ultimately, we are building a single dashboard that allows us to view resource usage at node, namespace, and pod levels.&lt;/p&gt;

&lt;p&gt;For example, we can see that my &lt;a href="https://docs.atuin.sh/self-hosting/kubernetes/"&gt;Atuin sync server&lt;/a&gt; has been assigned far more resources than it’s actually using (potentially preventing other workloads from being scheduled on that node):
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/9d4144bf3db944b198750755fc10a148/6737d1010a85291a3b46354598dcde16/unnamed.png" alt="" /&gt; 
An importable copy of the dashboard can be found &lt;a href="https://github.com/bentasker/article_scripts/blob/main/monitoring-k8s-with-telegraf-and-influxdb/Kubernetes_Resource_usage.json"&gt;on Github&lt;/a&gt;. Because it uses InfluxQL, it should be compatible with InfluxDB 1.x, 2.x and 3.x.&lt;/p&gt;

&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;If you’ve been following along, you should now have an InfluxDB instance receiving metrics about each resource in your Kubernetes cluster. If you enabled EDR, these metrics will be replicated to an external instance in order to help ensure continuity of monitoring during outages.&lt;/p&gt;

&lt;p&gt;I now have the means to see a bit more about what’s going on inside my cluster, including the information I need to ensure that resource requests aren’t being set too high, unnecessarily limiting capacity in the process.&lt;/p&gt;

&lt;p&gt;Although YAML’s verbosity makes it seem like a lot more than it is, it didn’t take much to get InfluxDB and Telegraf up and running in the cluster.&lt;/p&gt;

&lt;p&gt;The auto-provisioning of credentials and configuration means that if I ever want to start over, I can simply wipe the PVCs and roll the pods—the image will recreate the credentials, and everything will come back up.&lt;/p&gt;

&lt;p&gt;To get started on this or another project on InfluxDB, sign up for a free &lt;a href="https://cloud2.influxdata.com/signup?utm_source=website&amp;amp;utm_medium=direct&amp;amp;utm_campaign=%20Deploying_InfluxDB_Telegraf_Monitor_Kubernetes&amp;amp;utm_content=blog"&gt;cloud account&lt;/a&gt; now.&lt;/p&gt;
</description>
      <pubDate>Tue, 17 Sep 2024 08:00:00 +0000</pubDate>
      <link>https://www.influxdata.com/blog/deploying-influxdb-telegraf-to-monitor-kubernetes/</link>
      <guid isPermaLink="true">https://www.influxdata.com/blog/deploying-influxdb-telegraf-to-monitor-kubernetes/</guid>
      <category>Developer</category>
      <author>Ben Tasker (InfluxData)</author>
    </item>
    <item>
      <title>Monitoring an Aquarium with InfluxDB and Grafana</title>
      <description>&lt;p&gt;&lt;em&gt;This article was originally posted on &lt;a href="https://www.bentasker.co.uk/posts/blog/house-stuff/monitoring-a-fishtank-with-influxdb-and-grafana.html#"&gt;Ben Tasker’s blog&lt;/a&gt; and is reposted here with permission.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I’ve been setting up a new tropical fish tank and wanted to add some monitoring and alerting because, well, why not?&lt;/p&gt;

&lt;p&gt;The key questions that I was interested in answering were:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Is the filter running properly?&lt;/li&gt;
  &lt;li&gt;Is the temperature within acceptable bounds?&lt;/li&gt;
  &lt;li&gt;Are scheduled things (like the surface skimmer and lights) actually happening?&lt;/li&gt;
  &lt;li&gt;Are both heaters working?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The plan is to also add monitoring for PH levels, but the probe that I need for that hasn’t arrived yet.&lt;/p&gt;

&lt;p&gt;In this post I’ll talk about the aquarium monitoring and alerting system that I’ve built using a Raspberry Pi, InfluxDB, Telegraf and Grafana.&lt;/p&gt;

&lt;h4 id="metrics-sources"&gt;Metrics sources&lt;/h4&gt;

&lt;p&gt;Some elements of tank health can be monitored via the metrics exposed by my existing  &lt;a href="https://www.bentasker.co.uk/posts/blog/house-stuff/739-monitoring-our-electricity-usage-with-influxdb.html"&gt;energy usage tracking implementation&lt;/a&gt;  which allow us to see which components are drawing power (and how much/when).&lt;/p&gt;

&lt;p&gt;All I needed to do for this was plug things like heaters into smart sockets and add them to the config consumed by  &lt;a href="https://github.com/bentasker/tplink_to_influxdb"&gt;my docker image&lt;/a&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;kasa:
    devices:
        # ... etc ...     
        -
            name: "aquarium-light"
            ip: 192.168.5.165

        -
            name: "aquarium-surface-skimmer"
            ip: 192.168.5.166
        # ... etc ...&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The use of smart sockets also means that  &lt;a href="https://www.bentasker.co.uk/categories/homeassistant.html"&gt;HomeAssistant&lt;/a&gt;  can be used as a timer to schedule runs of things like the surface skimmer and the aquarium light:&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/5yExMRjGUOFdzfcfPrgliT/08f029cf0a0d0e078174c9eec80948cf/actions.png" alt="actions" /&gt;&lt;/p&gt;

&lt;p&gt;Watching power consumption on its own isn’t enough because some of the metrics that we care about require  &lt;em&gt;physical&lt;/em&gt;  measurements to be taken.&lt;/p&gt;

&lt;p&gt;For example, watching the energy consumption of the pump is only part of the solution: if the impeller were to fail, the motor would continue drawing power whilst spinning uselessly. There are  &lt;em&gt;many&lt;/em&gt;  failure modes for an aquarium filter, and motor failure is just one, what we actually care about is the  &lt;em&gt;flow rate&lt;/em&gt;  that the pump is achieving.&lt;/p&gt;

&lt;p&gt;So, we need some sensors:&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/4BIwzHZgUdh2j0skc6EzV7/43385719209cd15cae3b7496ec51e342/probe_and_meter.jpg" alt="probe and meter" /&gt;&lt;/p&gt;

&lt;p&gt;The sensors used are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A DS18B20 based temperature probe (a  &lt;a href="https://en.wikipedia.org/wiki/1-Wire"&gt;1-wire&lt;/a&gt;  device)&lt;/li&gt;
  &lt;li&gt;A Digitem FL-408 Water Flow Meter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Obviously, the future PH monitoring project will also require a physical probe of its own.&lt;/p&gt;

&lt;h4 id="a-note-on-temperature-sensors"&gt;A note on temperature sensors&lt;/h4&gt;

&lt;p&gt;If you’re planning on building something similar, it’s worth noting here that it’s  &lt;strong&gt;very&lt;/strong&gt;  important to buy your temperature probe from a reputable vendor (like RS Components). There are  &lt;a href="https://github.com/cpetrich/counterfeit_DS18B20"&gt;a lot of fake DS18B20 chips&lt;/a&gt;  about and they tend to misbehave in annoying and unpredictable ways.&lt;/p&gt;

&lt;p&gt;The temperature sensor shown in the image above was ordered from Amazon and  &lt;em&gt;seemed&lt;/em&gt;  to work fine at first. However, after about 6 hours of operation it developed a habit of dropping off the bus entirely, not re-appearing unless the Pi was fully power-cycled.&lt;/p&gt;

&lt;p&gt;This behavior, apparently, is not uncommon amongst dodgy chips. They’re also known to return some  &lt;strong&gt;very&lt;/strong&gt;  suspect readings:&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/4zNJPnch7iNGbyfxA7FBIX/83716728da57c8b4b27fb201168cbb40/mental_temperature.png" alt="mental temperature" /&gt;&lt;/p&gt;

&lt;p&gt;That’s right, if the probe is to be believed, then (in a matter of moments) my tank went from ~20c to 3x water’s critical temperature…&lt;/p&gt;

&lt;p&gt;Once I became aware of the issue, I ordered a replacement probe from  &lt;a href="https://www.dfrobot.com/product-689.html"&gt;DSRobot&lt;/a&gt;. It even managed to be  &lt;em&gt;cheaper&lt;/em&gt;  than the original: knock-offs don’t just exist at the bottom end of the price range.&lt;/p&gt;

&lt;h4 id="raspberry-pi-build"&gt;Raspberry Pi build&lt;/h4&gt;

&lt;p&gt;I had a spare Raspberry Pi 4 that had sat unused since it’s SD card failed (I migrated stuff elsewhere at the time), so I decided to use that. In theory though, any GPIO equipped single board computer should work.&lt;/p&gt;

&lt;p&gt;For convenience’s sake, I ordered a GPIO break-out board with screw terminals.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/6R6q5H2PU9vHwUP6lIqDDJ/22275f1ed7ff169fba86039389e8f6c7/gpio_breakout_amz_small.jpg" alt="gpio breakout amz small" /&gt;&lt;/p&gt;

&lt;p&gt;It turned out, though, that this was not actually  &lt;em&gt;that&lt;/em&gt;  convenient a route, because what turned up was a board and a set of connectors with  &lt;em&gt;not a drop&lt;/em&gt;  of solder in sight.&lt;/p&gt;

&lt;p&gt;The manufacturer presumably had had a conversation along the following lines:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Pers 1: Should we provide these pre-assembled?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Pers 2: It’ll mean using solder, that costs money.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Pers 1: Customers will expect it to work though and they probably have a 
project they want to get moving on.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Pers 2: No, these people are  &lt;strong&gt;hobbyists&lt;/strong&gt;  they  &lt;strong&gt;love&lt;/strong&gt;  soldering. We’ll be giving them more of what they enjoy and they’ll love us for it&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So, with some grumbling, I took my “convenient” board and soldered the connectors onto it. This didn’t go  &lt;em&gt;particularly&lt;/em&gt;  well because I’d had a conversation of my own:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Me: Where the F@!# is my good soldering iron?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Me: the gas powered one will have to do&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Me: Oh, it’s only got a fat tip… better than nothing&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Me: Which **&lt;/em&gt;** didn’t order replacement solder the last time I nearly finished it all up?*&lt;/p&gt;

&lt;p&gt;It turns out (surprising no-one) that delicate soldering can be  &lt;em&gt;a bit&lt;/em&gt;  of a challenge when you’re having to compete with an unweildy soldering iron which is heating a big fat tip  &lt;strong&gt;and&lt;/strong&gt;  blowing hot exhaust out over fingers that are trying to hold onto an unnecessarily short length of solder.&lt;/p&gt;

&lt;p&gt;It might just be the worst bit of soldering I’ve ever done, but solder it I did:&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/17CbWQfLLaoucMbCuSUk8m/04bc85503b25551ce27f9bc8e8490759/GPIO_breakout.jpg" alt="GPIO breakout" /&gt;&lt;/p&gt;

&lt;p&gt;(No, I’m not being self-deprecating, you haven’t seen the  &lt;em&gt;other&lt;/em&gt;  side)&lt;/p&gt;

&lt;p&gt;With my newly soldered breakout board in hand, it was time to connect the sensors.&lt;/p&gt;

&lt;p&gt;The temperature probe came first, wiring as follows&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;VCC (red) to pin 1 (3.3v supply)&lt;/li&gt;
  &lt;li&gt;Signal (yellow) to pin 7 (GPIO 4)&lt;/li&gt;
  &lt;li&gt;Ground (black) to pin 6 (GND)&lt;/li&gt;
  &lt;li&gt;4.7k resistor between Pin 1 and 7&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/5ZFRBvk72QS0V820v2qzAS/b25311318f4e8ec2a9b16f390e551e23/wired_temperature_probe.jpg" alt="wired temperature probe" /&gt;&lt;/p&gt;

&lt;p&gt;Next was the water flow meter:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;VCC (red) to pin 17 (3.3v supply)&lt;/li&gt;
  &lt;li&gt;Signal (yellow) to pin 29 (GPIO 5)&lt;/li&gt;
  &lt;li&gt;Ground (black) to pin 39 (GND)&lt;/li&gt;
  &lt;li&gt;4.7k resistor between Pin 17 and 29&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/5PfRh9QysJpGtYlttJM6oN/ec131d51568a542ba9b9d773a74269e2/wired_water_meter.jpg" alt="wired water meter" /&gt;&lt;/p&gt;

&lt;p&gt;With the hardware connected, I moved onto kicking together some code to collect and submit readings.&lt;/p&gt;

&lt;h4 id="collecting-readings"&gt;Collecting readings&lt;/h4&gt;

&lt;p&gt;The two sensors are accessible in different ways. I  &lt;em&gt;could&lt;/em&gt;  have built a more graceful implementation, using a single script to collect readings from both, but opted not to — in part because I wanted to poll at entirely different intervals.&lt;/p&gt;

&lt;p&gt;Full copies of the scripts referred to below are available  &lt;a href="https://github.com/bentasker/article_scripts/tree/main/monitoring-a-fishtank-with-influxdb-and-grafana"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h5 id="temperature-sensor"&gt;Temperature sensor&lt;/h5&gt;

&lt;p&gt;The temperature sensor presents a 1-wire bus device which can simply be read from:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;$ cat /sys/bus/w1/devices/28-3c6df6482d24/w1_slave
5a 01 55 05 7f a5 a5 66 c3 : crc=c3 YES
5a 01 55 05 7f a5 a5 66 c3 t=21625&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In the output above, the sensor is reporting 21.63 degrees celsius.&lt;/p&gt;

&lt;p&gt;&lt;code class="language-bash"&gt;28-3c6df6482d24&lt;/code&gt;  is the serial number of the chip in the probe (with the benefit of hindsight, it also tells us that there’s a  &lt;a href="https://github.com/cpetrich/counterfeit_DS18B20#tldr-how-do-i-know"&gt;good chance&lt;/a&gt;  that the chip is a knock-off).&lt;/p&gt;

&lt;p&gt;Because the probe can be read using  &lt;code class="language-bash"&gt;cat&lt;/code&gt;, collecting and submitting readings from it is &lt;em&gt;very&lt;/em&gt; simple&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;#!/bin/bash
#
# Read temperature from a 1-wire temperature sensor
#
# example: ./read_temperature.sh "dev name"
#
# Copyright (c) 2023 B Tasker
# Released under BSD 3-Clause
# https://www.bentasker.co.uk/pages/licenses/bsd-3-clause.html
#
DEVNAME=${1:-"28-3c6df6482d24"}
LOCATION=${LOCATION:-"diningroom"}

# Read the sensor
ts=`date +'%s'`
tempread=`cat /sys/bus/w1/devices/$DEVNAME/w1_slave`
temp=`echo "scale=2; "\`echo ${tempread##*=}\`" / 1000" | bc`

# Build line protocol
lp="aquarium,location=$LOCATION water_temperature=$temp $ts"

# Write the stats out
curl -s -d "$lp" "http://127.0.0.1:8086/write?db=telegraf&amp;amp;precision=s"
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I copied the script over to the Pi and added a crontab entry to call it every minute.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;* * * * * pi /home/pi/tank-monitoring-scripts/app/read_temperature.sh&lt;/code&gt;&lt;/pre&gt;

&lt;h5 id="water-flow-meter"&gt;Water-flow meter&lt;/h5&gt;

&lt;p&gt;The water-flow meter required a  &lt;em&gt;little&lt;/em&gt;  more effort, because the OS doesn’t helpfully expose an interface for us to  &lt;code class="language-bash"&gt;cat&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The sensor itself is quite simple, consisting of a turbine (turned by the water passing through) and a hall-effect sensor (to pick up the turbine’s rotation).&lt;/p&gt;

&lt;p&gt;As the turbine spins, the sensor sends pulses down the wire to the GPIO pin, so we need to count those pulses and then convert them into a flow-rate (e.g. liters per hour).&lt;/p&gt;

&lt;p&gt;At its simplest, the counting could look something like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;#!/usr/bin/env python3
#
# Copyright (c) 2023 B Tasker
# Released under BSD 3-Clause
# https://www.bentasker.co.uk/pages/licenses/bsd-3-clause.html
#
import RPi.GPIO as GPIO

def countPulse(channel):
    ''' Callback function for the GPIO event
    Increment the counter, assuming the counter is active
    '''
    global counter
    if start_counter == 1:
        counter += 1

global counter
counter = 0

GPIO.setmode(GPIO.BCM)
GPIO.setup(GPIO_NUM, GPIO.IN, pull_up_down = GPIO.PUD_UP)
GPIO.add_event_detect(GPIO_NUM, GPIO.FALLING, callback=countPulse)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Obviously we need something to  &lt;em&gt;read&lt;/em&gt;  the value of  &lt;code class="language-bash"&gt;counter&lt;/code&gt;. But, it also needs to convert the count over to a flowrate.&lt;/p&gt;

&lt;p&gt;The label on the sensor tells us how to perform that conversion:&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/1UnZX0Z2U7TCgDd008Tp55/f77c1b3a4c28f94e88f3f03707d78f85/probe_label.jpg" alt="probe label" /&gt;&lt;/p&gt;

&lt;p&gt;The annotation  &lt;code class="language-bash"&gt;F=7.5*Q(L/min)&lt;/code&gt;  says that every 7.5 blips is equivalent to a rate of 1L/min.&lt;/p&gt;

&lt;p&gt;This means that the math that we need to perform is quite simple: count pulses for a second and then divide the count by  &lt;code class="language-bash"&gt;7.5&lt;/code&gt;  to get a rate in liters per minute.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;# We use this list to track rates between writes
flowrates = []

while True:
    try:
        # Capture some pulses
        start_counter = 1
        time.sleep(1)
        start_counter = 0

        # Calculate the flow rate
        # Pulse frequency (Hz) = 7.5Q, Q is flow rate in L/min.
        flow = (counter / 7.5)

        # Add to the list of observed rates
        flowrates.append(flow)

        counter = 0
        time.sleep(5)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Every iteration a flow-rate is calculated and pushed into the list  &lt;code class="language-bash"&gt;flowrates&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;5 seconds is a far more regular a reading than I really need to submit, but I wanted to take those regular readings and periodically aggregate them before writing out to InfluxDB:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;# We use this list to track rates between writes
flowrates = []

while True:
    try:
        # Capture some pulses
        start_counter = 1
        time.sleep(1)
        start_counter = 0

        # Calculate the flow rate
        flow = (counter / 7.5) # Pulse frequency (Hz) = 7.5Q, Q is flow rate in L/min.

        # Add to the list of rates
        flowrates.append(flow)

        # Write out
        if len(flowrates) &amp;gt;= 4:
            # Calculate bounds and average
            stats = {
                "min" : min(flowrates),
                "max" : max(flowrates),
                "mean" : sum(flowrates) / len(flowrates)
            }
            writeStat(stats, session)
            flowrates = []        

        counter = 0
        time.sleep(5)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now, the script periodically writes out the mean, min and max flowrate for that sampling interval.&lt;/p&gt;

&lt;p&gt;After tidying the script up a bit, I was ready to deploy it, so copied it over to the Pi and created a unit file:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;[Unit]
Description=FlowRate Monitor
After=multi-user.target

[Service]
Type=simple
ExecStart=/home/pi/tank-monitoring-scripts/app/read_flowrate.py
Restart=on-failure

[Install]
WantedBy=multi-user.target&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I then installed, enabled and started it.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;sudo -s
cp flowrate.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable flowrate
systemctl start flowrate&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id="collecting-with-telegraf"&gt;Collecting with Telegraf&lt;/h4&gt;

&lt;p&gt;Whilst both scripts  &lt;em&gt;could&lt;/em&gt;  write directly into my InfluxDB instance, neither script implements a write buffer, so if my Influx instance were to be unavailable (whether because it’s down or because the tank Pi had Wifi problems) those writes would be lost. Probably not the end of the world, but also far from ideal.&lt;/p&gt;

&lt;p&gt;Adding a buffer to the Python script would be easy, the InfluxDB Python Client library  &lt;a href="https://github.com/influxdata/influxdb-client-python#batching"&gt;supports batched writes&lt;/a&gt;, however adding it to the BASH script wouldn’t be nearly so straightforward. In either case, if the script had to be restarted, buffered writes would be lost — not ideal considering how little effort I’ve really put into writing them.&lt;/p&gt;

&lt;p&gt;So, I decided to have the scripts instead write into Telegraf’s  &lt;a href="https://github.com/influxdata/telegraf/tree/master/plugins/inputs/influxdb_listener"&gt;InfluxDB_Listener Input Plugin&lt;/a&gt;  so that it could provide a buffer against downstream failures. As an additional positive benefit, it also allows collection of system metrics to help monitor the Pi itself.&lt;/p&gt;

&lt;p&gt;I’m running  &lt;code class="language-bash"&gt;Telegraf&lt;/code&gt;  in docker (because I’d originally intended to dockerize the collection scripts, but have since decided they’re too simple to really be worth the effort).&lt;/p&gt;

&lt;p&gt;Telegraf is launched from the following &lt;code class="language-bash"&gt;docker-compose.yml&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;version: '3.1'

services:
    telegraf:
         image: telegraf
         restart: always
         user: telegraf:995
         container_name: telegraf
         network_mode: "host"
         environment:
            HOST_ETC: /hostfs/etc
            HOST_PROC: /hostfs/proc
            HOST_SYS: /hostfs/sys
            HOST_VAR: /hostfs/var
            HOST_RUN: /hostfs/run
            HOST_MOUNT_PREFIX: /hostfs
         ports:
            - 8086:8086
         volumes:
            - /home/pi/tank-monitoring-scripts/config/telegraf/telegraf.conf:/etc/telegraf/telegraf.conf
            - /var/run/docker.sock:/var/run/docker.sock
            - /:/hostfs:ro&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Telegraf’s config file contains the following:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;[agent]
  interval = "1m"
  round_interval = true
  metric_batch_size = 1000
  metric_buffer_limit = 10000
  flush_interval = "10s"
  flush_jitter = "0s"
  debug = false
  quiet = true
  hostname = "tankmonitor"
  omit_hostname = false

[[inputs.cpu]]
[[inputs.diskio]]
[[inputs.mem]]
[[inputs.net]]
[[inputs.processes]]
[[inputs.swap]]
[[inputs.system]]

[[inputs.disk]]
  ignore_fs = ["tmpfs", "devtmpfs", "devfs", "overlay", "aufs", "squashfs"]

# Add the listener  
[[inputs.influxdb_listener]]
  service_address = "127.0.0.1:8086"

# Send it all to the local InfluxDB instance
[[outputs.influxdb]]
  urls = ["http://192.168.3.84:8086"]
  database = "telegraf"&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Starting Telegraf is as simple as:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;docker-compose up -d&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The monitoring scripts can then simply write line protocol into  &lt;code class="language-bash"&gt;http://127.0.0.1:8086/write&lt;/code&gt;  as if they were talking to an InfluxDB 1.x instance.&lt;/p&gt;

&lt;h4 id="hardware-installation"&gt;Hardware installation&lt;/h4&gt;

&lt;p&gt;With the sensors up and working, it was time to connect it all to the tank.&lt;/p&gt;

&lt;p&gt;The water flow meter needed to be connected into the filter circuit. It needs to be plumbed into the  &lt;strong&gt;return&lt;/strong&gt;  flow pipe and not the  &lt;strong&gt;feed&lt;/strong&gt;: not only might it get gummed up by unfiltered crud, but we also want to know that the water is actually getting back to the tank rather than (say) leaking from the cannister.&lt;/p&gt;

&lt;p&gt;The flow-meter has G1/2 screw threads, so I used a pair of 16mm barb to G1/2 connectors.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/13pliDjZddzu1cYpTTjeA2/61d5682a127fd12b741cd2da53c7eb17/g12_barb_small.jpg" alt="g12 barb small" /&gt;&lt;/p&gt;

&lt;p&gt;I ran into a couple of small issues here:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The connectors arrived without washers.&lt;/li&gt;
  &lt;li&gt;The tubing for my filter, despite being 20mm externally has a 15mm inner bore (not the more standard 16mm).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first was a matter of either patience (ordering some o-rings) or sealant (which I had on hand), the second simply required a bit of elbow grease.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/6ZSKW9owQesZi56CeoWbhF/d4b0a2b9932227b5fe2306c95cbceb3d/water_flow_sensor.jpg" alt="water flow sensor" /&gt;&lt;/p&gt;

&lt;p&gt;Although seemingly well connected and water-tight, it’s still a join and so gets to live in a bucket until I learn to trust it (or, at least, until I next need the bucket).&lt;/p&gt;

&lt;p&gt;Next up was mounting the Pi itself: I screwed the base of its case to the tank’s cabinet, making sure it was positioned under an overhang (to help protect against wayward drips).&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/6549MO6KSzqFiT8eML7L03/13a10ced34e7d63cb909bb1ce2f3fcbc/pi_installed.jpg" alt="pi installed" /&gt;&lt;/p&gt;

&lt;p&gt;Finally, the temperature sensor needed to be put into place. I used a wire tie (like those you might use on a freezer bag) to attach it to suckered hook and then installed it in the tank.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/3y0nGJv46dzl8hvU7WVf1P/97dcc28630e915c6b75b57b46eec83d8/temperature_sensor.jpg" alt="temperature sensor" /&gt;&lt;/p&gt;

&lt;p&gt;I’ve since ordered some suckers with clips intended to carry aquarium air lines — the clip is the perfect diameter for the sensor and looks much tidier than a wire tie.&lt;/p&gt;

&lt;p&gt;With everything connected and in place, I plugged the Pi’s power supply in and within a minute, readings started arriving in InfluxDB.&lt;/p&gt;

&lt;h4 id="graphing"&gt;Graphing&lt;/h4&gt;

&lt;p&gt;With readings coming into the database, the next thing to do was to build a dashboard in Grafana to allow visualization of that data:&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/3ge56X53KXXx245lr2KRUy/fcc8f4469e7e933e339b14771b88704e/grafana_dashboard.png" alt="grafana dashboard" /&gt;&lt;/p&gt;

&lt;p&gt;The gap at the top right will contain PH information once the probe arrives.&lt;/p&gt;

&lt;p&gt;There’s nothing particularly special about the underlying queries, the state indicators at the top simply query consumption and assess whether it’s over a threshold.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-javascript"&gt;from(bucket: "Systemstats/autogen")
  |&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |&amp;gt; filter(fn: (r) =&amp;gt; r._measurement == "power_watts")
  |&amp;gt; filter(fn: (r) =&amp;gt; r._field == "consumption")
  |&amp;gt; filter(fn: (r) =&amp;gt; r.host == "aquarium-filter-pump")
  |&amp;gt; aggregateWindow(every: v.windowPeriod, fn: last)
  |&amp;gt; map(fn: (r) =&amp;gt; ({Device: r.host, 
     _state: if r._value &amp;gt; 9 then
        1
      else
        0
  }))&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The pump only uses 10 watts when active, so we check if it’s consuming more than 9 to confirm that it’s actually turned on.&lt;/p&gt;

&lt;p&gt;The flow rate indicator just pulls the mean over time:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-javascript"&gt;from(bucket: "telegraf")
  |&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |&amp;gt; filter(fn: (r) =&amp;gt; r._measurement == "aquarium")
  |&amp;gt; filter(fn: (r) =&amp;gt; r._field == "flow_mean")
  |&amp;gt; aggregateWindow(every: v.windowPeriod, fn: last)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;There’s a graph further down the page which shows how the flow-rate bounds vary over time.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/49PZefIbn9JjZo6PPUh6XW/09debbfcb90f3636e69bd88aa81b0e7d/flow_rates.png" alt="flow rates" /&gt;&lt;/p&gt;

&lt;p&gt;The relative consistency of the differences between them is  &lt;em&gt;probably&lt;/em&gt;  reflective of a sampling issue rather than real fluctuations in the pump rate. What we really care about, though, is that water is being pumped at a relatively consistent rate.&lt;/p&gt;

&lt;h4 id="alerting"&gt;Alerting&lt;/h4&gt;

&lt;p&gt;I wanted to play around with Grafana’s inbuilt alerting, so after adding SMTP details to  &lt;code class="language-bash"&gt;grafana.ini&lt;/code&gt;  I added myself as a  &lt;a href="https://grafana.com/docs/grafana/latest/alerting/manage-notifications/create-contact-point/"&gt;Contact Point&lt;/a&gt;  and then proceeded to create a simple alert.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/46SgMWsMbalofLp6aircWI/d5ff11d847680d11436fadf4b4f7ae61/grafana_alert_config.png" alt="grafana alert config" /&gt;&lt;/p&gt;

&lt;p&gt;The query used for the alert is:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-javascript"&gt;from(bucket: "telegraf")
  |&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |&amp;gt; filter(fn: (r) =&amp;gt; r._measurement == "aquarium")
  |&amp;gt; filter(fn: (r) =&amp;gt; r._field == "water_temperature")
  |&amp;gt; mean()&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The alert simply calculates the average reported water temperature over the last 5 minutes and sends an notification if it’s not between 22 and 26 (many tropical fish will actually tolerate 20-28C, but these tighter thresholds allow time to respond before the fish get too stressed).&lt;/p&gt;

&lt;p&gt;When the alert fires, an email is sent.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/2mCR8VxW7WcQLoROGuLy8h/4630a6db29ea5405729506e105626e59/grafana_alert_mail.png" alt="grafana alert mail" /&gt;&lt;/p&gt;

&lt;p&gt;If there’s an interruption in data being received (for example because someone accidentally bought a counterfeit temperature probe which has dropped off the bus…) an alert mail goes out to notify of the deadman.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/6tP04FK6ZByn2zS9glxcvx/d0831f7553ecd2cca516357d6f718fdf/grafana_deadman.png" alt="grafana deadman" /&gt;&lt;/p&gt;

&lt;p&gt;Of course, it’s not just temperature that we want to alert on: because we collect filter flow rate we can alert if it drops below a threshold (indicating either pump issues or a clog in the filter or its intake).&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/4ySGA8ZowthESP30f8NUgC/640b5eb82d2023b4972b24ee23bcbccf/flow_rate_alert.png" alt="flow rate alert" /&gt;&lt;/p&gt;

&lt;p&gt;I also have a separate alert with an upper threshold — if the tank suddenly starts pumping  &lt;em&gt;very&lt;/em&gt;  quickly it might indicate that we’re pumping water all over the floor somewhere beyond the meter.&lt;/p&gt;

&lt;p&gt;There are also alerts based on power consumption to check that scheduled tasks (like the daily surface skimmer run) actually happened. These are driven by (more or less) the same query used for the status cells in the dashboards.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/5uvOGkVALUfNKtGHl8x4gS/0e4a690ea3573d2bc3028cd5741a84ea/grafana_scheduled_activity_alerts.png" alt="grafana scheduled activity alerts" /&gt;&lt;/p&gt;

&lt;p&gt;Once I have the bits for it, I’ll create a PH level based alert too.&lt;/p&gt;

&lt;h4 id="conclusion"&gt;Conclusion&lt;/h4&gt;

&lt;p&gt;It’s fairly straightforward to automate collection of metrics from an aquarium.&lt;/p&gt;

&lt;p&gt;Most of the components involved are also relatively inexpensive with the most expensive sensor being the water-flow meter (at £10). Obviously, the project’s cost would have been higher if I hadn’t already had a raspberry pi lying around, although it should be possible to do all of this with a cheap  &lt;a href="https://www.raspberrypi.com/products/raspberry-pi-zero/"&gt;Raspberry Pi Zero&lt;/a&gt;  or an  &lt;a href="https://store.arduino.cc/products/arduino-nano-rp2040-connect-with-headers"&gt;Arduino Nano&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As with anything involving water and piping, there is a little risk involved in fitting a water-flow meter: any join carries some risk of leakage. If you  &lt;em&gt;really&lt;/em&gt;  didn’t mind the look, that could be mitigated by fitting the meter inside the tank (obviously above the waterline) so that any leak remained inside the tank.&lt;/p&gt;

&lt;p&gt;Now that I’ve got the base system setup, it’s not just the forthcoming PH sensor that can be fitted, there are various other probes on the market which could help monitor water quality — for example a Total Dissolved Solids (TDS) sensor could probably be used to predict the levels of ammonium nitrate present in the water column.&lt;/p&gt;

&lt;p&gt;The real challenge, as I found with my temperature sensor, is in finding sensor hardware that’s genuine, reliable, and affordable.&lt;/p&gt;

&lt;p&gt;As well as providing automated monitoring/alerting, getting this all set up also helps to kill a little bit of time whilst waiting for the tank to cycle.&lt;/p&gt;
</description>
      <pubDate>Mon, 10 Apr 2023 07:00:00 +0000</pubDate>
      <link>https://www.influxdata.com/blog/monitoring-aquarium-influxdb-grafana/</link>
      <guid isPermaLink="true">https://www.influxdata.com/blog/monitoring-aquarium-influxdb-grafana/</guid>
      <category>Product</category>
      <author>Ben Tasker (InfluxData)</author>
    </item>
  </channel>
</rss>
