<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[HVAC Fitter Dev Notes]]></title><description><![CDATA[HVAC Fitter Dev Notes]]></description><link>https://hvacfitter.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Nov 2026 10:07:29 GMT</lastBuildDate><atom:link href="https://hvacfitter.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[AC Running but Not Cooling? Turn the Checks into a Fault Tree]]></title><description><![CDATA[A plain-English decision tree (with a tiny Python sketch) for the most common summer HVAC call]]></description><link>https://hvacfitter.hashnode.dev/ac-running-but-not-cooling-turn-the-checks-into-a-fault-tree</link><guid isPermaLink="true">https://hvacfitter.hashnode.dev/ac-running-but-not-cooling-turn-the-checks-into-a-fault-tree</guid><category><![CDATA[HVAC]]></category><category><![CDATA[Python]]></category><category><![CDATA[Home Improvement]]></category><category><![CDATA[troubleshooting]]></category><dc:creator><![CDATA[Lindsay Vangen]]></dc:creator><pubDate>Mon, 02 Nov 2026 17:16:43 GMT</pubDate><content:encoded><![CDATA[<p>When air comes out of the vents but the house stays warm, the indoor blower is working and the cooling cycle is not. HVAC Fitter's guide <a href="https://hvacfitter.com/guides/ac-running-but-not-cooling">AC Running But Not Cooling</a> puts the causes into three buckets: restricted indoor airflow, an outdoor unit that can't shed heat, or a part that failed to start the outdoor fan and compressor.</p>
<p>That order is useful because each step is cheap to check before the next one. Here it is as a fault tree.</p>
<p><strong>Safety first.</strong> Power off at the breaker and pull the outdoor disconnect before opening any panel, then test that it's off. A capacitor can hold a charge after power is off. Never bypass a float or safety switch. Refrigerant work needs an EPA Section 608 certified technician.</p>
<h3>1. Thermostat</h3>
<ul>
<li>Fan switch set to ON keeps the blower running even when nothing is cooling. Set it to AUTO.</li>
<li>Mode must be COOL, with the setpoint below room temperature.</li>
</ul>
<h3>2. Air filter</h3>
<p>A clogged filter chokes airflow over the evaporator coil, and the coil can freeze into a block of ice. Air still blows, but it isn't cold. Check a 1 inch filter monthly and plan to change it every 1 to 3 months, arrow toward the furnace or air handler. Sizing help: <a href="https://hvacfitter.com/guides/how-to-size-an-air-filter">how to size an air filter</a>.</p>
<h3>3. Outdoor unit</h3>
<p>Go outside while the system is calling for cooling.</p>
<ul>
<li>Fan silent: it may have no power. See <a href="https://hvacfitter.com/guides/outdoor-unit-not-turning-on">outdoor unit not turning on</a>.</li>
<li>Fan hums but won't spin, or the fan runs and the compressor doesn't: see <a href="https://hvacfitter.com/guides/ac-fan-running-compressor-not">AC fan running, compressor not</a>. A weak run capacitor is a common cause; <a href="https://hvacfitter.com/guides/which-capacitor-do-i-need">which capacitor do I need</a> explains how to match one by its ratings.</li>
</ul>
<h3>4. Still warm?</h3>
<p>Refrigerant or coil problems go to a licensed technician.</p>
<h3>The same tree as code</h3>
<pre><code class="language-python">def ac_not_cooling(t):
    if t["fan_switch"] == "ON" or t["mode"] != "COOL":
        return "Fix thermostat settings"
    if t["filter_dirty"]:
        return "Replace filter; let a frozen coil thaw before restarting"
    if not t["outdoor_fan_spins"]:
        return "Check power to the outdoor unit, then the capacitor"
    if not t["compressor_runs"]:
        return "Compressor not starting: capacitor/contactor checks"
    return "Call a technician (refrigerant/coil)"
