1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-02 08:26:41 +01:00

Updates for Casper glow Integraiton - Add Buttons (#166083)

Signed-off-by: Mike O'Driscoll <mike@unusedbytes.ca>
This commit is contained in:
Mike O'Driscoll
2026-03-25 09:32:47 -04:00
committed by GitHub
parent 56962ff907
commit 3ae6f8e7a0
8 changed files with 283 additions and 12 deletions

View File

@@ -11,7 +11,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.LIGHT]
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT]
async def async_setup_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -> bool:

View File

@@ -0,0 +1,73 @@
"""Casper Glow integration button platform."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from pycasperglow import CasperGlow
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
from .entity import CasperGlowEntity
PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class CasperGlowButtonEntityDescription(ButtonEntityDescription):
"""Describe a Casper Glow button entity."""
press_fn: Callable[[CasperGlow], Awaitable[None]]
BUTTON_DESCRIPTIONS: tuple[CasperGlowButtonEntityDescription, ...] = (
CasperGlowButtonEntityDescription(
key="pause",
translation_key="pause",
press_fn=lambda device: device.pause(),
),
CasperGlowButtonEntityDescription(
key="resume",
translation_key="resume",
press_fn=lambda device: device.resume(),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: CasperGlowConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the button platform for Casper Glow."""
async_add_entities(
CasperGlowButton(entry.runtime_data, description)
for description in BUTTON_DESCRIPTIONS
)
class CasperGlowButton(CasperGlowEntity, ButtonEntity):
"""A Casper Glow button entity."""
entity_description: CasperGlowButtonEntityDescription
def __init__(
self,
coordinator: CasperGlowCoordinator,
description: CasperGlowButtonEntityDescription,
) -> None:
"""Initialize a Casper Glow button."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = (
f"{format_mac(coordinator.device.address)}_{description.key}"
)
async def async_press(self) -> None:
"""Press the button."""
await self._async_command(self.entity_description.press_fn(self._device))

View File

@@ -4,6 +4,14 @@
"paused": {
"default": "mdi:timer-pause"
}
},
"button": {
"pause": {
"default": "mdi:pause"
},
"resume": {
"default": "mdi:play"
}
}
}
}

View File

@@ -14,6 +14,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pycasperglow"],
"quality_scale": "bronze",
"quality_scale": "silver",
"requirements": ["pycasperglow==1.1.0"]
}

View File

@@ -32,7 +32,9 @@ rules:
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
reauthentication-flow:
status: exempt
comment: Bluetooth device with no authentication credentials.
test-coverage: done
# Gold
@@ -53,15 +55,9 @@ rules:
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations:
status: exempt
comment: No entity translations needed.
exception-translations:
status: exempt
comment: No custom services that raise exceptions.
icon-translations:
status: exempt
comment: No icon translations needed.
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo

View File

@@ -31,6 +31,14 @@
"paused": {
"name": "Dimming paused"
}
},
"button": {
"pause": {
"name": "Pause dimming"
},
"resume": {
"name": "Resume dimming"
}
}
},
"exceptions": {

View File

@@ -0,0 +1,101 @@
# serializer version: 1
# name: test_entities[button.jar_pause_dimming-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.jar_pause_dimming',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Pause dimming',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Pause dimming',
'platform': 'casper_glow',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'pause',
'unique_id': 'aa:bb:cc:dd:ee:ff_pause',
'unit_of_measurement': None,
})
# ---
# name: test_entities[button.jar_pause_dimming-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Jar Pause dimming',
}),
'context': <ANY>,
'entity_id': 'button.jar_pause_dimming',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_entities[button.jar_resume_dimming-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.jar_resume_dimming',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Resume dimming',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Resume dimming',
'platform': 'casper_glow',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'resume',
'unique_id': 'aa:bb:cc:dd:ee:ff_resume',
'unit_of_measurement': None,
})
# ---
# name: test_entities[button.jar_resume_dimming-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Jar Resume dimming',
}),
'context': <ANY>,
'entity_id': 'button.jar_resume_dimming',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---

View File

@@ -0,0 +1,85 @@
"""Test the Casper Glow button platform."""
from unittest.mock import MagicMock, patch
from pycasperglow import CasperGlowError
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
PAUSE_ENTITY_ID = "button.jar_pause_dimming"
RESUME_ENTITY_ID = "button.jar_resume_dimming"
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_entities(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_casper_glow: MagicMock,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test all button entities match the snapshot."""
with patch("homeassistant.components.casper_glow.PLATFORMS", [Platform.BUTTON]):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(
("entity_id", "method"),
[
(PAUSE_ENTITY_ID, "pause"),
(RESUME_ENTITY_ID, "resume"),
],
)
async def test_button_press_calls_device(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_casper_glow: MagicMock,
entity_id: str,
method: str,
) -> None:
"""Test that pressing a button calls the correct device method."""
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
getattr(mock_casper_glow, method).assert_called_once()
@pytest.mark.parametrize(
("entity_id", "method"),
[
(PAUSE_ENTITY_ID, "pause"),
(RESUME_ENTITY_ID, "resume"),
],
)
async def test_button_raises_homeassistant_error_on_failure(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_casper_glow: MagicMock,
entity_id: str,
method: str,
) -> None:
"""Test that a CasperGlowError is converted to HomeAssistantError."""
getattr(mock_casper_glow, method).side_effect = CasperGlowError("connection lost")
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)