diff --git a/custom_components/petlibro/devices/__init__.py b/custom_components/petlibro/devices/__init__.py index 8c2f312..2558216 100644 --- a/custom_components/petlibro/devices/__init__.py +++ b/custom_components/petlibro/devices/__init__.py @@ -2,8 +2,15 @@ from .device import Device from .feeders.granary_feeder import GranaryFeeder from .feeders.granary_camera_feeder import GranaryCameraFeeder +from .fountains.dockstream_fountain import DockstreamFountain product_name_map : Dict[str, Type[Device]] = { "Granary Feeder": GranaryFeeder, "Granary Camera Feeder": GranaryCameraFeeder } + +product_id_map : Dict[str, Type[Device]] = { + "PLAF103": GranaryFeeder, + "PLAF203": GranaryCameraFeeder, + "PLWF105": DockstreamFountain, +} diff --git a/custom_components/petlibro/devices/fountains/dockstream_fountain.py b/custom_components/petlibro/devices/fountains/dockstream_fountain.py new file mode 100644 index 0000000..8925a88 --- /dev/null +++ b/custom_components/petlibro/devices/fountains/dockstream_fountain.py @@ -0,0 +1,29 @@ +from typing import cast + +from .fountain import Fountain + + +class DockstreamFountain(Fountain): + async def refresh(self): + await super().refresh() + + + @property + def remaining_cleaning(self) -> str: + return cast(str, self._data.get("remainingCleaningDays")) + + @property + def remaining_replacement_days(self) -> str: + return cast(str, self._data.get("remainingReplacementDays")) + + @property + def today_totalMl(self) -> int: + quantity = self._data.get("todayTotalMl") + if not quantity: + return 0 + + return self.convert_unit(quantity) + + @property + def weight_percent(self) -> int: + return cast(int, self._data.get("weightPercent")) diff --git a/custom_components/petlibro/devices/fountains/fountain.py b/custom_components/petlibro/devices/fountains/fountain.py index 8f952c1..69ebc96 100644 --- a/custom_components/petlibro/devices/fountains/fountain.py +++ b/custom_components/petlibro/devices/fountains/fountain.py @@ -1,5 +1,43 @@ +from typing import Optional, cast from . import Device +UNITS = { + 1: "ml", + 2: "oz" +} + +UNITS_RATIO = { + 1: 1, + 2: 0.034 +} class Fountain(Device): - pass + async def refresh(self): + await super().refresh() + + + @property + def unit_id(self) -> int | None: + """The device unit type identifier""" + return self._data.get("unitType") + + @property + def unit_type(self) -> str | None: + """The device unit type""" + unit : Optional[str] = None + + if unit_id := self.unit_id: + unit = UNITS.get(unit_id) + + return unit + + def convert_unit(self, value: int) -> int: + """ + Convert a value to the device unit + + :param unit: Value to convert + :return: Converted value or unchanged if no unit + """ + if self.unit_id: + return value * UNITS_RATIO.get(self.unit_id, 1) + return value \ No newline at end of file diff --git a/custom_components/petlibro/hub.py b/custom_components/petlibro/hub.py index 981cb48..d9b987c 100644 --- a/custom_components/petlibro/hub.py +++ b/custom_components/petlibro/hub.py @@ -14,10 +14,10 @@ from .const import DOMAIN from .api import PetLibroAPIError -from .devices import Device, product_name_map +from .devices import Device, product_name_map, product_id_map _LOGGER = getLogger(__name__) -UPDATE_INTERVAL_SECONDS = 60 * 5 +UPDATE_INTERVAL_SECONDS = 60 * 2 class PetLibroHub: @@ -37,6 +37,7 @@ def __init__(self, hass: HomeAssistant, data: Mapping[str, Any]) -> None: name=DOMAIN, update_method=self.refresh_devices, update_interval=timedelta(seconds=UPDATE_INTERVAL_SECONDS), + ) async def get_device(self, serial: str) -> Optional[Device]: @@ -52,12 +53,12 @@ async def load_devices(self): if device := await self.get_device(device_data["deviceSn"]): await device.refresh() else: - if device_data["productName"] in product_name_map: - device = product_name_map[device_data["productName"]](device_data, self.api) + if device_data["productIdentifier"] in product_id_map: + device = product_id_map[device_data["productIdentifier"]](device_data, self.api) await device.refresh() # Get all API data self.devices.append(device) else: - _LOGGER.error("Unsupported device found: %s", device_data["productName"]) + _LOGGER.info("Unsupported device found: %s", device_data["productIdentifier"]) async def refresh_devices(self) -> bool: """Update all known devices states from the PETLIBRO API.""" diff --git a/custom_components/petlibro/sensor.py b/custom_components/petlibro/sensor.py index 6f9058b..ed632ae 100644 --- a/custom_components/petlibro/sensor.py +++ b/custom_components/petlibro/sensor.py @@ -19,6 +19,8 @@ from .devices import Device from .devices.feeders.feeder import Feeder from .devices.feeders.granary_feeder import GranaryFeeder +from .devices.fountains.fountain import Fountain +from .devices.fountains.dockstream_fountain import DockstreamFountain from . import PetLibroHubConfigEntry from .entity import PetLibroEntity, _DeviceT, PetLibroEntityDescription @@ -46,6 +48,14 @@ def device_class_feeder(device: Feeder) -> SensorDeviceClass | None: return SensorDeviceClass.WEIGHT if device.unit_type in ["cup", UnitOfVolume.MILLILITERS]: return SensorDeviceClass.VOLUME + + +def unit_of_measurement_fountain(device: Fountain) -> str | None: + return device.unit_type + +def device_class_fountain(device: Fountain) -> SensorDeviceClass | None: + if device.unit_type in ["ml", UnitOfVolume.MILLILITERS]: + return SensorDeviceClass.VOLUME @dataclass(frozen=True) @@ -115,6 +125,34 @@ def device_class(self) -> SensorDeviceClass | None: icon="mdi:history", state_class=SensorStateClass.TOTAL_INCREASING ) + ], + DockstreamFountain: [ + PetLibroSensorEntityDescription[DockstreamFountain]( + key="remaining_cleaning", + translation_key="remaining_cleaning", + icon="mdi:calendar-clock" + ), + PetLibroSensorEntityDescription[DockstreamFountain]( + key="remaining_replacement_days", + translation_key="remaining_replacement_days", + icon="mdi:calendar-clock" + ), + PetLibroSensorEntityDescription[DockstreamFountain]( + key="today_totalMl", + translation_key="today_totalMl", + icon="mdi:water", + native_unit_of_measurement_fn=unit_of_measurement_fountain, + device_class_fn=device_class_fountain, + device_class=SensorDeviceClass.VOLUME, + state_class=SensorStateClass.MEASUREMENT + ), + PetLibroSensorEntityDescription[DockstreamFountain]( + key="weight_percent", + translation_key="weight_percent", + icon="mdi:percent", + native_unit_of_measurement="%", + device_class=SensorDeviceClass.MOISTURE, + ) ] } diff --git a/custom_components/petlibro/translations/en.json b/custom_components/petlibro/translations/en.json index 37d5715..f458756 100644 --- a/custom_components/petlibro/translations/en.json +++ b/custom_components/petlibro/translations/en.json @@ -35,7 +35,26 @@ }, "today_feeding_times": { "name": "Today's feeding times" + }, + "today_water_quantity": { + "name": "Today's water quantity" + }, + "today_water_times": { + "name": "Today's water times" + }, + "remaining_cleaning": { + "name": "Cleaning remaining days" + }, + "remaining_replacement_days": { + "name": "Filter remaining days" + }, + "weight_percent": { + "name": "Water Remaining" + }, + "today_totalMl": { + "name": "Today's total water" } + }, "switch": { "feeding_plan": {