Skip to content

Commit da9cdd1

Browse files
authored
Luma litterbox support (#199)
* Add Luma Smart Litter Box (PLLB001) support with full entity coverage Reverse-engineered the PetLibro API from the Flutter APK (libapp.so) to discover /device/device/execCmdService with the "action" field as the command executor. This enables triggering clean cycles, emptying waste, leveling litter, controlling the door, and running the air purifier -- all confirmed working against a real device. Key additions: - Device class: LumaSmartLitterBox with 40+ properties from 4 API endpoints - API: exec_device_command() generic command executor + 7 specific command methods - Buttons: Start Clean, Empty Waste, Level Litter, Stop, Open/Close Door, Air Purifier - Switches: Sound, Light, Deodorization - Sensors: 21 sensors (WiFi, battery, weight, maintenance days, states, modes, volume) - Binary sensors: 10 sensors (online, waste bin, vacuum, deodorization, door, errors, sleep) - Firmware update entity support Discovered API actions: CLEAN, STOP_CLEAN, SUSPEND_CLEAN, RESTART_CLEAN, EMPTY, STOP_EMPTY, RESTART_EMPTY, LEVELING, VACUUM, OPEN_DOOR, CLOSE_DOOR, STOP, CANCEL * Update README: Move Luma Smart Litter Box to supported devices
1 parent 4e3a83c commit da9cdd1

15 files changed

Lines changed: 924 additions & 5 deletions

File tree

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,9 @@ If you enjoy this integration and want to support its development, please consid
4545
* Dockstream 2 Smart Fountain | Cordless Model (PLWF116)
4646

4747
### Litter Boxes
48-
* N/A
48+
* Luma Smart Litter Box (PLLB001)
4949

5050
### Pending Device(s)
51-
* Luma Smart Litter Box (PLLB001)
5251

5352
### Some Devices / May or may not work as intended
5453

custom_components/petlibro/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
from .devices.fountains.dockstream_smart_rfid_fountain import DockstreamSmartRFIDFountain
1919
from .devices.fountains.dockstream_2_smart_cordless_fountain import Dockstream2SmartCordlessFountain
2020
from .devices.fountains.dockstream_2_smart_fountain import Dockstream2SmartFountain
21+
from .devices.litterboxes.litter_box import LitterBox
22+
from .devices.litterboxes.luma_smart_litter_box import LumaSmartLitterBox
2123
from .const import DOMAIN, CONF_EMAIL, CONF_PASSWORD, PLATFORMS, UPDATE_INTERVAL_SECONDS # Assuming UPDATE_INTERVAL_SECONDS is defined in const
2224
from .hub import PetLibroHub
2325

@@ -136,6 +138,16 @@
136138
Platform.TEXT,
137139
Platform.UPDATE
138140
),
141+
LumaSmartLitterBox: (
142+
Platform.SENSOR,
143+
Platform.BINARY_SENSOR,
144+
Platform.SWITCH,
145+
Platform.BUTTON,
146+
Platform.NUMBER,
147+
Platform.SELECT,
148+
Platform.TEXT,
149+
Platform.UPDATE
150+
),
139151
}
140152

141153

custom_components/petlibro/api.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,103 @@ async def set_vacuum_mode(self, serial: str, value: str):
691691
raise
692692

693693

