Skip to content

About

Home Assistant Custom Component - Generic Water Heater

Resources

Stars

2 stars

Watchers

1 watching

Forks

Repository files navigation

Home Assistant Custom Component - Generic Water Heater

The Generic Water Heater integration creates a virtual water heater entity in Home Assistant. It controls a switch using a temperature sensor, so you can manage domestic hot water with water heater controls in the UI and in automations.

Features

  • Thermostat-style control with configurable cold and hot tolerances.
  • Smart Eco policy controlled by a dedicated select entity (Smart Eco Mode). Heating is allowed by named inputs (a surplus entity, a utility entity with its own Eco: Use Utility switch) and an optional template.
  • Surplus boost: while surplus is active and Boost: Use PV Excess is on, heat toward a higher target, storing spare power as heat without ever heating past the normal target on paid power.
  • Smart Eco State sensor that exposes meaningful policy states (Off, Idle, Heating in eco, Blocked by eco condition, timed pause states, and override states), including two bypasses that put the policy back by themselves once the tank is satisfied.
  • Optional extra sensor that tracks the highest recorded temperature in the last 7 days, useful for legionella prevention workflows.
  • Manual override handling for both water heater entity actions and direct underlying switch toggles. A switch returning from unavailable is not treated as a manual action: it is a device reconnecting and reporting the state it already had. Without that distinction a brief network dropout could be read as "someone flipped it on", promoting electric to performance and running the element to the tank's own mechanical cutout. A genuine flip still arrives as a real off -> on transition and behaves exactly as before.
  • Always ON temporary override behavior for manual underlying switch changes, with an override state, an absolute resume time and persistent notifications.
  • Minimum on and off durations to avoid rapid switching.
  • Fleet load coordination across every instance, so several heaters cannot step onto a shared inverter or generator at the same moment.
  • Failsafe shutdown when the temperature sensor becomes unavailable.
  • Automatic device linking to the same device as the controlled switch when possible.

Heating Logic

The integration uses hysteresis to avoid short-cycling:

  • Heat turns on when the current temperature is less than or equal to target_temperature - cold_tolerance.
  • Heat turns off when the current temperature is greater than or equal to target_temperature + hot_tolerance.

Example with target 50°C, cold tolerance 0.5°C, and hot tolerance 0.5°C:

  • Heater turns on at 49.5°C or lower.
  • Heater turns off at 50.5°C or higher.

While the surplus boost is active (below), the boost target takes the place of the target, and the heater turns off at exactly the boost target: the hot tolerance is not added.

Operation behavior:

  • off: heater stays off.
  • electric: follows the threshold logic above.
  • performance: keeps the element on regardless of the target temperature, until the tank's own mechanical thermostat stops it. It never reports idle. Not the same as the surplus boost below.
  • Smart Eco Mode: applies policy behavior described below.

Sensor glitch filtering

A single reading more than 10 °C away from the previous one is held back until the next reading decides it. If the temperature comes back, the outlier is dropped. If it stays, the reading is accepted with its original timestamp, and so is any reading not contradicted within 60 seconds. No tank changes that much between two reports, but some combined relay and temperature devices report one wild value as their relay switches. Before this filter, one such reading could switch the element, trigger Hot Water In Use, enter the 7-day maximum, or discard a disinfection hold that was nearly complete. unavailable and unknown are never delayed, so the failsafe still turns the element off at once.

Installation

  1. Open HACS in Home Assistant.
  2. Add this repository as a Custom Repository for Integrations.
  3. Search for Generic Water Heater and install it.
  4. Restart Home Assistant.

Configuration

This integration is configured from the Home Assistant UI.

  1. Go to Settings > Devices & Services.
  2. Click Add Integration.
  3. Search for Generic Water Heater.
  4. Select the heater switch, temperature sensor, and your preferred operating parameters.

Configuration Options

The config flow groups these into collapsible sections — temperatures, cycle protection, Smart Eco, surplus boost, fleet, legionella, hot water in use, metering, advanced — with the name, switch and sensor left at the top. The grouping is presentation only: options are stored flat under the keys below, so YAML, diagnostics and anything reading the entry see no sections.

