Skip to content

Commit 1a7c5d6

Browse files
authored
Feature/Per-Pet RFID Fountain Drinking Sensors (#92) (#229)
* feat: add device_wear_list API method for RFID fountain pet data * feat: fetch per-pet RFID fountain drinking data during pet refresh * feat: add per-pet fountain drinking sensor entity descriptions * feat: add translation keys for per-pet fountain drinking sensors * fix: use hub devices as fallback for RFID fountain discovery Shared accounts return empty from getBoundDevices, so also check the hub's loaded devices for DockstreamSmartRFIDFountain instances. The wearListV2 response filters by petId so only matching data is used. * refactor: remove non-functional entity_registry_enabled_default_fn callbacks The base PL_PetEntity.entity_registry_enabled_default property uses an 'or super()' fallback that defaults to True, meaning these callbacks could never actually disable entities. Remove them rather than ship dead code. --------- Co-authored-by: Jason Cronje <jasoncronje@users.noreply.github.com>
1 parent 30fbdaa commit 1a7c5d6

17 files changed

Lines changed: 240 additions & 12 deletions

File tree

custom_components/petlibro/api.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,30 @@ async def device_get_bound_pets(self, device_sn: str) -> list[dict]:
562562
_LOGGER.debug("Bound pets retrieved successfully")
563563
return data or []
564564

565+
async def device_wear_list(self, device_sn: str) -> list[dict]:
566+
"""Get wear/RFID data for pets bound to a device, with caching."""
567+
now = utcnow()
568+
cache_key = f"{device_sn}_wearListV2"
569+
last_call_time = self._last_api_call_times.get(cache_key)
570+
571+
if last_call_time and (now - last_call_time) < timedelta(seconds=10):
572+
_LOGGER.debug(f"Skipping wearListV2 request for {device_sn}, using cached response.")
573+
return self._cached_responses.get(cache_key, [])
574+
575+
try:
576+
response = await self.session.request("POST", "/device/device/wear/wearListV2", json={
577+
"deviceSn": device_sn,
578+
"type": 1
579+
})
580+
581+
self._last_api_call_times[cache_key] = now
582+
self._cached_responses[cache_key] = response if isinstance(response, list) else []
583+
584+
return self._cached_responses[cache_key]
585+
except Exception as e:
586+
_LOGGER.error(f"Error fetching wearListV2 for device {device_sn}: {e}")
587+
raise PetLibroAPIError(f"Error fetching wearListV2 for device {device_sn}: {e}")
588+
565589
# Support for new switch functions
566590
async def set_feeding_plan(self, serial: str, enable: bool):
567591
"""Set the feeding plan on/off."""

custom_components/petlibro/pets/__init__.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,45 @@ async def refresh(self) -> None:
6363
"""Refresh the pet info from the API."""
6464
pet_details = await self.api.pets.get_details(self.id)
6565
bound_devices = await self.api.pets.get_bound_devices(self.id)
66+
67+
# Fetch per-pet drinking data from RFID fountains
68+
fountain_drinking = {"todayFountainDrinkingCount": 0,
69+
"todayFountainDrinkingAmount": 0,
70+
"todayFountainDrinkingTime": 0}
71+
72+
# Collect RFID fountain serial numbers from bound devices and hub devices
73+
fountain_sns = set()
74+
for d in (bound_devices or []):
75+
if d.get("productName") == "Dockstream Smart RFID Fountain":
76+
sn = d.get("deviceSn")
77+
if sn:
78+
fountain_sns.add(sn)
79+
80+
# Also check hub's loaded devices for RFID fountains (shared accounts
81+
# return empty from getBoundDevices)
82+
if self.hub and self.hub.devices:
83+
from ..devices.fountains.dockstream_smart_rfid_fountain import DockstreamSmartRFIDFountain
84+
for device in self.hub.devices.values():
85+
if isinstance(device, DockstreamSmartRFIDFountain):
86+
fountain_sns.add(device.serial)
87+
88+
for device_sn in fountain_sns:
89+
try:
90+
wear_list = await self.api.device_wear_list(device_sn)
91+
for entry in wear_list:
92+
if entry.get("petId") == self.id:
93+
fountain_drinking["todayFountainDrinkingCount"] += (entry.get("todayDrinkTimes") or 0)
94+
fountain_drinking["todayFountainDrinkingAmount"] += (entry.get("todayDrinkAmount") or 0)
95+
fountain_drinking["todayFountainDrinkingTime"] += (entry.get("petEatingTime") or 0)
96+
break
97+
except Exception:
98+
_LOGGER.warning("Failed to fetch wearListV2 for fountain %s", device_sn)
99+
66100
self.update_data(
67101
{
68102
**pet_details,
69103
"boundDevices": bound_devices,
104+
**fountain_drinking,
70105
}
71106
)
72107

@@ -298,3 +333,20 @@ def trainingGoal(self) -> float:
298333
def walkingGoal(self) -> float:
299334
"""Walking goal of pet in minutes per day."""
300335
return self._data.get("walkingGoal") or 0
336+
337+
# --- Fountain Drinking (from wearListV2)
338+
339+
@property
340+
def today_fountain_drinking_count(self) -> int:
341+
"""Number of drinking sessions at RFID fountains today."""
342+
return self._data.get("todayFountainDrinkingCount") or 0
343+
344+
@property
345+
def today_fountain_drinking_amount(self) -> int:
346+
"""Total milliliters consumed at RFID fountains today."""
347+
return self._data.get("todayFountainDrinkingAmount") or 0
348+
349+
@property
350+
def today_fountain_drinking_time(self) -> int:
351+
"""Total seconds spent drinking at RFID fountains today."""
352+
return self._data.get("todayFountainDrinkingTime") or 0

custom_components/petlibro/pets/entity.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
SensorEntityDescription,
3333
dataclass,
3434
)
35-
from homeassistant.components.sensor.const import SensorDeviceClass
35+
from homeassistant.components.sensor.const import SensorDeviceClass, SensorStateClass
3636
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
3737
from homeassistant.helpers.entity import EntityDescription
3838
from homeassistant.helpers.update_coordinator import (
@@ -557,6 +557,32 @@ async def _async_update_avatar(self, url: str | None) -> None:
557557
else (bday.replace(year=today.year + 1) - today).days,
558558
} if pet.age else None,
559559
),
560+
PL_PetSensorEntityDescription(
561+
key="today_fountain_drinking_count",
562+
translation_key="today_fountain_drinking_count",
563+
name="Today's Fountain Drinking Count",
564+
icon="mdi:water-plus",
565+
state_class=SensorStateClass.TOTAL_INCREASING,
566+
),
567+
PL_PetSensorEntityDescription(
568+
key="today_fountain_drinking_amount",
569+
translation_key="today_fountain_drinking_amount",
570+
name="Today's Fountain Water Consumption",
571+
icon="mdi:cup-water",
572+
state_class=SensorStateClass.TOTAL_INCREASING,
573+
device_class=SensorDeviceClass.VOLUME,
574+
native_unit_of_measurement=UnitOfVolume.MILLILITERS,
575+
petlibro_unit=API.WATER_UNIT,
576+
),
577+
PL_PetSensorEntityDescription(
578+
key="today_fountain_drinking_time",
579+
translation_key="today_fountain_drinking_time",
580+
name="Today's Fountain Drinking Time",
581+
icon="mdi:timer-outline",
582+
state_class=SensorStateClass.TOTAL_INCREASING,
583+
device_class=SensorDeviceClass.DURATION,
584+
native_unit_of_measurement=UnitOfTime.SECONDS,
585+
),
560586
),
561587
PL_PetImageEntity: (
562588
PL_PetImageEntityDescription(

custom_components/petlibro/translations/ar.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,15 @@
242242
},
243243
"rfid": {
244244
"name": "طوق RFID"
245+
},
246+
"today_fountain_drinking_count": {
247+
"name": "Today's Fountain Drinking Count"
248+
},
249+
"today_fountain_drinking_amount": {
250+
"name": "Today's Fountain Water Consumption"
251+
},
252+
"today_fountain_drinking_time": {
253+
"name": "Today's Fountain Drinking Time"
245254
}
246255
},
247256
"image": {

custom_components/petlibro/translations/bn.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,15 @@
245245
},
246246
"rfid": {
247247
"name": "RFID কলার"
248+
},
249+
"today_fountain_drinking_count": {
250+
"name": "Today's Fountain Drinking Count"
251+
},
252+
"today_fountain_drinking_amount": {
253+
"name": "Today's Fountain Water Consumption"
254+
},
255+
"today_fountain_drinking_time": {
256+
"name": "Today's Fountain Drinking Time"
248257
}
249258
},
250259
"image": {

custom_components/petlibro/translations/da.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,15 @@
245245
},
246246
"rfid": {
247247
"name": "RFID-halsbånd"
248+
},
249+
"today_fountain_drinking_count": {
250+
"name": "Today's Fountain Drinking Count"
251+
},
252+
"today_fountain_drinking_amount": {
253+
"name": "Today's Fountain Water Consumption"
254+
},
255+
"today_fountain_drinking_time": {
256+
"name": "Today's Fountain Drinking Time"
248257
}
249258
},
250259
"image": {

custom_components/petlibro/translations/de.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,15 @@
245245
},
246246
"rfid": {
247247
"name": "RFID-Halsband"
248+
},
249+
"today_fountain_drinking_count": {
250+
"name": "Today's Fountain Drinking Count"
251+
},
252+
"today_fountain_drinking_amount": {
253+
"name": "Today's Fountain Water Consumption"
254+
},
255+
"today_fountain_drinking_time": {
256+
"name": "Today's Fountain Drinking Time"
248257
}
249258
},
250259
"image": {

custom_components/petlibro/translations/en.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,15 @@
274274
},
275275
"rfid": {
276276
"name": "RFID Collar"
277+
},
278+
"today_fountain_drinking_count": {
279+
"name": "Today's Fountain Drinking Count"
280+
},
281+
"today_fountain_drinking_amount": {
282+
"name": "Today's Fountain Water Consumption"
283+
},
284+
"today_fountain_drinking_time": {
285+
"name": "Today's Fountain Drinking Time"
277286
}
278287
},
279288
"image": {

custom_components/petlibro/translations/es.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,15 @@
245245
},
246246
"rfid": {
247247
"name": "Collar RFID"
248+
},
249+
"today_fountain_drinking_count": {
250+
"name": "Today's Fountain Drinking Count"
251+
},
252+
"today_fountain_drinking_amount": {
253+
"name": "Today's Fountain Water Consumption"
254+
},
255+
"today_fountain_drinking_time": {
256+
"name": "Today's Fountain Drinking Time"
248257
}
249258
},
250259
"image": {

custom_components/petlibro/translations/fr.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,15 @@
245245
},
246246
"rfid": {
247247
"name": "Collier RFID"
248+
},
249+
"today_fountain_drinking_count": {
250+
"name": "Today's Fountain Drinking Count"
251+
},
252+
"today_fountain_drinking_amount": {
253+
"name": "Today's Fountain Water Consumption"
254+
},
255+
"today_fountain_drinking_time": {
256+
"name": "Today's Fountain Drinking Time"
248257
}
249258
},
250259
"image": {

0 commit comments

Comments
 (0)