694+
async def exec_device_command(self, serial: str, action: str):
695+
"""Execute a device command via execCmdService.
696+
697+
Discovered actions for the Luma Smart Litter Box:
698+
CLEAN / STOP_CLEAN / SUSPEND_CLEAN / RESTART_CLEAN
699+
EMPTY / STOP_EMPTY / RESTART_EMPTY
700+
LEVELING / RESTART_LEVELING
701+
VACUUM (air purifier)
702+
OPEN_DOOR / CLOSE_DOOR
703+
STOP / CANCEL
704+
"""
705+
_LOGGER.debug(f"Executing device command: serial={serial}, action={action}")
706+
try:
707+
request_id = str(uuid.uuid4()).replace("-", "")
708+
response = await self.session.post("/device/device/execCmdService", json={
709+
"deviceSn": serial,
710+
"action": action,
711+
"requestId": request_id,
712+
})
713+
_LOGGER.debug(f"execCmdService({action}) returned: {response}")
714+
return response
715+
except Exception as e:
716+
_LOGGER.error(f"Failed to exec command {action} for device {serial}: {e}")
717+
raise
718+
719+
async def trigger_manual_clean(self, serial: str):
720+
"""Trigger a manual clean cycle on a litter box device."""
721+
return await self.exec_device_command(serial, "CLEAN")
722+
723+
async def trigger_empty_waste(self, serial: str):
724+
"""Trigger waste bin emptying on a litter box device."""
725+
return await self.exec_device_command(serial, "EMPTY")
726+
727+
async def trigger_level_litter(self, serial: str):
728+
"""Trigger litter leveling on a litter box device."""
729+
return await self.exec_device_command(serial, "LEVELING")
730+
731+
async def trigger_stop_device_action(self, serial: str):
732+
"""Stop the current device action."""
733+
return await self.exec_device_command(serial, "STOP")
734+
735+
async def trigger_open_door(self, serial: str):
736+
"""Open the litter box door."""
737+
return await self.exec_device_command(serial, "OPEN_DOOR")
738+
739+
async def trigger_close_door(self, serial: str):
740+
"""Close the litter box door."""
741+
return await self.exec_device_command(serial, "CLOSE_DOOR")
742+
743+
async def trigger_vacuum(self, serial: str):
744+
"""Trigger the air purifier (vacuum) on a litter box device."""
745+
return await self.exec_device_command(serial, "VACUUM")
746+
747+
async def set_clean_mode(self, serial: str, clean_mode: str, auto_delay_sec: int = 60):
748+
"""Set the litter box clean mode (AUTO/MANUAL) and auto-delay."""
749+
_LOGGER.debug(f"Setting clean mode: serial={serial}, mode={clean_mode}, delay={auto_delay_sec}")
750+
try:
751+
response = await self.session.post("/device/setting/updateCleanModeSetting", json={
752+
"deviceSn": serial,
753+
"cleanMode": clean_mode,
754+
"autoDelaySec": auto_delay_sec,
755+
})
756+
_LOGGER.debug(f"Clean mode update returned code: {response}")
757+
return response
758+
except Exception as e:
759+
_LOGGER.error(f"Failed to set clean mode for device {serial}: {e}")
760+
raise
761+
762+
async def set_deodorization_setting(self, serial: str, mode: str, switch: bool):
763+
"""Update deodorization mode and master switch."""
764+
_LOGGER.debug(f"Setting deodorization: serial={serial}, mode={mode}, switch={switch}")
765+
try:
766+
response = await self.session.post("/device/setting/updateDeodorizationSetting", json={
767+
"deviceSn": serial,
768+
"deodorizationMode": mode,
769+
"deodorizationModeSwitch": switch,
770+
})
771+
_LOGGER.debug(f"Deodorization setting returned code: {response}")
772+
return response
773+
except Exception as e:
774+
_LOGGER.error(f"Failed to set deodorization for device {serial}: {e}")
775+
raise
776+
777+
async def set_volume(self, serial: str, volume: int):
778+
"""Set speaker volume (0-100)."""
779+
_LOGGER.debug(f"Setting volume: serial={serial}, volume={volume}")
780+
try:
781+
response = await self.session.post("/device/setting/updateVolumeSetting", json={
782+
"deviceSn": serial,
783+
"volume": volume,
784+
})
785+
_LOGGER.debug(f"Volume setting returned code: {response}")
786+
return response
787+
except Exception as e:
788+
_LOGGER.error(f"Failed to set volume for device {serial}: {e}")
789+
raise
790+
694791
# Not supported by the dockstream device firmware yet. hoping that maybe it will be in the future, so leaving code here.
695792
# async def set_water_sensing_delay(self, serial: str, value: float, current_mode: int):
696793
# """Set the water sensing delay."""