Option Type Default Description
heater_switch entity_id Required The switch entity that controls the heater.
temperature_sensor entity_id Required The sensor that reports the water temperature.
target_temperature_step float 1.0 The step used by the target temperature control in the UI.
cold_tolerance float 0.0 Difference below target temperature that allows heating to turn on.
hot_tolerance float 0.0 Difference above target temperature that forces heating to turn off.
min_temp float 15.0 Minimum selectable target temperature.
max_temp float 80.0 Maximum selectable target temperature.
min_on_duration duration 0 seconds Minimum time the heater must stay on before it can be turned off.
min_off_duration duration 120 seconds Minimum time the heater must stay off before it can be turned on. A configured 0 means zero; the default applies only when the option is absent.
surplus_entity entity_id empty Says there is spare power to use (for example a PV-excess binary sensor). While active, heating is allowed, and the surplus boost may heat past the normal target.
surplus_state string on The state of surplus_entity that means active. Compared ignoring case.
utility_entity entity_id empty Says the utility supply is carrying the house. While active and the heater's Eco: Use Utility switch is on, heating is allowed up to the normal target. Adds that switch.
utility_state string on The state of utility_entity that means active, for example Utility for an enum power-source sensor. Compared ignoring case.
eco_mode_template_condition template empty Optional extra term: a boolean template that also allows heating when true. If this and both entities are empty, Smart Eco Mode entities are not created and no Smart Eco policy is applied.
surplus_boost_target_temperature number empty The boost target, and the boost's peak: the boost stops at exactly this temperature. Needs surplus_entity. Not limited by max_temp, which only bounds the target dial. Adds the Boost: Use PV Excess switch. Empty = no boost.
enable_legionella_sensor boolean false Adds the Legionella thermal-conditions sensor described below.
legionella_interval_days number 7 How long a completed disinfection cycle counts for before the sensor reports Elevated (and twice that before High).
fleet_stagger_seconds number 60 Minimum spacing between this heater switching on and any other instance switching on. 0 disables staggering. The largest value set on any instance applies to the whole fleet.
smart_eco_manual_off_resume_hours number (slider) 6 Auto-resume/override duration in hours (range: 1 to 48). Used by Auto Resume after Delay and Always ON temporary overrides.
enable_max_temp_history_sensor boolean false Adds a sensor to the same device that exposes the highest recorded temperature in the last 7 days (useful in anti-legionella monitoring workflows).
enable_hot_water_in_use boolean false Adds the Hot Water In Use binary sensor described below.
water_in_use_entity entity_id empty Optional whole-house flow or pump signal. Corroborates the Hot Water In Use sensor on ambiguous fall rates; it does not gate it, and leaving it empty only costs the middle tier.
power_sensor entity_id empty Optional meter on the element's circuit. Not used for control: published as the heater's power_sensor attribute, so another integration can follow the element's live draw from the heater (a meter swap is then edited here once). Setting it also adds a Power (mirror) sensor to the heater's device (entity id ending _power_mirror, so nothing matching _power picks it up) — a mirror of the meter for the UI, with no state_class (so it never feeds statistics or the Energy dashboard; keep using the meter there) and a mirror_of attribute naming the meter. A mirror cannot be chosen as the meter. When the meter is on a different device than the heater switch (a relay fed by a separate metering breaker), the meter's device is also listed under the heater device's Linked devices, beside the switch's (Home Assistant 2026.9 or newer). Alongside it the heater publishes eco_gated: true only while its heat depends on the Smart Eco condition (policy enforcing, the condition true and the startup grace over, electric, no disinfection, not shed), i.e. it would stop if the condition went false — or while it heats only because of the surplus boost (the tank past its normal target plus hot tolerance), which stops when the surplus leaves.

Fleet Load Coordination

Several instances driven by one shared condition -- the same Smart Eco surplus entity, for example -- all react in the same Home Assistant event-loop pass. Without coordination each one commands its switch within milliseconds of its siblings, and a few kW of resistive load arrives as a single step. On an inverter or generator sized close to the house load, that step is what trips the supply.

