|
12 | 12 | from homeassistant.helpers.entity import EntityCategory |
13 | 13 | from homeassistant.helpers.entity_platform import AddEntitiesCallback |
14 | 14 | from pymammotion.data.model.hash_list import Plan |
| 15 | +from pymammotion.data.model.pool_state import PoolPlan |
15 | 16 | from pymammotion.transport.base import TransportType |
16 | 17 | from pymammotion.utility.device_type import DeviceType |
17 | 18 |
|
@@ -48,6 +49,20 @@ class MammotionSpinoButtonEntityDescription(ButtonEntityDescription): |
48 | 49 | press_fn: Callable[[MammotionSpinoCoordinator], Awaitable[None]] |
49 | 50 |
|
50 | 51 |
|
| 52 | +@dataclass(frozen=True, kw_only=True) |
| 53 | +class MammotionSpinoTaskButtonEntityDescription(ButtonEntityDescription): |
| 54 | + """Describes a dynamic per-schedule Spino task button entity. |
| 55 | +
|
| 56 | + Mirror of :class:`MammotionTaskButtonSensorEntityDescription` for the |
| 57 | + Spino pool cleaner. Spino plans are keyed by a 64-bit ``jobid``; we |
| 58 | + stringify it for ``key`` / ``unique_id`` so it survives the HA entity |
| 59 | + registry's string-only constraint. |
| 60 | + """ |
| 61 | + |
| 62 | + jobid: int |
| 63 | + press_fn: Callable[[MammotionSpinoCoordinator, int], Awaitable[None]] |
| 64 | + |
| 65 | + |
51 | 66 | SPINO_BUTTON_SENSORS: tuple[MammotionSpinoButtonEntityDescription, ...] = ( |
52 | 67 | MammotionSpinoButtonEntityDescription( |
53 | 68 | key="spino_fetch_map", |
@@ -194,6 +209,22 @@ async def async_setup_entry( |
194 | 209 | for entity_description in SPINO_BUTTON_SENSORS |
195 | 210 | ) |
196 | 211 |
|
| 212 | + # Dynamic per-schedule task buttons — mirrors the mower setup but |
| 213 | + # keyed by Spino ``jobid`` (int). Primary purpose: provide an |
| 214 | + # addressable HA entity so the rename / enable / delete / copy |
| 215 | + # services can target a specific schedule via entity_id. |
| 216 | + added_spino_tasks: set[int] = set() |
| 217 | + spino_task_entities_by_id: dict[int, MammotionSpinoTaskButtonEntity] = {} |
| 218 | + update_spino_tasks = partial( |
| 219 | + async_add_spino_task_entities, |
| 220 | + spino.coordinator, |
| 221 | + added_spino_tasks, |
| 222 | + spino_task_entities_by_id, |
| 223 | + async_add_entities, |
| 224 | + ) |
| 225 | + update_spino_tasks() |
| 226 | + spino.coordinator.async_add_listener(update_spino_tasks) |
| 227 | + |
197 | 228 |
|
198 | 229 | class MammotionButtonSensorEntity(MammotionBaseEntity, ButtonEntity): |
199 | 230 | """Mammotion button sensor entity.""" |
@@ -348,6 +379,134 @@ def async_remove_entities( |
348 | 379 | registry.async_remove(entity_id) |
349 | 380 |
|
350 | 381 |
|
| 382 | +class MammotionSpinoTaskButtonEntity(MammotionBaseSpinoEntity, ButtonEntity): |
| 383 | + """Per-schedule Spino task button. |
| 384 | +
|
| 385 | + Exists primarily so the schedule-modify services (rename / enable / |
| 386 | + disable / delete / copy / edit) can target an addressable HA entity |
| 387 | + via ``entity_id``. |
| 388 | +
|
| 389 | + Spino does not expose a "start this schedule now" command in the |
| 390 | + proto we have today, so the press triggers a refresh of the whole |
| 391 | + schedule list — a useful default and the closest analogue to the |
| 392 | + mower's ``start_task`` press semantics. |
| 393 | + """ |
| 394 | + |
| 395 | + entity_description: MammotionSpinoTaskButtonEntityDescription |
| 396 | + _attr_has_entity_name = True |
| 397 | + _attr_entity_category = EntityCategory.CONFIG |
| 398 | + |
| 399 | + def __init__( |
| 400 | + self, |
| 401 | + coordinator: MammotionSpinoCoordinator, |
| 402 | + entity_description: MammotionSpinoTaskButtonEntityDescription, |
| 403 | + ) -> None: |
| 404 | + """Initialize the Spino task button entity.""" |
| 405 | + super().__init__(coordinator, entity_description.key) |
| 406 | + self.entity_description = entity_description |
| 407 | + self._attr_translation_key = "spino_task" |
| 408 | + # ``task_id`` mirrors the mower entity's attribute name so the |
| 409 | + # service resolution helper can read it generically. Stored as a |
| 410 | + # string for HA-attribute compatibility; the int form is exposed |
| 411 | + # via ``jobid`` for callers that prefer it. |
| 412 | + self._attr_extra_state_attributes = { |
| 413 | + "task_id": str(entity_description.jobid), |
| 414 | + "jobid": entity_description.jobid, |
| 415 | + } |
| 416 | + |
| 417 | + def update_name(self, new_name: str) -> None: |
| 418 | + """Update the display name when the plan's jobname changes.""" |
| 419 | + self.entity_description = dataclass_replace( |
| 420 | + self.entity_description, |
| 421 | + name=new_name, |
| 422 | + translation_placeholders={"name": new_name}, |
| 423 | + ) |
| 424 | + if self.hass is not None: |
| 425 | + self.async_write_ha_state() |
| 426 | + |
| 427 | + async def async_press(self) -> None: |
| 428 | + """Refresh all Spino schedules from the device. |
| 429 | +
|
| 430 | + Spino has no per-schedule "execute now" command, so the press |
| 431 | + action triggers a full schedule re-sync (matching the spirit of |
| 432 | + the mower task button while staying within the proto we support). |
| 433 | + """ |
| 434 | + await self.entity_description.press_fn( |
| 435 | + self.coordinator, self.entity_description.jobid |
| 436 | + ) |
| 437 | + |
| 438 | + |
| 439 | +def _update_spino_task_names( |
| 440 | + coordinator: MammotionSpinoCoordinator, |
| 441 | + added_tasks: set[int], |
| 442 | + task_entities_by_id: dict[int, MammotionSpinoTaskButtonEntity], |
| 443 | +) -> None: |
| 444 | + """Rename Spino task button entities whose plan jobname has changed.""" |
| 445 | + for jobid in added_tasks: |
| 446 | + plan: PoolPlan | None = coordinator.data.plans.get(jobid) |
| 447 | + if plan is None: |
| 448 | + continue |
| 449 | + entity = task_entities_by_id.get(jobid) |
| 450 | + if entity is None: |
| 451 | + continue |
| 452 | + if entity.entity_description.name != plan.jobname: |
| 453 | + entity.update_name(plan.jobname) |
| 454 | + |
| 455 | + |
| 456 | +@callback |
| 457 | +def async_add_spino_task_entities( |
| 458 | + coordinator: MammotionSpinoCoordinator, |
| 459 | + added_tasks: set[int], |
| 460 | + task_entities_by_id: dict[int, MammotionSpinoTaskButtonEntity], |
| 461 | + async_add_entities: AddEntitiesCallback, |
| 462 | +) -> None: |
| 463 | + """Sync the per-schedule Spino task buttons against ``coordinator.data.plans``. |
| 464 | +
|
| 465 | + Mirror of :func:`async_add_task_entities` for the Spino path — adds a |
| 466 | + button when a new ``jobid`` appears, renames when ``jobname`` changes, |
| 467 | + and removes via the entity registry when a plan disappears. |
| 468 | + """ |
| 469 | + if coordinator.data is None: |
| 470 | + return |
| 471 | + |
| 472 | + button_entities: list[MammotionSpinoTaskButtonEntity] = [] |
| 473 | + current = set(coordinator.data.plans.keys()) |
| 474 | + new_tasks = current - added_tasks |
| 475 | + |
| 476 | + for jobid in new_tasks: |
| 477 | + plan = coordinator.data.plans.get(jobid) |
| 478 | + if plan is None: |
| 479 | + continue |
| 480 | + desc = MammotionSpinoTaskButtonEntityDescription( |
| 481 | + key=str(jobid), |
| 482 | + jobid=jobid, |
| 483 | + name=plan.jobname, |
| 484 | + translation_placeholders={"name": plan.jobname}, |
| 485 | + press_fn=lambda coord, _jobid: coord.async_refresh_spino_tasks(), |
| 486 | + ) |
| 487 | + entity = MammotionSpinoTaskButtonEntity(coordinator, desc) |
| 488 | + button_entities.append(entity) |
| 489 | + task_entities_by_id[jobid] = entity |
| 490 | + added_tasks.add(jobid) |
| 491 | + |
| 492 | + _update_spino_task_names(coordinator, added_tasks, task_entities_by_id) |
| 493 | + |
| 494 | + old_tasks = added_tasks - current |
| 495 | + if old_tasks: |
| 496 | + registry = er.async_get(coordinator.hass) |
| 497 | + for jobid in old_tasks: |
| 498 | + entity_id = registry.async_get_entity_id( |
| 499 | + BUTTON_DOMAIN, DOMAIN, f"{coordinator.device_name}_{jobid}" |
| 500 | + ) |
| 501 | + if entity_id: |
| 502 | + registry.async_remove(entity_id) |
| 503 | + task_entities_by_id.pop(jobid, None) |
| 504 | + added_tasks -= old_tasks |
| 505 | + |
| 506 | + if button_entities: |
| 507 | + async_add_entities(button_entities) |
| 508 | + |
| 509 | + |
351 | 510 | class MammotionSpinoButtonEntity(MammotionBaseSpinoEntity, ButtonEntity): |
352 | 511 | """Mammotion Spino pool cleaner button entity.""" |
353 | 512 |
|
|
0 commit comments