88from functools import cached_property
99from typing import Optional
1010import logging
11- from .const import DOMAIN
11+ from .const import DOMAIN , Unit , APIKey as API , VALID_UNIT_TYPES
1212from homeassistant .components .binary_sensor import (
1313 BinarySensorEntity ,
1414 BinarySensorEntityDescription ,
1515 BinarySensorDeviceClass ,
1616)
1717from homeassistant .core import HomeAssistant
1818from homeassistant .helpers .entity_platform import AddEntitiesCallback
19- from homeassistant .config_entries import ConfigEntry # Added ConfigEntry import
20- from .hub import PetLibroHub # Adjust the import path as necessary
21-
19+ from homeassistant .config_entries import ConfigEntry
20+ from .hub import PetLibroHub
2221
2322_LOGGER = logging .getLogger (__name__ )
2423
@@ -46,9 +45,12 @@ class PetLibroBinarySensorEntityDescription(BinarySensorEntityDescription, PetLi
4645 device_class_fn : Callable [[_DeviceT ], BinarySensorDeviceClass | None ] = lambda _ : None
4746 should_report : Callable [[_DeviceT ], bool ] = lambda _ : True
4847 device_class : Optional [BinarySensorDeviceClass ] = None
48+ # Optional override for is_on — use when the entity key differs from the device property
49+ value_fn : Callable | None = None
50+
4951
5052class PetLibroBinarySensorEntity (PetLibroEntity [_DeviceT ], BinarySensorEntity ):
51- """PETLIBRO sensor entity."""
53+ """PETLIBRO binary sensor entity."""
5254
5355 entity_description : PetLibroBinarySensorEntityDescription [_DeviceT ]
5456
@@ -60,34 +62,79 @@ def device_class(self) -> BinarySensorDeviceClass | None:
6062 @property
6163 def is_on (self ) -> bool :
6264 """Return True if the binary sensor is on."""
63- # Check if the binary sensor should report its state
6465 if not self .entity_description .should_report (self .device ):
6566 return False
6667
67- # Retrieve the state using getattr, defaulting to None if the attribute is missing
68+ # Use value_fn override when the key doesn't match a device property directly
69+ if self .entity_description .value_fn is not None :
70+ return bool (self .entity_description .value_fn (self .device ))
71+
6872 state = getattr (self .device , self .entity_description .key , None )
6973
70- # Check if this is the first time the sensor is being refreshed by checking if _last_state exists
7174 last_state = getattr (self , '_last_state' , None )
72- initial_log_done = getattr (self , '_initial_log_done' , False ) # Track if we've logged the initial state
75+ initial_log_done = getattr (self , '_initial_log_done' , False )
7376
74- # If this is the initial boot, don't log anything but track the state
7577 if not initial_log_done :
76- # Mark the initial log as done without logging
77- self ._initial_log_done = True
78+ self ._initial_log_done = True
7879 elif last_state != state :
79- # Log state changes: log online with INFO and offline with WARNING
8080 if state :
8181 _LOGGER .info (f"Device { self .device .name } is online." )
8282 else :
8383 _LOGGER .warning (f"Device { self .device .name } is offline." )
8484
85- # Store the last state for future comparisons
8685 self ._last_state = state
87-
88- # Return the state, ensuring it's a boolean
8986 return bool (state )
9087
88+ @property
89+ def extra_state_attributes (self ):
90+ """Return entity specific state attributes."""
91+ match self .key :
92+ case "feeding_plan_state" :
93+ # Today's feeding plan events with formatted amounts
94+ today_data = getattr (self .device , "feeding_plan_today_data" , {})
95+ plans = today_data .get ("plans" , []) if isinstance (today_data , dict ) else []
96+ if not plans :
97+ return {}
98+ plan_data = getattr (self .device , "feeding_plan_data" , {})
99+ conv = getattr (self .device , "feed_conv_factor" , 1 )
100+ unit = self .member .feedUnitType
101+ weight = unit if unit in (Unit .GRAMS , Unit .OUNCES ) else Unit .GRAMS
102+ volume = unit if unit in (Unit .MILLILITERS , Unit .CUPS ) else Unit .MILLILITERS
103+ return {
104+ plan_data .get (str (plan ["planId" ]), {}).get ("label" ) or f"plan_{ plan .get ('index' , plan ['planId' ])} " : {
105+ "time" : plan .get ("time" ),
106+ "amount (weight)" : f"{ Unit .convert_feed (plan .get ('grainNum' , 0 ) * conv , None , weight , True )} { weight .symbol } " ,
107+ "amount (volume)" : f"{ Unit .convert_feed (plan .get ('grainNum' , 0 ) * conv , None , volume , True )} { volume .symbol } " ,
108+ "state" : {1 : "Pending" , 2 : "Skipped" , 3 : "Completed" , 4 : "Skipped, Time Passed" }.get (plan .get ("state" ), "Unknown" ),
109+ "repeat" : plan .get ("repeat" ),
110+ "planID" : plan .get ("planId" ),
111+ }
112+ for plan in plans
113+ } or {}
114+ case "feeding_schedule" :
115+ # Full recurring schedule with formatted amounts
116+ plans = getattr (self .device , "feeding_plan_data" , {})
117+ if not plans :
118+ return {}
119+ conv = getattr (self .device , "feed_conv_factor" , 1 )
120+ unit = self .member .feedUnitType
121+ weight = unit if unit in (Unit .GRAMS , Unit .OUNCES ) else Unit .GRAMS
122+ volume = unit if unit in (Unit .MILLILITERS , Unit .CUPS ) else Unit .MILLILITERS
123+ return {
124+ plan .get ("label" ) or f"plan_{ plan_id } " : {
125+ "planID" : int (plan_id ),
126+ "time" : plan .get ("executionTime" ),
127+ "amount (weight)" : f"{ Unit .convert_feed (plan .get ('grainNum' , 0 ) * conv , None , weight , True )} { weight .symbol } " ,
128+ "amount (volume)" : f"{ Unit .convert_feed (plan .get ('grainNum' , 0 ) * conv , None , volume , True )} { volume .symbol } " ,
129+ "enabled" : plan .get ("enable" , False ),
130+ "repeat_days" : plan .get ("repeatDay" , "[]" ),
131+ "sound" : plan .get ("enableAudio" , False ),
132+ }
133+ for plan_id , plan in plans .items ()
134+ } or {}
135+ return {}
136+
137+
91138DEVICE_BINARY_SENSOR_MAP : dict [type [Device ], list [PetLibroBinarySensorEntityDescription ]] = {
92139 Feeder : [
93140 ],
@@ -138,6 +185,21 @@ def is_on(self) -> bool:
138185 should_report = lambda device : device .light_switch is not None ,
139186 name = "Indicator"
140187 ),
188+ PetLibroBinarySensorEntityDescription [AirSmartFeeder ](
189+ key = "feeding_plan_state" ,
190+ translation_key = "feeding_plan_state" ,
191+ icon = "mdi:calendar-check" ,
192+ should_report = lambda device : device .feeding_plan_state is not None ,
193+ name = "Today's Feeding Schedule"
194+ ),
195+ PetLibroBinarySensorEntityDescription [AirSmartFeeder ](
196+ key = "feeding_schedule" ,
197+ translation_key = "feeding_schedule" ,
198+ icon = "mdi:calendar-clock" ,
199+ should_report = lambda device : bool (getattr (device , "feeding_plan_data" , {})),
200+ value_fn = lambda device : device .feeding_plan_state ,
201+ name = "Feeding Schedule"
202+ ),
141203 ],
142204 GranarySmartFeeder : [
143205 PetLibroBinarySensorEntityDescription [GranarySmartFeeder ](
@@ -186,6 +248,21 @@ def is_on(self) -> bool:
186248 should_report = lambda device : device .light_switch is not None ,
187249 name = "Indicator"
188250 ),
251+ PetLibroBinarySensorEntityDescription [GranarySmartFeeder ](
252+ key = "feeding_plan_state" ,
253+ translation_key = "feeding_plan_state" ,
254+ icon = "mdi:calendar-check" ,
255+ should_report = lambda device : device .feeding_plan_state is not None ,
256+ name = "Today's Feeding Schedule"
257+ ),
258+ PetLibroBinarySensorEntityDescription [GranarySmartFeeder ](
259+ key = "feeding_schedule" ,
260+ translation_key = "feeding_schedule" ,
261+ icon = "mdi:calendar-clock" ,
262+ should_report = lambda device : bool (getattr (device , "feeding_plan_data" , {})),
263+ value_fn = lambda device : device .feeding_plan_state ,
264+ name = "Feeding Schedule"
265+ ),
189266 ],
190267 GranarySmartCameraFeeder : [
191268 PetLibroBinarySensorEntityDescription [GranarySmartCameraFeeder ](
@@ -234,6 +311,21 @@ def is_on(self) -> bool:
234311 should_report = lambda device : device .light_switch is not None ,
235312 name = "Indicator"
236313 ),
314+ PetLibroBinarySensorEntityDescription [GranarySmartCameraFeeder ](
315+ key = "feeding_plan_state" ,
316+ translation_key = "feeding_plan_state" ,
317+ icon = "mdi:calendar-check" ,
318+ should_report = lambda device : device .feeding_plan_state is not None ,
319+ name = "Today's Feeding Schedule"
320+ ),
321+ PetLibroBinarySensorEntityDescription [GranarySmartCameraFeeder ](
322+ key = "feeding_schedule" ,
323+ translation_key = "feeding_schedule" ,
324+ icon = "mdi:calendar-clock" ,
325+ should_report = lambda device : bool (getattr (device , "feeding_plan_data" , {})),
326+ value_fn = lambda device : device .feeding_plan_state ,
327+ name = "Feeding Schedule"
328+ ),
237329 ],
238330 OneRFIDSmartFeeder : [
239331 PetLibroBinarySensorEntityDescription [OneRFIDSmartFeeder ](
@@ -313,6 +405,21 @@ def is_on(self) -> bool:
313405 should_report = lambda device : device .display_switch is not None ,
314406 name = "Display Status"
315407 ),
408+ PetLibroBinarySensorEntityDescription [OneRFIDSmartFeeder ](
409+ key = "feeding_plan_state" ,
410+ translation_key = "feeding_plan_state" ,
411+ icon = "mdi:calendar-check" ,
412+ should_report = lambda device : device .feeding_plan_state is not None ,
413+ name = "Today's Feeding Schedule"
414+ ),
415+ PetLibroBinarySensorEntityDescription [OneRFIDSmartFeeder ](
416+ key = "feeding_schedule" ,
417+ translation_key = "feeding_schedule" ,
418+ icon = "mdi:calendar-clock" ,
419+ should_report = lambda device : bool (getattr (device , "feeding_plan_data" , {})),
420+ value_fn = lambda device : device .feeding_plan_state ,
421+ name = "Feeding Schedule"
422+ ),
316423 ],
317424 PolarWetFoodFeeder : [
318425 PetLibroBinarySensorEntityDescription [PolarWetFoodFeeder ](
@@ -361,6 +468,13 @@ def is_on(self) -> bool:
361468 should_report = lambda device : device .light_switch is not None ,
362469 name = "Indicator"
363470 ),
471+ PetLibroBinarySensorEntityDescription [PolarWetFoodFeeder ](
472+ key = "feeding_plan_state" ,
473+ translation_key = "feeding_plan_state" ,
474+ icon = "mdi:calendar-check" ,
475+ should_report = lambda device : device .feeding_plan_state is not None ,
476+ name = "Feeding Plan"
477+ ),
364478 ],
365479 SpaceSmartFeeder : [
366480 PetLibroBinarySensorEntityDescription [SpaceSmartFeeder ](
@@ -432,6 +546,21 @@ def is_on(self) -> bool:
432546 should_report = lambda device : device .light_switch is not None ,
433547 name = "Indicator"
434548 ),
549+ PetLibroBinarySensorEntityDescription [SpaceSmartFeeder ](
550+ key = "feeding_plan_state" ,
551+ translation_key = "feeding_plan_state" ,
552+ icon = "mdi:calendar-check" ,
553+ should_report = lambda device : device .feeding_plan_state is not None ,
554+ name = "Today's Feeding Schedule"
555+ ),
556+ PetLibroBinarySensorEntityDescription [SpaceSmartFeeder ](
557+ key = "feeding_schedule" ,
558+ translation_key = "feeding_schedule" ,
559+ icon = "mdi:calendar-clock" ,
560+ should_report = lambda device : bool (getattr (device , "feeding_plan_data" , {})),
561+ value_fn = lambda device : device .feeding_plan_state ,
562+ name = "Feeding Schedule"
563+ ),
435564 ],
436565 DockstreamSmartFountain : [
437566 PetLibroBinarySensorEntityDescription [DockstreamSmartFountain ](
@@ -605,34 +734,30 @@ def is_on(self) -> bool:
605734 ],
606735}
607736
737+
608738async def async_setup_entry (
609739 hass : HomeAssistant ,
610- entry : ConfigEntry , # Use ConfigEntry
740+ entry : ConfigEntry ,
611741 async_add_entities : AddEntitiesCallback ,
612742) -> None :
613743 """Set up PETLIBRO binary sensors using config entry."""
614- # Retrieve the hub from hass.data that was set up in __init__.py
615744 hub : PetLibroHub = hass .data [DOMAIN ].get (entry .entry_id )
616745
617746 if not hub :
618747 _LOGGER .error ("Hub not found for entry: %s" , entry .entry_id )
619748 return
620749
621- # Ensure that the devices are loaded (if load_devices is not already called elsewhere)
622750 if not hub .devices :
623751 _LOGGER .warning ("No devices found in hub during binary sensor setup." )
624752 return
625753
626- # Log the contents of the hub data for debugging
627754 _LOGGER .debug ("Hub data: %s" , hub )
628-
629- devices = hub .devices # Devices should already be loaded in the hub
755+ devices = hub .devices
630756 _LOGGER .debug ("Devices in hub: %s" , devices )
631757
632- # Create binary sensor entities for each device based on the binary sensor map
633758 entities = [
634759 PetLibroBinarySensorEntity (device , hub , description )
635- for device in devices .values () # Iterate through devices from the hub
760+ for device in devices .values ()
636761 for device_type , entity_descriptions in DEVICE_BINARY_SENSOR_MAP .items ()
637762 if isinstance (device , device_type )
638763 for description in entity_descriptions
@@ -641,10 +766,11 @@ async def async_setup_entry(
641766 if not entities :
642767 _LOGGER .warning ("No binary sensors added, entities list is empty!" )
643768 else :
644- # Log the number of entities and their details
645769 _LOGGER .debug ("Adding %d PetLibro binary sensors" , len (entities ))
646770 for entity in entities :
647- _LOGGER .debug ("Adding binary sensor entity: %s for device %s" , entity .entity_description .name , entity .device .name )
648-
649- # Add binary sensor entities to Home Assistant
650- async_add_entities (entities )
771+ _LOGGER .debug (
772+ "Adding binary sensor entity: %s for device %s" ,
773+ entity .entity_description .name ,
774+ entity .device .name ,
775+ )
776+ async_add_entities (entities )
0 commit comments