All instances share one coordinator, and it does one thing: stagger (fleet_stagger_seconds, default 60 s) -- no instance may switch on within N seconds of any other instance's switch-on. The largest value set on any instance applies to the whole fleet, so the resolved setting does not depend on which entry loaded first.

A deferred heater is never skipped. It retries on the same cooldown timer that min_off_duration uses, and the delay it is given is exactly the time remaining in the window.

The window opens when an element starts drawing, however that happened -- commanded here, flipped by a person, or found already on after a restart. An ON this integration did not command loads the same inverter, so a sibling still has to be spaced against it.

The coordinator can only ever delay its own commands -- it never refuses a human. Flipping the physical switch works exactly as before: the override is detected and Smart Eco steps back. A heater that is already drawing is never delayed by its own control passes either, and re-asking does not push the window forward -- otherwise a switch that stopped reporting while still drawing would starve its sibling indefinitely.

The window also survives the reload an options save triggers. That reload is precisely the moment two elements could otherwise come on together, and it removes the entity, so the anchor is kept outside it.

Set fleet_stagger_seconds to 0 on every instance to restore the previous uncoordinated behaviour.

Current fleet state is exposed on each water heater entity as the fleet_stagger_seconds and fleet_hold_reason attributes. fleet_hold_reason names exactly why a heater is currently waiting.

Power balancing is deliberately not done here (changed in 2.0.0)

Until 2.0.0 this integration also ran nameplate-watt admission control: a per-instance nominal_power_w, a shared fleet_power_budget_w, a committed-watts pool reconciled against observed switch state, a grace period so an entry reload could not hand a sibling watts that were still energised, and priority arbitration that ranked contending heaters by temperature deficit.

Both options and all of that machinery are gone. Deciding how much load the supply can carry needs visibility of the whole house, which a water heater integration does not have. A dedicated load balancer does, and it can carry its own per-appliance nameplates -- including the zero-latency part that justified nameplate accounting here in the first place, since it can veto a turn-on before any power sensor could react. Two systems balancing one supply from two independently-maintained sets of nameplates is worse than one: they disagree silently, and the one that cannot see the rest of the house is the one that should give way.

Staggering stayed because it is not the same problem. It is about the instant at which two elements step on together -- sequencing, not budgeting -- and it needs no nameplate to do its job.

For coordinated shedding driven by a real budget, see the load-shedding services below: an external balancer calls generic_water_heater.shed and .release, and that is the supported way to have something else decide what this heater may draw.

Upgrading: if you had a budget configured, nominal_power_w and fleet_power_budget_w stop having any effect and disappear from the options form. Nothing else changes, and the stagger keeps working with the value you already have. Move the budget to a load balancer if you need one.

Load shedding (external load balancer integration)

An external load balancer that protects a shared supply needs to drop this heater without its request being mistaken for a person operating it. Calling water_heater.set_operation_mode for that is indistinguishable from someone using the UI, so it trips the manual-override handling and pauses Smart Eco for hours -- on the shed and again on the restore, leaving the tank heating outside its eco condition.

Two entity services exist for that instead:

Service Effect
generic_water_heater.shed Forces the heater off for load shedding. The operation mode and Smart Eco policy are left exactly as they are -- no pause, no countdown, no notification.
generic_water_heater.release Releases the shed and resumes whatever was configured. No-op if not shed.

Behaviour while shed:

  • It outranks everything, including Smart Eco Always ON. Shedding protects the electrical supply; it is not a user preference. Smart Eco's "eco allows heating, restore the heating mode" path cannot undo a shed.
  • The min_on_duration hold is bypassed. That minimum exists to stop thermostat noise short-cycling the relay, and letting it delay a supply-protection action would hand the balancer a shed that has silently not happened yet. A release does still respect min_off_duration, so the element may take up to that long to come back.
  • A person asking for heat wins -- turn_on, setting a heating operation mode, or flipping the physical switch all clear the shed. A request to turn the heater off does not clear it; it has nothing to win, and clearing on an ignored OFF could switch the element back on.
  • The heater reports load_shed: true and a Smart Eco state of Shed by load balancer.

