diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 885985c2d0a..468db8fbc42 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -82,7 +82,7 @@ INSTANCE_LIST_SCHEMA = vol.All( ) CONFIG_SCHEMA = vol.Schema({DOMAIN: INSTANCE_LIST_SCHEMA}, extra=vol.ALLOW_EXTRA) -PLATFORMS = [Platform.BUTTON, Platform.SENSOR, Platform.TODO] +PLATFORMS = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH, Platform.TODO] SERVICE_API_CALL_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/habitica/coordinator.py b/homeassistant/components/habitica/coordinator.py index 0867a8bd550..f72c11053da 100644 --- a/homeassistant/components/habitica/coordinator.py +++ b/homeassistant/components/habitica/coordinator.py @@ -2,8 +2,10 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta +from http import HTTPStatus import logging from typing import Any @@ -12,6 +14,7 @@ from habitipy.aio import HabitipyAsync from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ADDITIONAL_USER_FIELDS, DOMAIN @@ -53,3 +56,23 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]): raise UpdateFailed(f"Error communicating with API: {error}") from error return HabiticaData(user=user_response, tasks=tasks_response) + + async def execute( + self, func: Callable[[HabiticaDataUpdateCoordinator], Any] + ) -> None: + """Execute an API call.""" + + try: + await func(self) + except ClientResponseError as e: + if e.status == HTTPStatus.TOO_MANY_REQUESTS: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="setup_rate_limit_exception", + ) from e + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="service_call_exception", + ) from e + else: + await self.async_refresh() diff --git a/homeassistant/components/habitica/icons.json b/homeassistant/components/habitica/icons.json index 8d4ec2b5249..eed8ad5b9b5 100644 --- a/homeassistant/components/habitica/icons.json +++ b/homeassistant/components/habitica/icons.json @@ -65,6 +65,14 @@ "rogue": "mdi:ninja" } } + }, + "switch": { + "sleep": { + "default": "mdi:sleep-off", + "state": { + "on": "mdi:sleep" + } + } } }, "services": { diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index c49beaacd6e..9edb6e3ee36 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -72,6 +72,11 @@ } } }, + "switch": { + "sleep": { + "name": "Rest in the inn" + } + }, "todo": { "todos": { "name": "To-Do's" diff --git a/homeassistant/components/habitica/switch.py b/homeassistant/components/habitica/switch.py new file mode 100644 index 00000000000..e75a6cdb352 --- /dev/null +++ b/homeassistant/components/habitica/switch.py @@ -0,0 +1,110 @@ +"""Switch platform for Habitica integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from enum import StrEnum +from typing import TYPE_CHECKING, Any + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.const import CONF_NAME, CONF_URL +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import HabiticaConfigEntry +from .const import DOMAIN, MANUFACTURER, NAME +from .coordinator import HabiticaData, HabiticaDataUpdateCoordinator + + +@dataclass(kw_only=True, frozen=True) +class HabiticaSwitchEntityDescription(SwitchEntityDescription): + """Describes Habitica switch entity.""" + + turn_on_fn: Callable[[HabiticaDataUpdateCoordinator], Any] + turn_off_fn: Callable[[HabiticaDataUpdateCoordinator], Any] + is_on_fn: Callable[[HabiticaData], bool] + + +class HabiticaSwitchEntity(StrEnum): + """Habitica switch entities.""" + + SLEEP = "sleep" + + +SWTICH_DESCRIPTIONS: tuple[HabiticaSwitchEntityDescription, ...] = ( + HabiticaSwitchEntityDescription( + key=HabiticaSwitchEntity.SLEEP, + translation_key=HabiticaSwitchEntity.SLEEP, + device_class=SwitchDeviceClass.SWITCH, + turn_on_fn=lambda coordinator: coordinator.api["user"]["sleep"].post(), + turn_off_fn=lambda coordinator: coordinator.api["user"]["sleep"].post(), + is_on_fn=lambda data: data.user["preferences"]["sleep"], + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: HabiticaConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up switches from a config entry.""" + + coordinator = entry.runtime_data + + async_add_entities( + HabiticaSwitch(coordinator, description) for description in SWTICH_DESCRIPTIONS + ) + + +class HabiticaSwitch(CoordinatorEntity[HabiticaDataUpdateCoordinator], SwitchEntity): + """Representation of a Habitica Switch.""" + + _attr_has_entity_name = True + entity_description: HabiticaSwitchEntityDescription + + def __init__( + self, + coordinator: HabiticaDataUpdateCoordinator, + entity_description: HabiticaSwitchEntityDescription, + ) -> None: + """Initialize a Habitica switch.""" + super().__init__(coordinator) + if TYPE_CHECKING: + assert coordinator.config_entry.unique_id + self.entity_description = entity_description + self._attr_unique_id = ( + f"{coordinator.config_entry.unique_id}_{entity_description.key}" + ) + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + manufacturer=MANUFACTURER, + model=NAME, + name=coordinator.config_entry.data[CONF_NAME], + configuration_url=coordinator.config_entry.data[CONF_URL], + identifiers={(DOMAIN, coordinator.config_entry.unique_id)}, + ) + + @property + def is_on(self) -> bool | None: + """Return the state of the device.""" + return self.entity_description.is_on_fn( + self.coordinator.data, + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + + await self.coordinator.execute(self.entity_description.turn_on_fn) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + + await self.coordinator.execute(self.entity_description.turn_off_fn)