custom_components/petlibro/binary_sensor.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from .devices.fountains.dockstream_smart_rfid_fountain import DockstreamSmartRFIDFountain
3636
from .devices.fountains.dockstream_2_smart_cordless_fountain import Dockstream2SmartCordlessFountain
3737
from .devices.fountains.dockstream_2_smart_fountain import Dockstream2SmartFountain
38+
from .devices.litterboxes.luma_smart_litter_box import LumaSmartLitterBox
3839
from .entity import PetLibroEntity, _DeviceT, PetLibroEntityDescription
3940

4041

@@ -523,7 +524,85 @@ def is_on(self) -> bool:
523524
should_report=lambda device: device.water_state is not None,
524525
name="Water Dispensing State"
525526
),
526-
]
527+
],
528+
LumaSmartLitterBox: [
529+
PetLibroBinarySensorEntityDescription[LumaSmartLitterBox](
530+
key="online",
531+
translation_key="online",
532+
icon="mdi:wifi",
533+
device_class=BinarySensorDeviceClass.CONNECTIVITY,
534+
should_report=lambda device: device.online is not None,
535+
name="Wi-Fi"
536+
),
537+
PetLibroBinarySensorEntityDescription[LumaSmartLitterBox](
538+
key="rubbish_full_state",
539+
translation_key="rubbish_full_state",
540+
icon="mdi:delete-alert",
541+
device_class=BinarySensorDeviceClass.PROBLEM,
542+
should_report=lambda device: device.rubbish_full_state is not None,
543+
name="Waste Bin Full"
544+
),
545+
PetLibroBinarySensorEntityDescription[LumaSmartLitterBox](
546+
key="rubbish_inplace_state",
547+
translation_key="rubbish_inplace_state",
548+
icon="mdi:delete-variant",
549+
device_class=BinarySensorDeviceClass.PRESENCE,
550+
should_report=lambda device: device.rubbish_inplace_state is not None,
551+
name="Waste Bin Installed"
552+
),
553+
PetLibroBinarySensorEntityDescription[LumaSmartLitterBox](
554+
key="vacuum_state",
555+
translation_key="vacuum_state",
556+
icon="mdi:robot-vacuum",
557+
should_report=lambda device: device.vacuum_state is not None,
558+
name="Vacuum Active"
559+
),
560+
PetLibroBinarySensorEntityDescription[LumaSmartLitterBox](
561+
key="deodorization_state_on",
562+
translation_key="deodorization_state_on",
563+
icon="mdi:air-purifier",
564+
should_report=lambda device: device.deodorization_state_on is not None,
565+
name="Deodorization Active"
566+
),
567+
PetLibroBinarySensorEntityDescription[LumaSmartLitterBox](
568+
key="door_open",
569+
translation_key="door_open",
570+
icon="mdi:door-open",
571+
device_class=BinarySensorDeviceClass.DOOR,
572+
should_report=lambda device: device.door_open is not None,
573+
name="Door"
574+
),
575+
PetLibroBinarySensorEntityDescription[LumaSmartLitterBox](
576+
key="device_stopped_working",
577+
translation_key="device_stopped_working",
578+
icon="mdi:alert-octagon",
579+
device_class=BinarySensorDeviceClass.PROBLEM,
580+
should_report=lambda device: device.device_stopped_working is not None,
581+
name="Device Error"
582+
),
583+
PetLibroBinarySensorEntityDescription[LumaSmartLitterBox](
584+
key="light_switch",
585+
translation_key="light_switch",
586+
icon="mdi:lightbulb",
587+
should_report=lambda device: device.light_switch is not None,
588+
name="Indicator"
589+
),
590+
PetLibroBinarySensorEntityDescription[LumaSmartLitterBox](
591+
key="whether_in_sleep_mode",
592+
translation_key="whether_in_sleep_mode",
593+
icon="mdi:sleep",
594+
should_report=lambda device: device.whether_in_sleep_mode is not None,
595+
name="Sleep Mode"
596+
),
597+
PetLibroBinarySensorEntityDescription[LumaSmartLitterBox](
598+
key="barn_door_error",
599+
translation_key="barn_door_error",
600+
icon="mdi:door-sliding-open",
601+
device_class=BinarySensorDeviceClass.PROBLEM,
602+
should_report=lambda device: device.barn_door_error is not None,
603+
name="Door Error"
604+
),
605+
],
527606
}
528607