Smart Eco Mode

Smart Eco Mode is a policy layer, not a water heater operation mode.

When any eco input is configured (a surplus entity, a utility entity or a template), the integration exposes:

  • Select: Smart Eco Mode
  • Sensor: Smart Eco State
  • Switch: Eco: Use Utility, when a utility entity is configured
  • Switch: Boost: Use PV Excess, when a surplus entity and a boost target are configured

The eco condition is true while the surplus entity is active, OR the utility entity is active and Eco: Use Utility is on, OR the template is true. An entity is active when its state equals its configured active state, ignoring case, so an enum sensor works as well as a binary one. An entity that is unavailable or unknown is not active. The two switches are policy choices, not manual control: toggling them never starts a Smart Eco pause. The heater owns and restores their state (its eco_use_utility and surplus_boost_enabled attributes). Use Utility starts on and Boost starts off.

Available Smart Eco Mode options:

  • Off: no Smart Eco policy enforcement.
  • Off until target reached: stands the policy down, then puts back whichever option was in force before, by itself, once the tank is satisfied. Described below.
  • On until next manual control: policy stops when manual control is detected.
  • Auto Resume after Delay: manual control pauses policy and resumes automatically after the configured delay.
  • Always ON: policy is enforced continuously for normal entity-level manual actions. Manual changes on the underlying switch create a temporary timed override, then enforcement resumes automatically.

High-level behavior:

  • If Smart Eco policy is actively enforcing and the eco condition is false, heating is blocked.
  • If Smart Eco policy is actively enforcing and the eco condition is true, heating is allowed.
  • If water heater mode is off while policy allows heating, last heating mode is restored.

Surplus boost

With a boost target set and Boost: Use PV Excess on, a tank in electric heats toward the boost target instead of its normal one while the surplus entity is active. The published target temperature stays the person's own; surplus_boost_active and surplus_boost_target report the boost. When surplus leaves, the tank goes back to its normal target, so heat above that target is only ever drawn from surplus: on the utility or a battery the tank stops at its normal target.

The boost stops at exactly its target: the hot tolerance is not added on top, so the number set is the peak. The cold tolerance still decides when it starts again. The boost target is not limited by max_temp, which bounds only the target dial a person sets every day.

  • Off stays off, performance is unchanged, a load shed outranks the boost, and a disinfection cycle runs its own course.
  • A boost target at or below the normal target does nothing.
  • Short-boost lockout. A surplus detector that judges surplus from the battery sees the heater's own draw: when the element needs more than the spare power, the battery discharges, surplus clears a few minutes later, the element stops, the battery refills and surplus returns. So when surplus leaves within 10 minutes of arriving while the boost had the element on, the boost is not tried again for 30 minutes (surplus_boost_locked_until). Heating to the normal target is never locked out.

Optional template

The template is an extra term, OR'd with the two entities, for conditions they cannot express. A single entity in a given state is better set as the surplus or utility entity: those are what the boost and the Eco: Use Utility switch work from, and a template cannot tell the heater which kind of power it is. Examples of what a template is for:

{{ states('sensor.grid_price_level') in ['low', 'very_low'] }}
{{ states('sensor.pv_generation_w') | float(0) > 3000 }}
{{ is_state('input_boolean.allow_eco_heating', 'on') }}

Whatever allows heating, while the policy is enforcing and the whole eco condition is false, heating is blocked even if the target would otherwise ask for heat.

After a Home Assistant restart

