From cc5bb556c82c69f02edbc08b38962227cbd32086 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 9 Feb 2022 22:20:24 +0100 Subject: [PATCH] Move Freebox reboot service to a button entity (#65501) * Add restart button to freebox * Add warning * restart => reboot * Add button tests Co-authored-by: epenet --- homeassistant/components/freebox/__init__.py | 10 +++ homeassistant/components/freebox/button.py | 76 ++++++++++++++++++++ homeassistant/components/freebox/const.py | 2 +- tests/components/freebox/test_button.py | 43 +++++++++++ 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/freebox/button.py create mode 100644 tests/components/freebox/test_button.py diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index b5deb8517bb..7d7bc7695cd 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -1,5 +1,6 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" from datetime import timedelta +import logging from freebox_api.exceptions import HttpRequestError import voluptuous as vol @@ -29,6 +30,8 @@ CONFIG_SCHEMA = vol.Schema( SCAN_INTERVAL = timedelta(seconds=30) +_LOGGER = logging.getLogger(__name__) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Freebox integration.""" @@ -67,6 +70,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Services async def async_reboot(call: ServiceCall) -> None: """Handle reboot service call.""" + # The Freebox reboot service has been replaced by a + # dedicated button entity and marked as deprecated + _LOGGER.warning( + "The 'freebox.reboot' service is deprecated and " + "replaced by a dedicated reboot button entity; please " + "use that entity to reboot the freebox instead" + ) await router.reboot() hass.services.async_register(DOMAIN, SERVICE_REBOOT, async_reboot) diff --git a/homeassistant/components/freebox/button.py b/homeassistant/components/freebox/button.py new file mode 100644 index 00000000000..b3313bba9dd --- /dev/null +++ b/homeassistant/components/freebox/button.py @@ -0,0 +1,76 @@ +"""Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .router import FreeboxRouter + + +@dataclass +class FreeboxButtonRequiredKeysMixin: + """Mixin for required keys.""" + + async_press: Callable[[FreeboxRouter], Awaitable] + + +@dataclass +class FreeboxButtonEntityDescription( + ButtonEntityDescription, FreeboxButtonRequiredKeysMixin +): + """Class describing Freebox button entities.""" + + +BUTTON_DESCRIPTIONS: tuple[FreeboxButtonEntityDescription, ...] = ( + FreeboxButtonEntityDescription( + key="reboot", + name="Reboot Freebox", + device_class=ButtonDeviceClass.RESTART, + async_press=lambda router: router.reboot(), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the buttons.""" + router: FreeboxRouter = hass.data[DOMAIN][entry.unique_id] + entities = [ + FreeboxButton(router, description) for description in BUTTON_DESCRIPTIONS + ] + async_add_entities(entities, True) + + +class FreeboxButton(ButtonEntity): + """Representation of a Freebox button.""" + + entity_description: FreeboxButtonEntityDescription + + def __init__( + self, router: FreeboxRouter, description: FreeboxButtonEntityDescription + ) -> None: + """Initialize a Freebox button.""" + self.entity_description = description + self._router = router + self._attr_unique_id = f"{router.mac} {description.name}" + + @property + def device_info(self) -> DeviceInfo: + """Return the device information.""" + return self._router.device_info + + async def async_press(self) -> None: + """Press the button.""" + await self.entity_description.async_press(self._router) diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index 77f36cf44de..65e5576f9d7 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -17,7 +17,7 @@ APP_DESC = { } API_VERSION = "v6" -PLATFORMS = [Platform.DEVICE_TRACKER, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.BUTTON, Platform.DEVICE_TRACKER, Platform.SENSOR, Platform.SWITCH] DEFAULT_DEVICE_NAME = "Unknown device" diff --git a/tests/components/freebox/test_button.py b/tests/components/freebox/test_button.py new file mode 100644 index 00000000000..0a6625e163a --- /dev/null +++ b/tests/components/freebox/test_button.py @@ -0,0 +1,43 @@ +"""Tests for the Freebox config flow.""" +from unittest.mock import Mock, patch + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.components.freebox.const import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import MOCK_HOST, MOCK_PORT + +from tests.common import MockConfigEntry + + +async def test_reboot_button(hass: HomeAssistant, router: Mock): + """Test reboot button.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, + unique_id=MOCK_HOST, + ) + entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert hass.config_entries.async_entries() == [entry] + + assert router.call_count == 1 + assert router().open.call_count == 1 + + with patch( + "homeassistant.components.freebox.router.FreeboxRouter.reboot" + ) as mock_service: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + service_data={ + ATTR_ENTITY_ID: "button.reboot_freebox", + }, + blocking=True, + ) + await hass.async_block_till_done() + mock_service.assert_called_once()