|
1 | 1 | """Support for PETLIBRO binary sensors.""" |
2 | 2 | from __future__ import annotations |
| 3 | +import json |
3 | 4 | from .api import make_api_call |
4 | 5 | import aiohttp |
5 | 6 | from aiohttp import ClientSession, ClientError |
|
9 | 10 | from typing import Optional |
10 | 11 | import logging |
11 | 12 | from .const import DOMAIN, Unit, APIKey as API, VALID_UNIT_TYPES |
| 13 | +from homeassistant.util.dt import now as ha_now |
12 | 14 | from homeassistant.components.binary_sensor import ( |
13 | 15 | BinarySensorEntity, |
14 | 16 | BinarySensorEntityDescription, |
@@ -89,61 +91,76 @@ def is_on(self) -> bool: |
89 | 91 | def extra_state_attributes(self): |
90 | 92 | """Return entity specific state attributes.""" |
91 | 93 | match self.key: |
92 | | - case "feeding_plan_state": |
93 | | - # Today's feeding schedule events with amounts in all units |
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 | | - return { |
101 | | - plan_data.get(str(plan["planId"]), {}).get("label") or f"plan_{plan.get('index', plan['planId'])}": { |
102 | | - "planID": plan.get("planId"), |
103 | | - "time": plan.get("time"), |
104 | | - **{ |
105 | | - f"amount_{unit.symbol.lower()}": Unit.convert_feed(plan.get("grainNum", 0) * conv, None, unit, True) |
106 | | - for unit in VALID_UNIT_TYPES[API.FEED_UNIT] if unit |
107 | | - }, |
108 | | - "amount_raw": plan.get("grainNum", 0), |
109 | | - "feed_conv_factor": conv, |
110 | | - "enabled": plan_data.get(str(plan["planId"]), {}).get("enable", False), |
111 | | - "repeat_days": plan_data.get(str(plan["planId"]), {}).get("repeatDay", "[]"), |
112 | | - "sound": plan_data.get(str(plan["planId"]), {}).get("enableAudio", False), |
113 | | - "feed_state": {1: "Pending", 2: "Skipped", 3: "Completed", 4: "Skipped, Time Passed"}.get(plan.get("state"), "Unknown"), |
114 | | - "repeat": plan.get("repeat"), |
115 | | - } |
116 | | - for plan in plans |
117 | | - } or {} |
118 | 94 | case "feeding_schedule": |
119 | | - # Full recurring schedule with amounts in all units + today's state |
| 95 | + # Full recurring schedule as a list with all unit amounts + today's state |
120 | 96 | plans = getattr(self.device, "feeding_plan_data", {}) |
121 | | - if not plans: |
122 | | - return {} |
123 | 97 | conv = getattr(self.device, "feed_conv_factor", 1) |
124 | | - # Build lookup of today's feed states by planId |
| 98 | + today_weekday = ha_now().isoweekday() # 1=Mon … 7=Sun, matches repeatDay format |
| 99 | + # Build lookup of today's feed states and data by planId |
125 | 100 | today_data = getattr(self.device, "feeding_plan_today_data", {}) |
126 | 101 | today_plans = today_data.get("plans", []) if isinstance(today_data, dict) else [] |
127 | 102 | today_state_map = {p["planId"]: p.get("state") for p in today_plans} |
128 | | - return { |
129 | | - plan.get("label") or f"plan_{plan_id}": { |
130 | | - "planID": int(plan_id), |
| 103 | + state_map = {1: "pending", 2: "to_be_skipped", 3: "dispensed", 4: "skipped", 5: "state_5", 6: "unknown"} |
| 104 | + schedule = [] |
| 105 | + seen_plan_ids = set() |
| 106 | + # First pass: plans from the full schedule list |
| 107 | + for plan_id, plan in plans.items(): |
| 108 | + raw_repeat = plan.get("repeatDay", "[]") |
| 109 | + try: |
| 110 | + repeat_list = json.loads(raw_repeat) if isinstance(raw_repeat, str) else raw_repeat |
| 111 | + except (json.JSONDecodeError, TypeError): |
| 112 | + repeat_list = [] |
| 113 | + pid = int(plan_id) |
| 114 | + seen_plan_ids.add(pid) |
| 115 | + in_today = pid in today_state_map |
| 116 | + today = in_today or (today_weekday in repeat_list) |
| 117 | + if in_today: |
| 118 | + raw_state = today_state_map.get(pid) |
| 119 | + state = state_map.get(raw_state, "unknown") |
| 120 | + else: |
| 121 | + state = "pending" |
| 122 | + schedule.append({ |
| 123 | + "id": pid, |
| 124 | + "label": plan.get("label", ""), |
131 | 125 | "time": plan.get("executionTime"), |
132 | 126 | **{ |
133 | 127 | f"amount_{unit.symbol.lower()}": Unit.convert_feed(plan.get("grainNum", 0) * conv, None, unit, True) |
134 | 128 | for unit in VALID_UNIT_TYPES[API.FEED_UNIT] if unit |
135 | 129 | }, |
136 | 130 | "amount_raw": plan.get("grainNum", 0), |
137 | | - "feed_conv_factor": conv, |
138 | 131 | "enabled": plan.get("enable", False), |
139 | | - "repeat_days": plan.get("repeatDay", "[]"), |
| 132 | + "recurring": bool(repeat_list), |
| 133 | + "repeat_days": repeat_list, |
140 | 134 | "sound": plan.get("enableAudio", False), |
141 | | - "feed_state": {1: "Pending", 2: "Skipped", 3: "Completed", 4: "Skipped, Time Passed"}.get( |
142 | | - today_state_map.get(int(plan_id)), "Not Scheduled Today" |
143 | | - ), |
144 | | - } |
145 | | - for plan_id, plan in plans.items() |
146 | | - } or {} |
| 135 | + "today": today, |
| 136 | + "state": state, |
| 137 | + }) |
| 138 | + # Second pass: today-only plans not in the full schedule (e.g. non-recurring one-time feeds) |
| 139 | + for tp in today_plans: |
| 140 | + pid = tp["planId"] |
| 141 | + if pid in seen_plan_ids: |
| 142 | + continue |
| 143 | + raw_state = tp.get("state") |
| 144 | + state = state_map.get(raw_state, "unknown") |
| 145 | + schedule.append({ |
| 146 | + "id": pid, |
| 147 | + "label": "", |
| 148 | + "time": tp.get("time"), |
| 149 | + **{ |
| 150 | + f"amount_{unit.symbol.lower()}": Unit.convert_feed(tp.get("grainNum", 0) * conv, None, unit, True) |
| 151 | + for unit in VALID_UNIT_TYPES[API.FEED_UNIT] if unit |
| 152 | + }, |
| 153 | + "amount_raw": tp.get("grainNum", 0), |
| 154 | + "enabled": True, |
| 155 | + "recurring": False, |
| 156 | + "repeat_days": [], |
| 157 | + "sound": False, |
| 158 | + "today": True, |
| 159 | + "state": state, |
| 160 | + }) |
| 161 | + if not schedule: |
| 162 | + return {"feed_conv_factor": conv, "schedule": []} |
| 163 | + return {"feed_conv_factor": conv, "schedule": schedule} |
147 | 164 | return {} |
148 | 165 |
|
149 | 166 |
|
@@ -198,19 +215,18 @@ def extra_state_attributes(self): |
198 | 215 | name="Indicator" |
199 | 216 | ), |
200 | 217 | PetLibroBinarySensorEntityDescription[AirSmartFeeder]( |
201 | | - key="feeding_plan_state", |
202 | | - translation_key="feeding_plan_state", |
| 218 | + key="today_feeding_schedule", |
| 219 | + translation_key="today_feeding_schedule", |
203 | 220 | icon="mdi:calendar-check", |
204 | | - device_class=BinarySensorDeviceClass.RUNNING, |
205 | | - should_report=lambda device: device.feeding_plan_state is not None, |
| 221 | + should_report=lambda device: device.feeding_plan_today_data is not None, |
| 222 | + value_fn=lambda device: not device.today_feeding_plan_state, |
206 | 223 | name="Today's Feeding Schedule" |
207 | 224 | ), |
208 | 225 | PetLibroBinarySensorEntityDescription[AirSmartFeeder]( |
209 | 226 | key="feeding_schedule", |
210 | 227 | translation_key="feeding_schedule", |
211 | 228 | icon="mdi:calendar-clock", |
212 | | - device_class=BinarySensorDeviceClass.RUNNING, |
213 | | - should_report=lambda device: bool(getattr(device, "feeding_plan_data", {})), |
| 229 | + should_report=lambda device: device.feeding_plan_state is not None, |
214 | 230 | value_fn=lambda device: device.feeding_plan_state, |
215 | 231 | name="Feeding Schedule" |
216 | 232 | ), |
@@ -263,19 +279,18 @@ def extra_state_attributes(self): |
263 | 279 | name="Indicator" |
264 | 280 | ), |
265 | 281 | PetLibroBinarySensorEntityDescription[GranarySmartFeeder]( |
266 | | - key="feeding_plan_state", |
267 | | - translation_key="feeding_plan_state", |
| 282 | + key="today_feeding_schedule", |
| 283 | + translation_key="today_feeding_schedule", |
268 | 284 | icon="mdi:calendar-check", |
269 | | - device_class=BinarySensorDeviceClass.RUNNING, |
270 | | - should_report=lambda device: device.feeding_plan_state is not None, |
| 285 | + should_report=lambda device: device.feeding_plan_today_data is not None, |
| 286 | + value_fn=lambda device: not device.today_feeding_plan_state, |
271 | 287 | name="Today's Feeding Schedule" |
272 | 288 | ), |
273 | 289 | PetLibroBinarySensorEntityDescription[GranarySmartFeeder]( |
274 | 290 | key="feeding_schedule", |
275 | 291 | translation_key="feeding_schedule", |
276 | 292 | icon="mdi:calendar-clock", |
277 | | - device_class=BinarySensorDeviceClass.RUNNING, |
278 | | - should_report=lambda device: bool(getattr(device, "feeding_plan_data", {})), |
| 293 | + should_report=lambda device: device.feeding_plan_state is not None, |
279 | 294 | value_fn=lambda device: device.feeding_plan_state, |
280 | 295 | name="Feeding Schedule" |
281 | 296 | ), |
@@ -328,19 +343,18 @@ def extra_state_attributes(self): |
328 | 343 | name="Indicator" |
329 | 344 | ), |
330 | 345 | PetLibroBinarySensorEntityDescription[GranarySmartCameraFeeder]( |
331 | | - key="feeding_plan_state", |
332 | | - translation_key="feeding_plan_state", |
| 346 | + key="today_feeding_schedule", |
| 347 | + translation_key="today_feeding_schedule", |
333 | 348 | icon="mdi:calendar-check", |
334 | | - device_class=BinarySensorDeviceClass.RUNNING, |
335 | | - should_report=lambda device: device.feeding_plan_state is not None, |
| 349 | + should_report=lambda device: device.feeding_plan_today_data is not None, |
| 350 | + value_fn=lambda device: not device.today_feeding_plan_state, |
336 | 351 | name="Today's Feeding Schedule" |
337 | 352 | ), |
338 | 353 | PetLibroBinarySensorEntityDescription[GranarySmartCameraFeeder]( |
339 | 354 | key="feeding_schedule", |
340 | 355 | translation_key="feeding_schedule", |
341 | 356 | icon="mdi:calendar-clock", |
342 | | - device_class=BinarySensorDeviceClass.RUNNING, |
343 | | - should_report=lambda device: bool(getattr(device, "feeding_plan_data", {})), |
| 357 | + should_report=lambda device: device.feeding_plan_state is not None, |
344 | 358 | value_fn=lambda device: device.feeding_plan_state, |
345 | 359 | name="Feeding Schedule" |
346 | 360 | ), |
@@ -424,19 +438,18 @@ def extra_state_attributes(self): |
424 | 438 | name="Display Status" |
425 | 439 | ), |
426 | 440 | PetLibroBinarySensorEntityDescription[OneRFIDSmartFeeder]( |
427 | | - key="feeding_plan_state", |
428 | | - translation_key="feeding_plan_state", |
| 441 | + key="today_feeding_schedule", |
| 442 | + translation_key="today_feeding_schedule", |
429 | 443 | icon="mdi:calendar-check", |
430 | | - device_class=BinarySensorDeviceClass.RUNNING, |
431 | | - should_report=lambda device: device.feeding_plan_state is not None, |
| 444 | + should_report=lambda device: device.feeding_plan_today_data is not None, |
| 445 | + value_fn=lambda device: not device.today_feeding_plan_state, |
432 | 446 | name="Today's Feeding Schedule" |
433 | 447 | ), |
434 | 448 | PetLibroBinarySensorEntityDescription[OneRFIDSmartFeeder]( |
435 | 449 | key="feeding_schedule", |
436 | 450 | translation_key="feeding_schedule", |
437 | 451 | icon="mdi:calendar-clock", |
438 | | - device_class=BinarySensorDeviceClass.RUNNING, |
439 | | - should_report=lambda device: bool(getattr(device, "feeding_plan_data", {})), |
| 452 | + should_report=lambda device: device.feeding_plan_state is not None, |
440 | 453 | value_fn=lambda device: device.feeding_plan_state, |
441 | 454 | name="Feeding Schedule" |
442 | 455 | ), |
@@ -492,8 +505,8 @@ def extra_state_attributes(self): |
492 | 505 | key="feeding_plan_state", |
493 | 506 | translation_key="feeding_plan_state", |
494 | 507 | icon="mdi:calendar-check", |
495 | | - device_class=BinarySensorDeviceClass.RUNNING, |
496 | 508 | should_report=lambda device: device.feeding_plan_state is not None, |
| 509 | + value_fn=lambda device: not device.feeding_plan_today_data.get("allSkipped", False), |
497 | 510 | name="Today's Feeding Schedule" |
498 | 511 | ), |
499 | 512 | ], |
@@ -568,19 +581,18 @@ def extra_state_attributes(self): |
568 | 581 | name="Indicator" |
569 | 582 | ), |
570 | 583 | PetLibroBinarySensorEntityDescription[SpaceSmartFeeder]( |
571 | | - key="feeding_plan_state", |
572 | | - translation_key="feeding_plan_state", |
| 584 | + key="today_feeding_schedule", |
| 585 | + translation_key="today_feeding_schedule", |
573 | 586 | icon="mdi:calendar-check", |
574 | | - device_class=BinarySensorDeviceClass.RUNNING, |
575 | | - should_report=lambda device: device.feeding_plan_state is not None, |
| 587 | + should_report=lambda device: device.feeding_plan_today_data is not None, |
| 588 | + value_fn=lambda device: not device.today_feeding_plan_state, |
576 | 589 | name="Today's Feeding Schedule" |
577 | 590 | ), |
578 | 591 | PetLibroBinarySensorEntityDescription[SpaceSmartFeeder]( |
579 | 592 | key="feeding_schedule", |
580 | 593 | translation_key="feeding_schedule", |
581 | 594 | icon="mdi:calendar-clock", |
582 | | - device_class=BinarySensorDeviceClass.RUNNING, |
583 | | - should_report=lambda device: bool(getattr(device, "feeding_plan_data", {})), |
| 595 | + should_report=lambda device: device.feeding_plan_state is not None, |
584 | 596 | value_fn=lambda device: device.feeding_plan_state, |
585 | 597 | name="Feeding Schedule" |
586 | 598 | ), |
|
0 commit comments