Skip to content

Commit ea35bdf

Browse files
committed
Merge branch 'feature/pets-management' of https://github.com/FeliGoblin/petlibro into feature/pets-management
Litterbox merge
2 parents ea09035 + 2349139 commit ea35bdf

16 files changed

Lines changed: 933 additions & 6 deletions

File tree

.github/workflows/validate.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ on:
66

77
permissions:
88
contents: read
9-
9+
packages: read
10+
1011
jobs:
1112
validate-hassfest:
1213
name: Hassfest Validation
@@ -15,6 +16,13 @@ jobs:
1516
- name: Checkout repository
1617
uses: actions/checkout@v5
1718

19+
- name: Log in to GHCR
20+
uses: docker/login-action@v3
21+
with:
22+
registry: ghcr.io
23+
username: ${{ github.actor }}
24+
password: ${{ secrets.GITHUB_TOKEN }}
25+
1826
- name: Run Hassfest validation
1927
uses: home-assistant/actions/hassfest@master
2028

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
@@ -711,6 +711,103 @@ async def set_vacuum_mode(self, serial: str, value: str):
711711
raise
712712

713713

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