</code></pre>
<p>Encoding it this way makes the order explicit: you never reach for a part until the free checks are done.</p>
<p>More repair guides: <a href="https://hvacfitter.com/guides">hvacfitter.com/guides</a></p>
<p><em>HVAC Fitter sells replacement HVAC parts. This post is general information; when in doubt, call a licensed technician.</em></p>
]]></content:encoded></item><item><title><![CDATA[16x25x1 Isn't 16 by 25: Parsing and Validating HVAC Air Filter Sizes in Python]]></title><description><![CDATA[Nominal vs actual sizes, order-insensitive matching and depth checks.]]></description><link>https://hvacfitter.hashnode.dev/16x25x1-isn-t-16-by-25-parsing-and-validating-hvac-air-filter-sizes-in-python</link><guid isPermaLink="true">https://hvacfitter.hashnode.dev/16x25x1-isn-t-16-by-25-parsing-and-validating-hvac-air-filter-sizes-in-python</guid><category><![CDATA[Python]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[data validation]]></category><dc:creator><![CDATA[Lindsay Vangen]]></dc:creator><pubDate>Wed, 28 Oct 2026 19:12:34 GMT</pubDate><content:encoded><![CDATA[<p><em>This is the HVAC Fitter team's first post on Hashnode. It first ran on DEV; we're sharing it here for Hashnode's Python readers. hvacfitter.com sells HVAC replacement parts, and this is the small sizing model we use to explain furnace filters.</em></p>
<p>Measure a common furnace filter with a tape and you'll get about 15.5 by 24.5 inches, three-quarters of an inch thick. Nobody sells that size. The label on the frame says <strong>16x25x1</strong>.</p>
<p>That little gap between the label and the tape is a nice, small data-modeling problem. If you build anything that matches parts (a store search, an inventory sheet, a home-maintenance app), you'll hit it. Here's a tiny Python model of the rules.</p>
<p><em>Written by the HVAC Fitter team (hvacfitter.com). General information, not a replacement for your system's manual.</em></p>
<h2>The rules, in plain English</h2>
<p>These come from our guide on <a href="https://hvacfitter.com/guides/how-to-size-an-air-filter">how to size an air filter</a>:</p>
<ol>
<li>The size is printed on the frame of the old filter, like <code>16x25x1</code>: length, width and depth, in inches.</li>
<li><strong>The order of the first two numbers doesn't matter.</strong> A 16x25 and a 25x16 are the same filter.</li>
<li>The label is the <strong>nominal</strong> size, rounded up. A 16x25x1 really measures about 15.5 x 24.5 x 0.75 in, so it can slide into the slot.</li>
<li>No label? Measure it and <strong>round each number up</strong> to the next whole inch (or the next half inch for odd sizes).</li>
<li><strong>Depth must match the slot.</strong> Filters come 1, 2, 4 and 5 in deep. Never force a thicker one in, and don't use a thin one in a deep slot, because air goes around it.</li>
</ol>
<p>Rules 2, 3 and 5 are exactly the kind of thing a naive string match gets wrong.</p>
<h2>Parsing a size string</h2>
<p>People type sizes every way: <code>16x25x1</code>, <code>25 X 16 x 1</code>, <code>16*25*1</code>. Normalize them into one canonical form, with the two face numbers sorted so order stops mattering.</p>
<pre><code class="language-python">import math
import re
from dataclasses import dataclass

VALID_DEPTHS = {1, 2, 4, 5}  # inches; the common furnace filter depths
SIZE_RE = re.compile(
    r"^\s*(\d+(?:\.\d+)?)\s*[x×X*]\s*(\d+(?:\.\d+)?)\s*[x×X*]\s*(\d+(?:\.\d+)?)\s*$"
)


@dataclass(frozen=True)
class FilterSize:
    a: float      # the smaller face dimension
    b: float      # the larger face dimension
    depth: float

    def __str__(self):
        f = lambda n: f"{n:g}"
        return f"{f(self.a)}x{f(self.b)}x{f(self.depth)}"


def parse_size(text: str) -&gt; FilterSize:
    """Parse '16x25x1', '25 X 16 x 1' or '16*25*1' into one canonical size."""
    m = SIZE_RE.match(text)
    if not m:
        raise ValueError(f"not a filter size: {text!r}")
    w, l, d = (float(g) for g in m.groups())
    a, b = sorted((w, l))  # 16x25 and 25x16 are the same filter
    return FilterSize(a, b, d)
</code></pre>
<p>Sorting the face dimensions is the whole trick for rule 2. Now <code>parse_size("25 X 16 x 1")</code> and <code>parse_size("16x25x1")</code> are equal, because frozen dataclasses compare by value.</p>
<h2>From a tape measure to a label size</h2>
<p>Rule 3 and 4 in code: round each measured number up.</p>
<pre><code class="language-python">
def nominal_from_measured(w: float, l: float, d: float) -&gt; FilterSize:
    """Measured frame (e.g. 15.5 x 24.5 x 0.75) -&gt; the size printed on the label."""
    up = lambda n: float(math.ceil(n - 1e-9))
    return FilterSize(*sorted((up(w), up(l))), up(d))
</code></pre>
<p>The <code>- 1e-9</code> keeps a measured <code>16.0</code> from turning into 17 when float math leaves it at <code>16.0000000001</code>. This version only rounds to whole inches. If you need the "next half inch for odd sizes" case, round to <code>math.ceil(n * 2) / 2</code> and check the result against a list of sizes you actually stock.</p>
<h2>Checking a swap</h2>
<p>Most real bugs show up when someone swaps one filter for another. Compare the face sizes and the depth separately, so the error message says <em>what</em> is wrong.</p>
<pre><code class="language-python">
def check_swap(old: str, new: str) -&gt; list[str]:
    """Return problems with replacing filter `old` by filter `new` (empty = fine)."""
    o, n = parse_size(old), parse_size(new)
    problems = []
    if (o.a, o.b) != (n.a, n.b):
        problems.append(f"face size differs: {o.a:g}x{o.b:g} vs {n.a:g}x{n.b:g}")
    if o.depth != n.depth:
        why = ("too thick, it won't fit the slot" if n.depth &gt; o.depth
               else "too thin, air will go around it")
        problems.append(f"depth {n.depth:g} in vs slot {o.depth:g} in: {why}")
    if n.depth not in VALID_DEPTHS:
        problems.append(f"unusual depth {n.depth:g} in; check the slot")
    return problems


if __name__ == "__main__":
    assert str(parse_size("25 X 16 x 1")) == "16x25x1"
    assert str(nominal_from_measured(15.5, 24.5, 0.75)) == "16x25x1"
    assert check_swap("16x25x1", "25x16x1") == []
    print(check_swap("16x25x4", "16x25x1"))
    print(check_swap("16x25x1", "16x20x1"))
</code></pre>
<p>Running it:</p>
<pre><code class="language-text">['depth 1 in vs slot 4 in: too thin, air will go around it']
['face size differs: 16x25 vs 16x20']
</code></pre>
<p>The first case is an easy mistake: the slot takes a 4-inch filter, someone grabs a 1-inch one because the face size matches. The face check passes, the depth check catches it.</p>
<h2>Why not just store the string?</h2>
<p>Because three different strings mean the same filter, and one string (<code>16x25x1</code>) means a frame that measures something else. If your search index stores the raw label text, "25x16x1" returns nothing, and a customer with a tape measure never finds a match. Store the canonical form, and search on it.</p>
<p>The same idea works for part numbers. A 4-inch filter marked CF100A1009 has a set of replacements, which is a lookup table problem, not a parsing problem. See the <a href="https://hvacfitter.com/replacement/cf100a1009">CF100A1009 replacement page</a> for one example.</p>
<h2>A few edge cases worth a test</h2>
<ul>
<li><code>16 x 25 x 1"</code> with an inch mark: strip <code>"</code> and <code>in</code> before matching.</li>
<li>Metric labels: some filters print both. Keep the inch size as the key.</li>
<li>MERV rating: it isn't part of the size. Many homes use MERV 8 to 11, and a higher MERV can slow airflow on some systems, so keep it as a separate field.</li>
</ul>
<p>That's the whole model: one regex, one sort, one round-up and two comparisons. If you want to see it used on real stock, our <a href="https://hvacfitter.com/parts/air-filters">air filter listings</a> are filtered by the numbers printed on the old frame.</p>
<p><em>Turn the system off before you swap a filter, and point the airflow arrow toward the furnace or air handler.</em></p>
]]></content:encoded></item></channel></rss>