At startup the eco inputs (the surplus and utility entities, or the template's sources) may not have loaded yet, and a sensor with a delay reads a plausible off until the delay passes. Such a condition can't be evaluated yet; it isn't false. So for 2 minutes after Home Assistant finishes starting, a false condition doesn't park the tank. The heater keeps the mode it had before the restart and controls to target as before, with Smart Eco State reading Starting up. A true condition is acted on at once. When the window ends, the condition is enforced as usual. The window is time-bounded, so a source that never comes back can't keep eco bypassed.

In the same window, switch changes are not taken as a person acting on the switch. Devices reconnecting after a restart can replay states, and one replayed on used to start a manual eco pause. The control loop puts the switch back where it should be. A config-entry reload while Home Assistant is running gets no window, because its sources are already loaded.

Heat now, without leaving the policy off

Under Auto Resume after Delay there is a one-shot bypass, and it is the plain ON button (water_heater.turn_on) rather than anything in the Smart Eco Mode select. It behaves differently from every other manual action: instead of the timed pause, Smart Eco pauses with no deadline, the tank heats on electric regardless of the eco condition, and the policy resumes by itself once the tank has reached target and stayed idle for a minute. The Smart Eco State sensor reads Paused until the tank is satisfied while that is in effect.

Use it when you want hot water now and do not want to remember to switch the policy back on. Turning the tank off, or changing the operation mode, takes the timed path instead (smart_eco_manual_off_resume_hours), which reads Paused by manual control until it ends.

This does not work for performance. That mode holds the element on unconditionally, so the appliance's own mechanical thermostat is what eventually stops it and Home Assistant never sees that — hvac_action stays heating, the tank never reads idle, and the pause would never resolve. For a deliberate high-temperature run, use Off until target reached below, which is bounded on time as well and so cannot hang on a tank that never reads idle.

Off until target reached

The same idea reached from the select instead of the ON button, and available under any policy option rather than only Auto Resume after Delay. Choose it when you want hot water now regardless of the eco condition, and do not want to remember to switch the policy back on.

  • It reverts to the option that was in force before, not to a default. Re-selecting it while it is already active does not overwrite that memory.
  • It is bounded, and the bound survives a restart. Past smart_eco_manual_off_resume_hours the policy comes back regardless, with a persistent notification saying the tank never got there. The elapsed time is persisted rather than restarted, because restarting it always extends the bypass — five hours in, a restart would buy another full window, and a crash loop could hold the policy down indefinitely one window at a time. The bound is the point: a dead element or a target the tank cannot reach must not be able to leave Smart Eco disabled, which is the failure this option exists to avoid.
  • "Target reached" means idle, not shed, with no disinfection cycle still outstanding. Three conditions, because hvac_action only reports off when the operation mode is off: a load shed leaves the mode alone and forces just the switch, so it reads idle and is otherwise indistinguishable from a satisfied tank. Without that guard the load balancer would end your bypass for you. performance never reports idle at all, so on that path the time bound is what ends it.
  • It un-parks a tank Smart Eco had already switched off — that tank only, once. Eco parks the operation mode at off and only its own restore branch lifts that, so without this the option would leave the tank sitting off, the opposite of what it is for. But whether an off came from the eco gate or from a person is decided at the moment you choose this mode and never guessed afterwards: a tank you switched off stays off, and turning the tank off later ends the heating rather than being reversed. An unscoped version of this made off unreachable for the whole duration of the bypass.
  • The Smart Eco State sensor reads Off until the tank reaches target while it is in effect.

Always ON temporary override details:

  • Trigger: manual toggle of the underlying heater switch (for example, panel/smart-breaker action).
  • Duration: uses smart_eco_manual_off_resume_hours.
  • State sensor: shows Always ON override (Paused by manual control).
  • Notifications: Home Assistant persistent notifications are created when override starts and when policy resumes.

A timed pause, either kind, keeps the same label until it ends: the label does not count down. When it ends is the heater's smart_eco_resume_at attribute, an absolute time, and a dashboard can count down to that by itself:

type: entities
entities:
  - type: attribute
    entity: water_heater.example
    attribute: smart_eco_resume_at
    name: Smart Eco resumes
    format: relative

Until 4.6.0 the label was a Resuming in HH MM countdown, refreshed every minute, which rewrote the heater's state and the Smart Eco State sensor's, and so added two recorder rows a minute per paused heater for as long as the pause lasted. A template that matched that text should test smart_eco_pause_reason == "manual_off_timer" instead, which is unchanged.

Hot Water In Use

Optional per-tank sensor, created when you enable Hot Water In Use in the config flow. The water-in-use entity is corroboration rather than a prerequisite, so creation does not hang on it.

It answers a question a whole-house flow or pump signal cannot: whether hot water is being drawn from this tank. That signal fires for a cold tap, an irrigation valve, or another appliance entirely, and it cannot tell you which. What identifies one tank is its own temperature falling faster than standing loss can manage.

Two tiers, because measured draws do not all separate cleanly from cooling on rate alone:

Fall rate Verdict
≥ 0.8 °C/min A draw. Roughly 6× the fastest cooling ever measured on these tanks, so nothing else reaches it. Reported on the tank's evidence alone.
0.35 – 0.8 °C/min A draw only if the configured water-in-use entity agrees. Real, but close enough to post-cutout cooling to be arguable on its own.
< 0.35 °C/min Not a draw.

The external entity is therefore corroboration, not a gate. It can break a tie; it can never veto an unambiguous signal from the tank. That matters in two real cases: a draw served from a pressure tank without engaging the pump still registers, and so does one during an outage of the pump sensor. The attribute water_in_use_agrees records what it had to say — true, false, or null when it could not say anything.

Calibrated against six hand-verified draws on a real tank, which averaged 0.365 to 2.53 °C/min, and against the fastest passive cooling on record, 0.13 °C/min in the minutes just after a thermostat cutout. The regression tests replay those traces.

What it deliberately does not do:

  • No special case for the element being on. A draw steep enough to matter outruns the roughly 0.15 °C/min the element adds, so one rule catches the mid-heat cases too — several of the verified draws were mid-heat. The cost is that a draw which merely cancels heating goes undetected. The alternative, treating a flat trace as a draw, rested on a single observation out of fifteen.
  • Short draws are missed. A fall has to last at least 30 seconds and total at least 0.8 °C. A hand wash will not register; a shower will.
  • It cannot distinguish a hot draw from anything else that cools this tank fast. Nothing else plausibly does, but that is an argument from the absence of a mechanism, not a measurement.

Attributes expose the rate, the observed drop, when the draw started and both thresholds, so a disagreement can be argued with rather than guessed at.

Legionella Risk Sensor

An optional sensor reporting how favourable this tank's recent temperature history has been to Legionella growth, and how long since it last reached a disinfection temperature. Enable it with enable_legionella_sensor.

Read this before relying on it. It is a thermal-conditions index computed from one sensor at one height. It is not a measurement of contamination, and only a laboratory culture or PCR test can tell you what is actually in your water. It is structurally blind to:

  • The coldest water in the tank. In a 151 L electric storage tank with the thermostat at 66 °C the measured base was still 43.2 °C. Electric tanks are heated by side-wall immersion elements, so water below the lowest element moves only by weak convection.
  • Sediment, where thermal disinfection fails worst — 50 °C for 4 hours produced no measurable inactivation in water-heater deposits, and 55 °C left culturable cells after 24 hours.
  • Biofilm and amoebae, which shelter Legionella through sub-60 °C cycles.
  • Every outlet downstream. Distal pipework cools to room temperature within ~25 minutes of a draw regardless of tank setpoint.

What it reports

State is Low, Elevated, High, or Unknown, driven by how long it has been since a qualifying cycle relative to legionella_interval_days (Elevated past one interval, High past two). The numbers behind it are attributes, including last_disinfection_at, hours_in_growth_band_7d, equivalent_log10_reduction_7d and max_temperature_7d.

Two more figures are entities of their own, so they keep history and can drive automations directly:

  • Disinfection Due In: days left before the interval lapses, negative once it is overdue. unknown until a cycle has been seen. (Named "Days Until Disinfection" in 3.0.0; existing installs keep their entity ID.)
  • Disinfection Hold Progress: minutes banked toward the current hour at 60 °C. It is live, so a manual high-temperature session can be watched as it accumulates.

Both are diagnostic entities, as are Smart Eco State and Highest Temperature (7 days): they sit in the device page's Diagnostic section, stay usable in automations and dashboards, and are left out of auto-generated dashboards and voice assistants.

Before 3.0.0 these were the attributes days_since_disinfection and hold_progress_minutes.

The model

  • A qualifying cycle is 60 °C held for one continuous hour at the sensor. Both figures are fixed, not configurable. Guidance that prescribes a cycle at all (HSE HSG274 Part 2 cl. 2.25/2.28, ESGLI cl. 3.154) asks for the whole vessel at ≥60 °C for an hour, and below 60 °C a cycle is not a gentler version of the same thing — after a 4 h/55 °C shock with amoebae present, populations rebounded 5 log₁₀ higher than controls within four days. A sub-60 °C option would be a footgun.
  • The hold is hysteretic. A mechanical tank thermostat cycles — slow decay, fast re-heat — so a bare threshold is the wrong tool. A real 200 L tank rippled 59.6–64 °C with dips below 60 °C lasting 19 and 28 minutes while sitting at pasteurisation temperature for five hours; a strict "continuous hour ≥60 °C" would have reported it as never disinfected. So the window opens at 60 °C, stays open while the tank holds above 59 °C, and closes for good below that. Only time genuinely at or above 60 °C counts toward the hour — ripple keeps the window open, it does not earn credit. Sitting at 59.5 °C forever never opens a hold at all.
  • An abandoned hold is discarded, never banked. Partial treatment is the failure mode this is meant to detect, not something to award partial credit for. It takes two consecutive readings below 59 °C to abandon one, though: a single implausible sample — one bad packet from a networked sensor reading one point on a stratified tank — must not throw away a nearly complete hour. That grace is deliberately narrow. A reading more than a degree below the sustain threshold, or a recovery that arrives outside the normal sampling cadence, abandons the hold immediately, and the forgiven interval earns no credit either way.
  • A hold in flight survives a restart. Most of a real hold is banked with the element already off, coasting down from the thermostat cutout — a measured 200 L tank credited its final minutes seven minutes after its switch turned off — so a cycle routinely spans an evening. Progress is persisted and resumed, but only when the gap across the restart is inside the 30-minute observation limit. Longer than that and there is no evidence the tank stayed hot while Home Assistant was down, so the hold is discarded rather than resumed.
  • Growth band is 20–50 °C, deliberately wider than the 20–45 °C regulatory trigger: measured multiplication does not stop until 48.4–50.0 °C, so a tank plateauing at 47 °C is still growing.
  • Disinfection credit uses the published inactivation kinetics (D₅₅ = 3.47 min, z = 5.54 °C), accrued only within the model's validated 51–61 °C range and clamped at the top of it so nothing above 61 °C is extrapolated.
  • Every interval is credited at the lower of its two endpoint temperatures, so a brief spike between two widely spaced samples cannot claim the whole gap. Gaps longer than 30 minutes are treated as unobserved rather than as held temperature, with one exception toward a disinfection hold: a gap of up to 2 hours counts when the heater switch stayed off for all of it and the readings at both ends are at or above 60 °C. With no heat source the tank can only cool, so it stayed at or above 60 °C. This matters for sensors that report only when the reading changes, which go quiet for long stretches while a tank sits idle and hot after a surplus boost.

Running a disinfection cycle

With the risk sensor enabled, each tank also gets a Legionella Disinfection select. Each option says when it runs (Once, or As needed each time the interval lapses) and whether it waits for Smart Eco ((eco): it only heats while the eco condition is met).

Option Behaviour
Off Inert. The default, and what a one-shot returns to when it completes.
Once One cycle now, even if the risk is still Low, without waiting for the eco condition: starting it sets Smart Eco to Off until target reached, which stands the policy down and gives it back by itself. Then clears back to Off. Switching to it while a Once (eco) cycle is already waiting upgrades that cycle in place.
Once (eco) One cycle now, but it only heats while the eco condition is met, then clears back to Off. It completes only if a single eco window is long enough to reach 60 °C and hold it for an hour.
As needed Standing policy. Starts a cycle each time the risk turns Elevated (the interval has lapsed), standing Smart Eco down for it like Once.
As needed (eco) Standing policy, like As needed, but it only heats while the eco condition is met.

A cycle ends when a full hour at 60 °C completes after it started.

Before 4.0.0 these were Disinfect (now Once (eco)), Disinfect ASAP (now Once) and Always ON (now As needed (eco)). A saved state carries over, but an automation or script that selects an old label by name must be updated: Home Assistant rejects the old label before this integration sees it.

A cycle puts the tank into performance and leaves it there until a full hour at 60 °C completes after the cycle started, then hands it back to the mode it had. A one-shot starts as soon as you choose it; an As needed policy starts when the risk reads Elevated or High. The cycle is goal-seeking, not timed — it is a standing request for temperature, not a fixed run.

It deliberately does not outrank anything:

  • Smart Eco still gates it — unless you chose an option without "(eco)". The eco condition decides whether the tank actually heats; the cycle only asks. Because the request is also written to smart_eco_last_heating_mode, a cycle survives the nightly gap — the eco gate parks the mode at off and restores performance next time the condition returns, so a cycle can span several days without anyone re-arming it.
  • A load shed still drops it. Shedding protects the supply and is checked first, so a balancer can take the tank down mid-cycle with no special-casing. The request stays standing.
  • A tank you switched off stays off — unless you ask for a cycle now. Once and Once (eco) are commands and will start on an off tank; the As needed options are standing policies and will not, because a policy should not overrule a mode you chose. This distinction carries the weight once Smart Eco is itself Off, since there is then no way to tell an eco-parked off from a deliberate one. A cycle started from off returns the tank to off, not to electric — otherwise, with no eco gate left, it would quietly hold its target on grid for ever after a cycle you thought was one-shot.

Taking the tank back ends the cycle. Changing the operation mode yourself — or turning the tank off, at the wall or in the UI — stops the cycle and sets the policy to Off, so a standing As needed policy cannot pull the tank straight back into performance the moment the risk sensor next reports. Re-arming is one tap. Asking for performance yourself does not end a cycle, since that is what it already wants.

Ending a Once / As needed cycle gives Smart Eco back immediately, rather than leaving it standing down until the time bound. That matters because a cancelled or abandoned cycle usually leaves the tank off, and an off tank never reports idle — so the "target reached" path could never fire and the full window would always elapse with the policy down for no live reason.

If you want it to finish faster, choose Once rather than pausing Smart Eco by hand. It does the same thing — stands the policy down — but through Off until target reached, so the policy is restored for you instead of waiting to be remembered. On a tank whose element cannot reach 60 °C inside one eco window, standing the policy down is the only way a cycle will ever complete.

It gives up after three days and tells you, via a persistent notification, how far the hold actually got. That bound is on the calendar rather than on run length on purpose: most of a hold is banked with the element already off, coasting down, so a run-length cap would abort precisely the part that earns the credit. A tank that has not got there in three days is not going to without help. A one-shot then returns to Off. An As needed policy stays on but waits one full interval before trying again; otherwise the risk, still Elevated, would restart the same failing cycle at once. Choosing a policy yourself clears that wait.

Reaching a disinfection temperature

performance mode heats continuously, ignoring the target, until the appliance's own mechanical thermostat opens — so it needs no change to max_temp. Two things to know:

  • The mechanical thermostat is the real ceiling. If it is set below 60 °C no cycle can ever qualify, and the attempt would be exactly the sub-60 °C treatment described above.
  • Smart Eco can cut a session short. Switching from electric to performance is not a heating boundary change, so it does not pause Smart Eco — and when the eco condition goes false the heater is forced off mid-session. Switching from off to performance does pause Smart Eco, which gives an uninterrupted session. Set the heater to off first, or set Smart Eco to Off until target reached for the duration.

Acknowledgments

This project was originally inspired by the upstream work from @dgomes on Generic Water Heater. Thanks for the original implementation and idea that this variant builds on.

About

Home Assistant Custom Component - Generic Water Heater

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages