201 lines
9.9 KiB
Python
201 lines
9.9 KiB
Python
"""Pet tech — feeders, fountains, litter boxes.
|
|
|
|
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
|
|
|
|
SIGNATURES: dict[str, IntegrationSignature] = {
|
|
"petkit": IntegrationSignature(
|
|
name="PetKit",
|
|
verified="2026-07-19 @ Jezza34000/homeassistant_petkit main sensor.py; resets + odor eliminator 2026-09-27 @ a749d54",
|
|
source=(
|
|
"HACS petkit (Jezza34000): tk 'desiccant_left_days' (feeder, "
|
|
"UnitOfTime.DAYS) and tk 'filter_percent' (water fountain, "
|
|
"PERCENTAGE). 2026-09-27: button.py tk 'reset_desiccant' (feeders) and "
|
|
"'reset_filter' (fountains; BLE for all but the W7H, cloud API there — a BLE "
|
|
"press without a reachable relay is a silent no-op). Litter boxes: tk "
|
|
"'odor_eliminator_n50_left_days' (state.deodorant_left_days) and "
|
|
"'odor_eliminator_n60_left_days' (state.spray_left_days), both "
|
|
"UnitOfTime.DAYS countdowns, created only when the box reports them, with "
|
|
"the resets 'reset_n50_odor_eliminator' (litter boxes except T7) and "
|
|
"'reset_n60_odor_eliminator' (camera boxes T5/T6/T7). "
|
|
"'purification_n60_left_days' is skipped (a third N60 countdown whose part "
|
|
"is not established)."
|
|
),
|
|
tasks=(
|
|
# 48 canonical hours = warn at 2 days of desiccant left.
|
|
ConsumableSignature(
|
|
("desiccant_left_days",),
|
|
"Replace Desiccant",
|
|
"duration_left",
|
|
below_hours=48,
|
|
resets=(("desiccant_left_days", "reset_desiccant"),),
|
|
),
|
|
ConsumableSignature(
|
|
("filter_percent",),
|
|
"Replace Water Filter",
|
|
"percent_left",
|
|
resets=(("filter_percent", "reset_filter"),),
|
|
),
|
|
# N50 block / N60 spray are separate parts → one task each where a
|
|
# box reports both.
|
|
ConsumableSignature(
|
|
("odor_eliminator_n50_left_days", "odor_eliminator_n60_left_days"),
|
|
"Replace Odor Eliminator",
|
|
"duration_left",
|
|
below_hours=48,
|
|
per_entity=True,
|
|
resets=(
|
|
("odor_eliminator_n50_left_days", "reset_n50_odor_eliminator"),
|
|
("odor_eliminator_n60_left_days", "reset_n60_odor_eliminator"),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
"litterrobot": IntegrationSignature(
|
|
name="Litter-Robot",
|
|
verified="2026-07-18 @ home-assistant/core dev; re-checked 2026-09-27 @ home-assistant/core 2026.9; filter due date 2026-10-03 @ home-assistant/core 2026.10.0b0",
|
|
source=(
|
|
"home-assistant/core homeassistant/components/litterrobot/sensor.py "
|
|
"(waste_drawer_level tk 'waste_drawer' % FULL -> alert_above; "
|
|
"litter_level tk 'litter_level' % remaining (LR4/5) -> "
|
|
"percent_left; total_cycles lifetime counter -> usage_delta). "
|
|
"2026-10-03 (identical in 2026.9): LR5 tk 'next_filter_replacement' "
|
|
"(device_class TIMESTAMP, DIAGNOSTIC, enabled; pylitterbot LitterRobot5."
|
|
"next_filter_replacement_date = the cloud's nextFilterReplacementDate) → "
|
|
"due_date, now that the catalog reads due dates; button.py LR5 tk "
|
|
"'change_filter' (CONFIG; pylitterbot change_filter() 'Reset the filter "
|
|
"replacement counter', command CHANGE_FILTER) moves that date forward → the "
|
|
"duty's reset. Skipped: LR5 binary 'laser_dirty' (device_class problem → "
|
|
"problem-sensor adoption), button 'reset' (LR4/LR5 robot reset, not a counter "
|
|
"reset)."
|
|
),
|
|
tasks=(
|
|
ConsumableSignature(("waste_drawer",), "Empty Waste Drawer", "alert_above", delta_units=90, resets=(("waste_drawer", "reset_waste_drawer"),)),
|
|
ConsumableSignature(("litter_level",), "Refill Litter", "percent_left"),
|
|
ConsumableSignature(("total_cycles",), "Wash Litter Box", "usage_delta", delta_units=150),
|
|
# LR5 filter: a week's lead time to order one (as for Vitesy's filter).
|
|
ConsumableSignature(
|
|
("next_filter_replacement",),
|
|
"Replace Filter",
|
|
"due_date",
|
|
days_before=7,
|
|
resets=(("next_filter_replacement", "change_filter"),),
|
|
),
|
|
),
|
|
),
|
|
"petlibro": IntegrationSignature(
|
|
name="PETLIBRO",
|
|
verified="2026-09-25 @ jjjonesjr33/petlibro dev; resets 2026-09-27 @ 7ee757f",
|
|
source=(
|
|
"jjjonesjr33/petlibro custom_components/petlibro/sensor.py: tk "
|
|
"'remaining_desiccant' (feeders, native 'd', DURATION — "
|
|
"remainingDesiccantDays), tk 'remaining_filter_days' (Dockstream "
|
|
"fountains, 'd' — realInfo.remainingReplacementDays), tk "
|
|
"'remaining_cleaning_days' (fountains + Polar wet-food feeder + Luma "
|
|
"litter box, 'd' — remainingCleaningDays), tk "
|
|
"'remaining_replacement_days' (Luma litter box 'Filter Replacement "
|
|
"Days', same API field as the fountain filter); button.py "
|
|
"desiccant_reset / filter_reset / cleaning_reset restart the countdowns "
|
|
"(tk = key; Granary/One RFID desiccant, the Dockstream fountains and the Luma "
|
|
"filter, fountains + Polar + Luma cleaning — wired as each duty's reset; "
|
|
"async_press only logs API errors). Luma's remaining_mat_days skipped (minor "
|
|
"accessory, no catalog duty, no reset)."
|
|
),
|
|
tasks=(
|
|
# 48 canonical hours = warn at 2 days left, like PetKit's desiccant.
|
|
ConsumableSignature(
|
|
("remaining_desiccant",),
|
|
"Replace Desiccant",
|
|
"duration_left",
|
|
below_hours=48,
|
|
resets=(("remaining_desiccant", "desiccant_reset"),),
|
|
),
|
|
ConsumableSignature(
|
|
("remaining_filter_days",),
|
|
"Replace Water Filter",
|
|
"duration_left",
|
|
below_hours=48,
|
|
resets=(("remaining_filter_days", "filter_reset"),),
|
|
),
|
|
ConsumableSignature(
|
|
("remaining_cleaning_days",),
|
|
"Clean Appliance",
|
|
"duration_left",
|
|
below_hours=48,
|
|
resets=(("remaining_cleaning_days", "cleaning_reset"),),
|
|
),
|
|
ConsumableSignature(
|
|
("remaining_replacement_days",),
|
|
"Replace Filter",
|
|
"duration_left",
|
|
below_hours=48,
|
|
resets=(("remaining_replacement_days", "filter_reset"),),
|
|
),
|
|
),
|
|
),
|
|
"eheimdigital": IntegrationSignature(
|
|
name="EHEIM Digital (aquarium)",
|
|
verified="2026-07-20 @ home-assistant/core dev",
|
|
source=(
|
|
"core eheimdigital: tk 'service_hours' (DURATION, HOURS remaining "
|
|
"to the next filter service, suggested display DAYS) — the "
|
|
"filter's own service countdown."
|
|
),
|
|
tasks=(ConsumableSignature(("service_hours",), "Filter Cleaning", "duration_left"),),
|
|
),
|
|
"petsafe": IntegrationSignature(
|
|
name="PetSafe ScoopFree",
|
|
verified="2026-09-27 @ dcmeglio/homeassistant-petsafe master (9c83b49, HACS default) + petsafe 2.0.6",
|
|
source=(
|
|
"dcmeglio/homeassistant-petsafe sensor.py + SensorEntities.py: per litter box "
|
|
"'Rake Counter' (_attr_has_entity_name, no translation_key → suffix "
|
|
"_rake_counter) = shadow.state.reported.rakeCount, the rakes since the "
|
|
"counter was last reset; button.py 'Reset' (suffix _reset, litter boxes only) "
|
|
"→ petsafe devices.DeviceScoopfree.reset(0) PATCHes rakeCount = 0 — the "
|
|
"reset the app offers after a fresh litter tray. usage_above 120 rakes ≈ a "
|
|
"month for one cat (tunable). Feeder food level is daily care — skipped."
|
|
),
|
|
tasks=(
|
|
ConsumableSignature(
|
|
("rake_counter",),
|
|
"Change Litter",
|
|
"usage_above",
|
|
above_hours=120,
|
|
resets=(("rake_counter", "reset"),),
|
|
),
|
|
),
|
|
),
|
|
# --- Round 16 (2026-10-03) --------------------------------------------
|
|
"neakasa": IntegrationSignature(
|
|
name="Neakasa litter box",
|
|
verified="2026-10-03 @ tabascoz/hass-neakasa main (0052c0a)",
|
|
source=(
|
|
"HACS neakasa (custom repository; Neakasa M1) — every entity class sets "
|
|
"_attr_translation_key: sensor/sand_percent.py tk 'sand_percent' ('Cat litter level', "
|
|
"PERCENTAGE = the device's sand level percent) → Refill Litter at the household floor (the "
|
|
"Litter-Robot precedent); binary_sensor/bin_full.py tk 'bin_full' ('Garbage can full' = "
|
|
"property binFullWaitReset == 1, NO device_class → not adoptable as a problem sensor) → "
|
|
"latch on 'on', cleared once the bin is emptied. Skipped: the 'bin_state' / 'sand_state' "
|
|
"ENUMs (the same two signals as text) and the clean / level buttons (actions, not "
|
|
"counter resets)."
|
|
),
|
|
translation_keys_authoritative=True,
|
|
tasks=(
|
|
ConsumableSignature(("sand_percent",), "Refill Litter", "percent_left"),
|
|
ConsumableSignature(
|
|
("bin_full",),
|
|
"Empty Waste Drawer",
|
|
"event_present",
|
|
entity_domain="binary_sensor",
|
|
on_states=("on",),
|
|
),
|
|
),
|
|
),
|
|
}
|