"""Robot lawn mowers, irrigation and pool/spa water care. Data module of the suggested-setups signature catalog — see ``helpers/signatures/_model.py`` for the direction semantics and the method contract (every entry cites and is verified against the integration's source; drift-probed weekly).""" from __future__ import annotations from ._model import ConsumableSignature, IntegrationSignature from ._shared import ( LANDROID_UNDERCARRIAGE_RUNTIME, MOWER_BLADES_RUNTIME, MOWER_UNDERCARRIAGE_RUNTIME, POOL_SALT_LOW, ) SIGNATURES: dict[str, IntegrationSignature] = { "husqvarna_automower": IntegrationSignature( name="Husqvarna Automower", verified="2026-07-17 @ home-assistant/core dev", source=( "home-assistant/core homeassistant/components/husqvarna_automower/sensor.py " "(translation_key 'cutting_blade_usage_time', DURATION s→h; matching reset button exists)" ), tasks=( ConsumableSignature(("cutting_blade_usage_time",), "Replace Blades", "usage_above", resets=(("cutting_blade_usage_time", "reset_cutting_blade_usage_time"),)), # Lifetime statistics sensors (SECONDS, suggested h) carry two more # duties: undercarriage washing by mowing time, contact cleaning by # docking cycles (unitless counter -> delta target is the count). ConsumableSignature(("total_cutting_time",), "Clean Undercarriage", "usage_delta", delta_units=25), ConsumableSignature( ("number_of_charging_cycles",), "Clean Charging Contacts", "usage_delta", delta_units=100, ), ), ), "landroid_cloud": IntegrationSignature( name="Worx Landroid", verified="2026-07-17 (sensors) / 2026-09-27 (reset) @ MTrab/landroid_cloud master", source=( "MTrab/landroid_cloud custom_components/landroid_cloud/sensor.py " "(translation_key 'blade_runtime_current' — since last reset, DURATION min→h). " "button.py tk 'reset_blade_time' (CONFIG, disabled by default; " "en 'Reset blade runtime' → suffix _reset_blade_runtime) → " "pyworxcloud reset_blade_counter moves the reset marker, so the " "current runtime drops to 0 (verified @ f30af7f)." ), tasks=( ConsumableSignature( ("blade_runtime_current",), "Replace Blades", "usage_above", resets=(("blade_runtime_current", "reset_blade_time"),), ), LANDROID_UNDERCARRIAGE_RUNTIME, ), ), "gardena_smart_system": IntegrationSignature( name="Gardena Smart System", verified="2026-07-18 @ py-smart-gardena/hass-gardena-smart-system master", source=( "py-smart-gardena/hass-gardena-smart-system sensor.py " "GardenaMowerOperatingHoursSensor (entity_id " "'{device.id}_{service.id}_operating_hours', UnitOfTime.HOURS, " "TOTAL_INCREASING lifetime — no reset anywhere) → usage_delta." ), tasks=( # Sileno mowers: pivoting razor blades wear by mowing time — every # 100 operating hours since the last change (delta re-baselines on # completion, matching the Husqvarna default). ConsumableSignature(("operating_hours",), "Replace Blades", "usage_delta", delta_units=100), ConsumableSignature(("operating_hours",), "Clean Undercarriage", "usage_delta", delta_units=25), ), ), # Same source entity, second duty — the matcher allows multi-duty. "navimow": IntegrationSignature( name="Segway Navimow", verified="2026-07-18 @ pgoutsos/NavimowHA main", source=( "pgoutsos/NavimowHA lawn_mower.py (one LawnMower entity per " "device) + const.py MOWER_STATUS_TO_ACTIVITY ('mowing' → " "LawnMowerActivity.MOWING). The integration exposes NO usage " "counter — the ENGINE accumulates mowing time itself via the " "runtime trigger on the lawn_mower entity." ), tasks=( MOWER_BLADES_RUNTIME, MOWER_UNDERCARRIAGE_RUNTIME, ), ), "sunseeker": IntegrationSignature( name="Sunseeker mowers", verified="2026-07-20 (sensors) / 2026-09-27 (parts split, resets) @ Sdahl1234/Sunseeker-lawn-mower main (HACS default)", source=( "HACS sunseeker (also Ambrogio/Techline via ZCS), models S/X: " "four consumables, each with its own reset — 'Blade' (tk " "sunseeker_blade_time_left, HOURS, and sunseeker_blade_health, %), " "'Cutterplade' = the cutting disc (sunseeker_cutterplade_*), and " "with support_edge_trim the edge trimmer's 'Small blade' / 'Small " "cutterplade' (sunseeker_small_*); remaining = mp minus at of the " "consumable item. button.py (tk set on every entity): " "sunseeker_reset_blade / _reset_bladeplade / _reset_small_blade / " "_reset_small_bladeplade → maintain_consumable_item " "blade/cutter/small_blade/small_cutter (verified @ 659ef13). The " "parts wear and are reset independently, so each is its own duty " "(one reset per duty; the former any-low 'Replace Blades' over " "all parts could only have pressed one of them). The blade keeps " "both directions (hours + %); the other parts use the % sensor. " "Plain alias keys only match entities without a translation_key " "(translation_keys_authoritative) and are never paired with a " "reset." ), translation_keys_authoritative=True, tasks=( ConsumableSignature( ("blade_time_left", "sunseeker_blade_time_left"), "Replace Blades", "duration_left", below_hours=24, resets=(("sunseeker_blade_time_left", "sunseeker_reset_blade"),), ), ConsumableSignature( ("blade_health", "sunseeker_blade_health"), "Replace Blades", "percent_left", resets=(("sunseeker_blade_health", "sunseeker_reset_blade"),), ), ConsumableSignature( ("cutterplade_health", "sunseeker_cutterplade_health"), "Replace Cutting Disc", "percent_left", resets=(("sunseeker_cutterplade_health", "sunseeker_reset_bladeplade"),), ), ConsumableSignature( ("small_blade_health", "sunseeker_small_blade_health"), "Replace Edge Trimmer Blade", "percent_left", resets=(("sunseeker_small_blade_health", "sunseeker_reset_small_blade"),), ), ConsumableSignature( ("small_cutterplade_health", "sunseeker_small_cutterplade_health"), "Replace Edge Trimmer Disc", "percent_left", resets=(("sunseeker_small_cutterplade_health", "sunseeker_reset_small_bladeplade"),), ), ), ), "husqvarna_automower_ble": IntegrationSignature( name="Husqvarna Automower BLE", verified="2026-07-18 @ home-assistant/core dev", source=( "home-assistant/core homeassistant/components/husqvarna_automower_ble/" "lawn_mower.py (lawn_mower entity; the BLE variant exposes no blade " "counter) — the ENGINE accumulates mowing time." ), tasks=( MOWER_BLADES_RUNTIME, MOWER_UNDERCARRIAGE_RUNTIME, ), ), "rainbird": IntegrationSignature( name="Rain Bird irrigation", verified="2026-07-20 @ home-assistant/core dev", source=( "core rainbird: switch.py creates one irrigation-zone switch per " "zone, each on its own 'Rain Bird Sprinkler ' device (single " "switch entity per device). No consumable sensors; the rainsensor " "binary carries no device_class (not adoptable) and raindelay is " "operational — the ENGINE accumulates actual watering time on the " "zone switch instead. Cadence per Rain Bird's maintenance " "guidance: inspect/clean heads and (drip) filters at least once a " "season — ~30 h of watering at typical in-season use." ), tasks=( ConsumableSignature( (), "Clean Sprinkler Heads", "runtime_hours", delta_units=30, entity_domain="switch", on_states=("on",), ), ), ), # Pool chlorinator salt: refill when the water's salt concentration # drops below the generator's operating band (Pentair: 2600-4500 ppm; # low-salt cells stop producing chlorine). Topping up raises the value # back — auto-resolve. "screenlogic": IntegrationSignature( name="Pentair ScreenLogic", verified="2026-07-20 @ home-assistant/core dev; skips re-checked 2026-10-03 @ home-assistant/core 2026.10.0b0", source=( "core screenlogic: tk 'salt_ppm' (MEASUREMENT, ppm) — IntelliChlor salt concentration. " "2026-10-03 skip: 'salt_tds_ppm' is IntelliChem's CONFIGURATION value (a CONFIG " "number; its sensor twin is disabled by default as superseded) — a setting, not a " "reading." ), tasks=(ConsumableSignature(("salt_ppm",), "Refill Pool Salt", "value_below", delta_units=2700),), ), "ondilo_ico": IntegrationSignature( name="Ondilo ICO", verified="2026-07-20 @ home-assistant/core dev", source=("core ondilo_ico: tk 'salt' (mg/L ≡ ppm numerically) — pool salt concentration."), tasks=(POOL_SALT_LOW,), ), # --- Round 14 (2026-09-25) -------------------------------------------- "mammotion": IntegrationSignature( name="Mammotion (Luba)", verified="2026-09-25 @ mikey0000/Mammotion-HA main", source=( "HACS mammotion (not in the default store) sensor.py " "LUBA_SENSOR_ONLY_TYPES (MammotionSensorEntity sets translation_key " "= key): 'blade_used_time' (maintenance.blade_used_time, SECONDS, " "suggested h — counts since the blade reset; lawn_mower.py service " "reset_blade_time) and 'maintenance_work_time' ('Total work " "time', SECONDS, lifetime). Yuka models carry no blade sensors." ), tasks=( ConsumableSignature(("blade_used_time",), "Replace Blades", "usage_above"), ConsumableSignature(("maintenance_work_time",), "Clean Undercarriage", "usage_delta", delta_units=25), ), ), "dreame_mower": IntegrationSignature( name="Dreame Mower", verified="2026-09-25 @ bhuebschen/dreame-mower main", source=( "HACS dreame_mower sensor.py DreameMowerProperty.BLADES_LEFT → " "key/translation_key 'blades_left' (dreame/const.py; siid 9 piid " "2, UNIT_PERCENT — device.blades_life 'blade remaining life in " "percent'; only created when the mower reports it, exists_fn). " "button.py RESET_BLADES: entity.py sets translation_key = " "ACTION_TO_NAME key 'reset_blades' (entity_id _reset_blades); " "device.py sets BLADES_LEFT back to 100 (verified @ 33a89dd). The " "button is unavailable while the blades are at 100 %. The sibling " "'blades_time_left' (hours, same reset) is the same duty and is not " "signed twice." ), tasks=( ConsumableSignature( ("blades_left",), "Replace Blades", "percent_left", resets=(("blades_left", "reset_blades"),), ), ), ), # Two HACS forks share the domain and the keys (SmartServicePL and # ADNPolymerase) — mirrors landroid_cloud. "worx_vision_cloud": IntegrationSignature( name="Worx Landroid Vision", verified=("2026-09-25 @ SmartServicePL/worx_vision_cloud_plus_github main + ADNPolymerase/ha-landroid-vision main"), source=( "HACS worx_vision_cloud sensor.py (both forks): tk " "'blade_runtime_current' (blades.current_on or blade_work_time " "minus the reset marker — since the last blade reset, MINUTES) " "and 'mower_runtime_total' (worktime_total, MINUTES, " "TOTAL_INCREASING lifetime). button.py (both forks, verified @ " "ddccee6 / 2540f6c): tk 'reset_blade_counter' (CONFIG, enabled; " "note: not landroid_cloud's 'reset_blade_time') → pyworxcloud " "reset_blade_counter, the same reset marker." ), tasks=( ConsumableSignature( ("blade_runtime_current",), "Replace Blades", "usage_above", resets=(("blade_runtime_current", "reset_blade_counter"),), ), LANDROID_UNDERCARRIAGE_RUNTIME, ), ), "intellicenter": IntegrationSignature( name="Pentair IntelliCenter", verified="2026-09-25 @ joyfulhouse/intellicenter main", source=( "HACS intellicenter sensor.py: chlorinator (CHLORINATOR_SUBTYPE) " "PoolSensor on SALT_ATTR, name '+ (Salt)' → ' " "(Salt)', CONCENTRATION_PPM (no translation_key → suffix _salt). " "Same IntelliChlor band as ScreenLogic." ), tasks=(POOL_SALT_LOW,), ), "hotspring": IntegrationSignature( name="Hot Spring spas", verified="2026-09-25 (cartridge) / 2026-09-27 (10-day check) @ home-assistant/core 2026.9 + dev", source=( "core hotspring sensor.py (new in 2026.9): tk " "'water_care_120_day_timer' ('Salt cartridge age' — per the " "integration docs 'number of days the FreshWater Salt System " "cartridge has been in use', i.e. counts UP; DURATION DAYS; only " "created while a cartridge is installed; python-hotspring passes " "the spa's raw 120DayTimer). Hot Spring rates the cartridge for " "up to four months → 120 days = 2,880 h. tk " "'water_care_10_day_timer' ('Salt 10-day check timer', DURATION " "DAYS, same cartridge gate) — the docs: 'number of days remaining " "until the next 10-day salt water test reminder', their example " "automation fires below 1 day → duration_left at 24 h (= 1 d) " "for the test-strip water test; the task auto-resolves when the " "spa's timer starts over. 'water_care_salt_value' (unitless " "reading, no documented scale) is not used." ), tasks=( ConsumableSignature(("water_care_120_day_timer",), "Replace Salt Cartridge", "usage_above", above_hours=2880), ConsumableSignature(("water_care_10_day_timer",), "Water Test", "duration_left"), ), ), "neopool": IntegrationSignature( name="Sugar Valley NeoPool", verified="2026-09-25 @ home-assistant/core dev", source=( "core neopool binary_sensor.py (platform on dev, i.e. 2026.10): tk " "'uv_lamp' (RUNNING; neopool_modbus decodes the UV relay bit, " "created only for a valid MBF_PAR_UV_RELAY_GPIO). No lamp-hours " "counter — the ENGINE accumulates lamp-on time. Sugar Valley " "UVScenic manual (routine maintenance): UV lamps last '1 year or " "8,000 hours'. NOTE: 'measure_cl' is chlorine ppm despite its " "'Salt level' label and is never mapped to salt; " "cell_runtime_part has no documented cleaning interval (cells " "self-clean by polarity reversal) and is not used. Re-checked " "2026-09-27 @ core 2026.9: button.py 'reset_cell_partial' " "(disabled by default) calls neopool_modbus " "async_reset_user_counters, which clears the cell partial " "runtime AND the ION/UV partial work-time — no duty watches " "those counters, so it stays unwired." ), tasks=( ConsumableSignature( ("uv_lamp",), "Replace UV Lamp", "runtime_hours", delta_units=8000, entity_domain="binary_sensor", on_states=("on",), ), ), ), "bestway": IntegrationSignature( name="Bestway (Lay-Z-Spa / Flowclear)", verified="2026-09-25 @ cdpuk/ha-bestway main", source=( "HACS bestway binary_sensor.py PoolFilterChangeRequiredSensor " "(key 'pool_filter_change_required', name 'Pool Filter Change " "Required', no device class → not adoptable as a problem sensor; " "status.filter_change_required from the pump's 'filter' " "attribute). No has_entity_name → entity_id " "'binary_sensor.pool_filter_change_required' (exact object-id " "match). State latch on 'on'; clears after the change." ), tasks=( ConsumableSignature( ("pool_filter_change_required",), "Replace Filter", "event_present", entity_domain="binary_sensor", on_states=("on",), ), ), ), # --- Round 15 (2026-09-27): HACS mowers and pool dosing ----------------- "robonect": IntegrationSignature( name="Robonect (Husqvarna/Gardena/Flymo)", verified="2026-09-27 @ geertmeersman/robonect main (114b8ca)", source=( "HACS robonect definitions.py: 'mower/blades/hours' " "($.status.blades.hours — blade hours since the last blade reset; " "DURATION, h, TOTAL_INCREASING) and 'mower/statistic/hours' " "($.status.status.hours, lifetime mowing hours); entity.py sets " "translation_key = slugify(key.replace('/', '_')) → tk " "'mower_blades_hours' / 'mower_statistic_hours'. button.py " "'blades_reset' (cmd reset_blades; created when the REST API is " "enabled) resets the blade counter. Same 100 h blades / 25 h " "undercarriage cadence as the Husqvarna Automower entry." ), translation_keys_authoritative=True, tasks=( ConsumableSignature( ("mower_blades_hours",), "Replace Blades", "usage_above", resets=(("mower_blades_hours", "blades_reset"),), ), ConsumableSignature(("mower_statistic_hours",), "Clean Undercarriage", "usage_delta", delta_units=25), ), ), "indego": IntegrationSignature( name="Bosch Indego", verified="2026-09-27 @ sander1988/Indego develop (bf10e09)", source=( "HACS indego __init__.py ENTITY_RUNTIME: tk 'runtime_total' " "(DURATION, h, TOTAL_INCREASING; state = runtime.total.cut, never " "allowed to decrease) — LIFETIME cutting hours → usage_delta, like " "the Gardena operating-hours pair (100 h blades / 25 h " "undercarriage). 'maintenance_hours' (lifetime operate hours, " "integration-side good/due banding, not resettable) is not used; " "the mower's own blade reminder arrives via the problem-class " "alert binary (adoption path). No reset button." ), tasks=( ConsumableSignature(("runtime_total",), "Replace Blades", "usage_delta", delta_units=100), ConsumableSignature(("runtime_total",), "Clean Undercarriage", "usage_delta", delta_units=25), ), ), "plant": IntegrationSignature( name="Plant Monitor", verified="2026-10-06 @ Olen/homeassistant-plant master (aa1c721, 2026.9.0)", source=( "HACS plant (Olen/homeassistant-plant; shadows HA's YAML plant " "component) custom_components/plant/__init__.py PlantDevice: one " "plant. entity per plant, unique_id = its config entry, state " "STATE_OK / STATE_PROBLEM — problem while any watched reading " "(moisture, light, temperature, conductivity, humidity, DLI) is " "outside the plant's thresholds, which the plant keeps as its own " "number entities. plant.py adds it through the config-entry " "platform, so it is registered with the plant's device." ), tasks=( # The plant's own summary, not one reading: the thresholds stay # the plant's (OpenPlantbook or edited there), the latch follows # every change of them, and back to "ok" (watered, moved into the # shade) completes the task by itself. #204. ConsumableSignature((), "Check Plant", "event_present", entity_domain="plant", on_states=("problem",)), ), ), "dreame_lawn_mower": IntegrationSignature( name="Dreame / MOVA lawn mowers", verified="2026-09-27 @ EvotecIT/homeassistant-dreamelawnmower main (967e50b)", source=( "HACS dreame_lawn_mower (not the dreame_mower domain) " "sensor_operations.py DreameLawnMowerMaintenanceRemainingSensor: " "name ' Remaining' under has_entity_name, no " "translation_key, '%' = the cloud CMS counter's remaining_percent " "— items (dreame_lawn_mower_client/maintenance.py) 'Blade' (6000 " "min), 'Cleaning Brush' (30000 min), 'Robot Maintenance' (3600 " "min) → suffixes _blade_remaining / _cleaning_brush_remaining / " "_robot_maintenance_remaining. button.py " "DreameLawnMowerResetMaintenanceButton 'Reset Maintenance' " "(DIAGNOSTIC, disabled by default — wiring enables it) resets " "that item's counter. 'Robot Maintenance' (the app's 60-hour " "check) is mapped to Clean Undercarriage." ), tasks=( ConsumableSignature( ("blade_remaining",), "Replace Blades", "percent_left", resets=(("blade_remaining", "reset_blade_maintenance"),), ), ConsumableSignature( ("cleaning_brush_remaining",), "Replace Cleaning Brush", "percent_left", resets=(("cleaning_brush_remaining", "reset_cleaning_brush_maintenance"),), ), ConsumableSignature( ("robot_maintenance_remaining",), "Clean Undercarriage", "percent_left", resets=(("robot_maintenance_remaining", "reset_robot_maintenance_maintenance"),), ), ), ), "bayrol": IntegrationSignature( name="Bayrol pool (Automatic SALT / Cl-pH, PoolManager 5)", verified="2026-09-27 @ 0xQuantumHome/bayrol-home-hassistant main (c84c5ca)", source=( "HACS bayrol sensor.py BayrolSensor: entity_id = " "sensor.bayrol__, no " "translation_key. const.py: 'pH Minus Canister Status' (5.80, " "Automatic) and 'pH Canister Level' (5.6064, PM5); 'Cl Canister " "Status' (5.169, Automatic Cl-pH), 'Cl Canister Level' / 'Redox " "Canister Level' (5.6066 / 5.6068, PM5); the decode table maps " "19.259 and 7523 to the state 'Empty' (19.258 'Not Empty', 7521 " "'Full', 7522 'Low') → latch on 'Empty', cleared by a new " "canister. 'Salt To Add' (4.138, kg, Automatic SALT) = salt " "missing to the preferred level → alert_above 5 kg (editorial, a " "fifth of a 25 kg bag; clears once salted). 'Missing' canisters " "and the salt alarms (messages sensor only) are faults, not " "duties." ), tasks=( ConsumableSignature( ("ph_minus_canister_status", "ph_canister_level"), "Replace pH Canister", "event_present", on_states=("Empty",), ), ConsumableSignature( ("cl_canister_status", "cl_canister_level", "redox_canister_level"), "Replace Chlorine Canister", "event_present", on_states=("Empty",), per_entity=True, ), ConsumableSignature(("salt_to_add",), "Refill Pool Salt", "alert_above", delta_units=5), ), ), # --- Round 16 (2026-10-03): HACS spas, mowers and pool equipment -------- "gecko": IntegrationSignature( name="Gecko spa packs (in.touch)", verified="2026-10-03 @ gazoodle/gecko-home-assistant main (eb3fba4) + geckolib 1.0.16", source=( "HACS gecko sensor.py GeckoReminderSensor: one sensor per reminder the spa pack reports " "(geckolib GeckoReminders.reminders, INVALID slots dropped), name ': due' " "(GeckoEntityBase.name; no has_entity_name, no translation_key → entity-id and " "original-name suffixes _rinse_filter_due, _clean_filter_due, _change_water_due, " "_change_ozonator_due, _change_vision_cartridge_due), device_class timestamp, no unit; the " "value is midnight UTC today + the reminder's remaining days (in the past once overdue). " "Resetting the reminder on the spa (or through the CONFIG date twin of the same name, " "date.py set_reminder) moves the date forward, which clears the trigger and auto-completes " "the task — there is no button to wire. The date.* twins carry the same names and are kept " "out by the sensor domain. Rinse / clean / change water map onto the Hot Tub template's " "duties, due on the date itself (the spa's own reminder); the ozonator and the Vision " "cartridge are parts to order → two weeks' lead. Skipped: 'Check Spa due' (a generic " "check, no part or action)." ), tasks=( ConsumableSignature(("rinse_filter_due",), "Rinse Filter", "due_date"), ConsumableSignature(("clean_filter_due",), "Deep Clean Filter", "due_date"), ConsumableSignature(("change_water_due",), "Drain and Refill", "due_date"), ConsumableSignature(("change_ozonator_due",), "Replace Ozonator", "due_date", days_before=14), ConsumableSignature(("change_vision_cartridge_due",), "Replace Vision Cartridge", "due_date", days_before=14), ), ), "terramow": IntegrationSignature( name="TerraMow", verified="2026-10-03 @ TerraMow/TerraMowHA main (045d789)", source=( "TerraMow's own HACS integration (custom repository) sensor.py — has_entity_name and " "_attr_translation_key on every entity class: 'remaining_blade_time' ('Remaining Blade " "Time', de 'Verbleibende Laufzeit der Klingen'; MINUTES, DURATION, DIAGNOSTIC) = " "const.BLADE_MAINTENANCE_CYCLE_MINUTES (14,400 = 240 h) minus dp_126 'blade disk usage " "time', floored at 0; 'remaining_base_station_time' (MINUTES) = " "BASE_STATION_MAINTENANCE_CYCLE_MINUTES (43,200 = 30 days) minus dp_125 'base station usage " "time' — both computed in the integration from the vendor's recommended cycles and both " "restart when the robot's counter is reset in the TerraMow app (dp value 0; no HA " "button). The base-station cycle maps to Clean Charging Contacts (the Robot Lawn Mower " "template's station duty). 'total_mowing_time' (dp_124 statistics duration, SECONDS, " "TOTAL_INCREASING, lifetime) → Clean Undercarriage every 25 h like the other lifetime " "mowing counters. NOTE: docs/en/developers/data_point.md calls both cycles a 'recommended " "cleaning cycle' (and gives 240 minutes for the blade disk where the integration uses 240 " "hours)." ), translation_keys_authoritative=True, tasks=( ConsumableSignature(("remaining_blade_time",), "Replace Blades", "duration_left"), ConsumableSignature(("remaining_base_station_time",), "Clean Charging Contacts", "duration_left"), ConsumableSignature(("total_mowing_time",), "Clean Undercarriage", "usage_delta", delta_units=25), ), ), "stihl_imow": IntegrationSignature( name="STIHL iMOW", verified="2026-10-03 @ ChrisHaPunkt/ha-stihl-imow main (0fe6c7b)", source=( "HACS stihl_imow (custom repository) maps.py IMOW_SENSORS_MAP + entity.py: has_entity_name, " "translation_key = to_translation_key(property) on every entity → " "'statistics_total_blade_operating_time' (statistics_totalBladeOperatingTime) and " "'statistics_total_operating_time' — DURATION, SECONDS with suggested HOURS, " "TOTAL_INCREASING lifetime counters, enabled (not in DISABLED_BY_DEFAULT_PROPERTIES) → the " "Gardena / Indego pair: blades every 100 h of blade time, undercarriage every 25 h of " "operation. Skipped: 'status_blade_service' (status_bladeService, no type or unit, meaning " "undocumented). No reset button." ), translation_keys_authoritative=True, tasks=( ConsumableSignature(("statistics_total_blade_operating_time",), "Replace Blades", "usage_delta", delta_units=100), ConsumableSignature(("statistics_total_operating_time",), "Clean Undercarriage", "usage_delta", delta_units=25), ), ), "omnilogic_local": IntegrationSignature( name="Hayward OmniLogic (local)", verified="2026-10-03 @ cryptk/haomnilogic-local main (6a8abb8)", source=( "HACS omnilogic_local (custom repository; not the core 'omnilogic' cloud domain) sensor.py " "CHLORINATOR_SALT_SENSORS 'chlorinator_salt_level_average' (name 'Average Salt Level', " "PARTS_PER_MILLION, MEASUREMENT); the entity is named f'{equipment.name} Average Salt " "Level' under has_entity_name, no translation_key → suffix _average_salt_level. Hayward " "salt cells start their operating range at 2,700 ppm, the IntelliChlor band of the " "ScreenLogic entry → value_below 2,700; topping up raises the reading (auto-resolve). The " "'Instant Salt Level' twin (the raw reading) is not used." ), tasks=(ConsumableSignature(("average_salt_level",), "Refill Pool Salt", "value_below", delta_units=2700),), ), "fluidra_pool": IntegrationSignature( name="Fluidra Pool (Fluidra Connect)", verified="2026-10-03 @ foXaCe/Fluidra-pool main (fef0d33)", source=( "HACS fluidra_pool (custom repository) sensor/chlorinator.py FluidraUvRunningHoursSensor: " "tk 'uv_running_hours' (DURATION, HOURS, TOTAL_INCREASING — 'a plain integer hour counter " "… the lamp-replacement interval is a running-hours threshold'), created only when the " "chlorinator reports a UV block with running_hours (sensor/__init__.py) → Replace UV Lamp " "every 8,000 h (the UV-lamp rating of the NeoPool entry). Whether the controller restarts " "the counter after a lamp change is not documented, hence usage_delta (completing " "re-baselines either way)." ), tasks=(ConsumableSignature(("uv_running_hours",), "Replace UV Lamp", "usage_delta", delta_units=8000),), ), }