529608
async def async_setup_entry(

custom_components/petlibro/button.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from .devices.fountains.dockstream_smart_rfid_fountain import DockstreamSmartRFIDFountain
3232
from .devices.fountains.dockstream_2_smart_cordless_fountain import Dockstream2SmartCordlessFountain
3333
from .devices.fountains.dockstream_2_smart_fountain import Dockstream2SmartFountain
34+
from .devices.litterboxes.luma_smart_litter_box import LumaSmartLitterBox
3435

3536
@dataclass(frozen=True)
3637
class RequiredKeysMixin(Generic[_DeviceT]):
@@ -398,6 +399,50 @@ class PetLibroButtonEntityDescription(ButtonEntityDescription, PetLibroEntityDes
398399
name="Filter Reset"
399400
)
400401
],
402+
LumaSmartLitterBox: [
403+
PetLibroButtonEntityDescription[LumaSmartLitterBox](
404+
key="trigger_clean",
405+
translation_key="trigger_clean",
406+
set_fn=lambda device: device.trigger_manual_clean(),
407+
name="Start Clean Cycle",
408+
),
409+
PetLibroButtonEntityDescription[LumaSmartLitterBox](
410+
key="trigger_empty_waste",
411+
translation_key="trigger_empty_waste",
412+
set_fn=lambda device: device.trigger_empty_waste(),
413+
name="Empty Waste Bin",
414+
),
415+
PetLibroButtonEntityDescription[LumaSmartLitterBox](
416+
key="trigger_level_litter",
417+
translation_key="trigger_level_litter",
418+
set_fn=lambda device: device.trigger_level_litter(),
419+
name="Level Litter",
420+
),
421+
PetLibroButtonEntityDescription[LumaSmartLitterBox](
422+
key="trigger_stop_action",
423+
translation_key="trigger_stop_action",
424+
set_fn=lambda device: device.trigger_stop_action(),
425+
name="Stop Current Action",
426+
),
427+
PetLibroButtonEntityDescription[LumaSmartLitterBox](
428+
key="trigger_open_door",
429+
translation_key="trigger_open_door",
430+
set_fn=lambda device: device.trigger_open_door(),
431+
name="Open Door",
432+
),
433+
PetLibroButtonEntityDescription[LumaSmartLitterBox](
434+
key="trigger_close_door",
435+
translation_key="trigger_close_door",
436+
set_fn=lambda device: device.trigger_close_door(),
437+
name="Close Door",
438+
),
439+
PetLibroButtonEntityDescription[LumaSmartLitterBox](
440+
key="trigger_vacuum",
441+
translation_key="trigger_vacuum",
442+
set_fn=lambda device: device.trigger_vacuum(),
443+
name="Run Air Purifier",
444+
),
445+
],
401446
}
402447

403448
class PetLibroButtonEntity(PetLibroEntity[_DeviceT], ButtonEntity):

custom_components/petlibro/devices/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
from .fountains.dockstream_smart_rfid_fountain import DockstreamSmartRFIDFountain
1515
from .fountains.dockstream_2_smart_cordless_fountain import Dockstream2SmartCordlessFountain
1616
from .fountains.dockstream_2_smart_fountain import Dockstream2SmartFountain
17+
from .litterboxes.litter_box import LitterBox
18+
from .litterboxes.luma_smart_litter_box import LumaSmartLitterBox
1719

1820
product_name_map : Dict[str, Type[Device]] = {
1921
"Air Smart Feeder": AirSmartFeeder,
@@ -25,5 +27,6 @@
2527
"Dockstream Smart RFID Fountain": DockstreamSmartRFIDFountain,
2628
"Dockstream 2 Smart Cordless Fountain": Dockstream2SmartCordlessFountain,
2729
"Dockstream 2 Smart Fountain": Dockstream2SmartFountain,
28-
"Space Smart Feeder": SpaceSmartFeeder
30+
"Space Smart Feeder": SpaceSmartFeeder,
31+
"Luma Smart Litter Box": LumaSmartLitterBox
2932
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .. import Device
2+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from . import Device
2+
3+
4+
class LitterBox(Device):
5+
pass

0 commit comments

Comments
 (0)