Add entity base class in Habitica integration (#121320)

* Sensor refactoring

* Change todo entities to use common base entity

* Requested changes

* Update button platform to use base class

* Update swtich platform to use base entity class
This commit is contained in:
Mr. Bubbles 2024-07-08 10:18:09 +02:00 committed by GitHub
parent 6350c5479b
commit 00aafc0cf7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 205 additions and 220 deletions

View File

@ -6,21 +6,19 @@ from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from enum import StrEnum from enum import StrEnum
from http import HTTPStatus from http import HTTPStatus
from typing import TYPE_CHECKING, Any from typing import Any
from aiohttp import ClientResponseError from aiohttp import ClientResponseError
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.const import CONF_NAME, CONF_URL
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import HabiticaConfigEntry from . import HabiticaConfigEntry
from .const import DOMAIN, MANUFACTURER, NAME from .const import DOMAIN
from .coordinator import HabiticaData, HabiticaDataUpdateCoordinator from .coordinator import HabiticaData, HabiticaDataUpdateCoordinator
from .entity import HabiticaBase
@dataclass(kw_only=True, frozen=True) @dataclass(kw_only=True, frozen=True)
@ -90,34 +88,11 @@ async def async_setup_entry(
) )
class HabiticaButton(CoordinatorEntity[HabiticaDataUpdateCoordinator], ButtonEntity): class HabiticaButton(HabiticaBase, ButtonEntity):
"""Representation of a Habitica button.""" """Representation of a Habitica button."""
_attr_has_entity_name = True
entity_description: HabiticaButtonEntityDescription entity_description: HabiticaButtonEntityDescription
def __init__(
self,
coordinator: HabiticaDataUpdateCoordinator,
entity_description: HabiticaButtonEntityDescription,
) -> None:
"""Initialize a Habitica button."""
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)},
)
async def async_press(self) -> None: async def async_press(self) -> None:
"""Handle the button press.""" """Handle the button press."""
try: try:

View File

@ -21,3 +21,5 @@ MANUFACTURER = "HabitRPG, Inc."
NAME = "Habitica" NAME = "Habitica"
ADDITIONAL_USER_FIELDS: set[str] = {"lastCron"} ADDITIONAL_USER_FIELDS: set[str] = {"lastCron"}
UNIT_TASKS = "tasks"

View File

@ -0,0 +1,42 @@
"""Base entity for Habitica."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from homeassistant.const import CONF_NAME, CONF_URL
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER, NAME
from .coordinator import HabiticaDataUpdateCoordinator
class HabiticaBase(CoordinatorEntity[HabiticaDataUpdateCoordinator]):
"""Base Habitica entity."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: HabiticaDataUpdateCoordinator,
entity_description: EntityDescription,
context: Any = None,
) -> None:
"""Initialize a Habitica entity."""
super().__init__(coordinator, context)
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)},
)

View File

@ -64,6 +64,18 @@
"wizard": "mdi:wizard-hat", "wizard": "mdi:wizard-hat",
"rogue": "mdi:ninja" "rogue": "mdi:ninja"
} }
},
"todos": {
"default": "mdi:checkbox-outline"
},
"dailys": {
"default": "mdi:calendar-month"
},
"habits": {
"default": "mdi:contrast-box"
},
"rewards": {
"default": "mdi:treasure-chest"
} }
}, },
"switch": { "switch": {

View File

@ -2,11 +2,11 @@
from __future__ import annotations from __future__ import annotations
from collections import namedtuple from collections.abc import Callable, Mapping
from dataclasses import dataclass from dataclasses import dataclass
from enum import StrEnum from enum import StrEnum
import logging import logging
from typing import TYPE_CHECKING, cast from typing import TYPE_CHECKING, Any
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN, DOMAIN as SENSOR_DOMAIN,
@ -14,11 +14,8 @@ from homeassistant.components.sensor import (
SensorEntity, SensorEntity,
SensorEntityDescription, SensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, CONF_URL
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import ( from homeassistant.helpers.issue_registry import (
IssueSeverity, IssueSeverity,
@ -26,11 +23,11 @@ from homeassistant.helpers.issue_registry import (
async_delete_issue, async_delete_issue,
) )
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import HabiticaConfigEntry from . import HabiticaConfigEntry
from .const import DOMAIN, MANUFACTURER, NAME from .const import DOMAIN, UNIT_TASKS
from .coordinator import HabiticaDataUpdateCoordinator from .coordinator import HabiticaDataUpdateCoordinator
from .entity import HabiticaBase
from .util import entity_used_in from .util import entity_used_in
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -40,7 +37,15 @@ _LOGGER = logging.getLogger(__name__)
class HabitipySensorEntityDescription(SensorEntityDescription): class HabitipySensorEntityDescription(SensorEntityDescription):
"""Habitipy Sensor Description.""" """Habitipy Sensor Description."""
value_path: list[str] value_fn: Callable[[dict[str, Any]], StateType]
ctx: str
@dataclass(kw_only=True, frozen=True)
class HabitipyTaskSensorEntityDescription(SensorEntityDescription):
"""Habitipy Task Sensor Description."""
value_fn: Callable[[list[dict[str, Any]]], list[dict[str, Any]]]
class HabitipySensorEntity(StrEnum): class HabitipySensorEntity(StrEnum):
@ -56,87 +61,88 @@ class HabitipySensorEntity(StrEnum):
LEVEL = "level" LEVEL = "level"
GOLD = "gold" GOLD = "gold"
CLASS = "class" CLASS = "class"
HABITS = "habits"
DAILIES = "dailys"
TODOS = "todos"
REWARDS = "rewards"
SENSOR_DESCRIPTIONS: dict[str, HabitipySensorEntityDescription] = { SENSOR_DESCRIPTIONS: tuple[HabitipySensorEntityDescription, ...] = (
HabitipySensorEntity.DISPLAY_NAME: HabitipySensorEntityDescription( HabitipySensorEntityDescription(
key=HabitipySensorEntity.DISPLAY_NAME, key=HabitipySensorEntity.DISPLAY_NAME,
translation_key=HabitipySensorEntity.DISPLAY_NAME, translation_key=HabitipySensorEntity.DISPLAY_NAME,
value_path=["profile", "name"], value_fn=lambda user: user.get("profile", {}).get("name"),
ctx="profile",
), ),
HabitipySensorEntity.HEALTH: HabitipySensorEntityDescription( HabitipySensorEntityDescription(
key=HabitipySensorEntity.HEALTH, key=HabitipySensorEntity.HEALTH,
translation_key=HabitipySensorEntity.HEALTH, translation_key=HabitipySensorEntity.HEALTH,
native_unit_of_measurement="HP", native_unit_of_measurement="HP",
suggested_display_precision=0, suggested_display_precision=0,
value_path=["stats", "hp"], value_fn=lambda user: user.get("stats", {}).get("hp"),
ctx="stats",
), ),
HabitipySensorEntity.HEALTH_MAX: HabitipySensorEntityDescription( HabitipySensorEntityDescription(
key=HabitipySensorEntity.HEALTH_MAX, key=HabitipySensorEntity.HEALTH_MAX,
translation_key=HabitipySensorEntity.HEALTH_MAX, translation_key=HabitipySensorEntity.HEALTH_MAX,
native_unit_of_measurement="HP", native_unit_of_measurement="HP",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_path=["stats", "maxHealth"], value_fn=lambda user: user.get("stats", {}).get("maxHealth"),
ctx="stats",
), ),
HabitipySensorEntity.MANA: HabitipySensorEntityDescription( HabitipySensorEntityDescription(
key=HabitipySensorEntity.MANA, key=HabitipySensorEntity.MANA,
translation_key=HabitipySensorEntity.MANA, translation_key=HabitipySensorEntity.MANA,
native_unit_of_measurement="MP", native_unit_of_measurement="MP",
suggested_display_precision=0, suggested_display_precision=0,
value_path=["stats", "mp"], value_fn=lambda user: user.get("stats", {}).get("mp"),
ctx="stats",
), ),
HabitipySensorEntity.MANA_MAX: HabitipySensorEntityDescription( HabitipySensorEntityDescription(
key=HabitipySensorEntity.MANA_MAX, key=HabitipySensorEntity.MANA_MAX,
translation_key=HabitipySensorEntity.MANA_MAX, translation_key=HabitipySensorEntity.MANA_MAX,
native_unit_of_measurement="MP", native_unit_of_measurement="MP",
value_path=["stats", "maxMP"], value_fn=lambda user: user.get("stats", {}).get("maxMP"),
ctx="stats",
), ),
HabitipySensorEntity.EXPERIENCE: HabitipySensorEntityDescription( HabitipySensorEntityDescription(
key=HabitipySensorEntity.EXPERIENCE, key=HabitipySensorEntity.EXPERIENCE,
translation_key=HabitipySensorEntity.EXPERIENCE, translation_key=HabitipySensorEntity.EXPERIENCE,
native_unit_of_measurement="XP", native_unit_of_measurement="XP",
value_path=["stats", "exp"], value_fn=lambda user: user.get("stats", {}).get("exp"),
ctx="stats",
), ),
HabitipySensorEntity.EXPERIENCE_MAX: HabitipySensorEntityDescription( HabitipySensorEntityDescription(
key=HabitipySensorEntity.EXPERIENCE_MAX, key=HabitipySensorEntity.EXPERIENCE_MAX,
translation_key=HabitipySensorEntity.EXPERIENCE_MAX, translation_key=HabitipySensorEntity.EXPERIENCE_MAX,
native_unit_of_measurement="XP", native_unit_of_measurement="XP",
value_path=["stats", "toNextLevel"], value_fn=lambda user: user.get("stats", {}).get("toNextLevel"),
ctx="stats",
), ),
HabitipySensorEntity.LEVEL: HabitipySensorEntityDescription( HabitipySensorEntityDescription(
key=HabitipySensorEntity.LEVEL, key=HabitipySensorEntity.LEVEL,
translation_key=HabitipySensorEntity.LEVEL, translation_key=HabitipySensorEntity.LEVEL,
value_path=["stats", "lvl"], value_fn=lambda user: user.get("stats", {}).get("lvl"),
ctx="stats",
), ),
HabitipySensorEntity.GOLD: HabitipySensorEntityDescription( HabitipySensorEntityDescription(
key=HabitipySensorEntity.GOLD, key=HabitipySensorEntity.GOLD,
translation_key=HabitipySensorEntity.GOLD, translation_key=HabitipySensorEntity.GOLD,
native_unit_of_measurement="GP", native_unit_of_measurement="GP",
suggested_display_precision=2, suggested_display_precision=2,
value_path=["stats", "gp"], value_fn=lambda user: user.get("stats", {}).get("gp"),
ctx="stats",
), ),
HabitipySensorEntity.CLASS: HabitipySensorEntityDescription( HabitipySensorEntityDescription(
key=HabitipySensorEntity.CLASS, key=HabitipySensorEntity.CLASS,
translation_key=HabitipySensorEntity.CLASS, translation_key=HabitipySensorEntity.CLASS,
value_path=["stats", "class"], value_fn=lambda user: user.get("stats", {}).get("class"),
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
options=["warrior", "healer", "wizard", "rogue"], options=["warrior", "healer", "wizard", "rogue"],
ctx="stats",
), ),
} )
SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"])
TASKS_TYPES = {
"habits": SensorType(
"Habits", "mdi:clipboard-list-outline", "n_of_tasks", ["habit"]
),
"dailys": SensorType(
"Dailys", "mdi:clipboard-list-outline", "n_of_tasks", ["daily"]
),
"todos": SensorType("TODOs", "mdi:clipboard-list-outline", "n_of_tasks", ["todo"]),
"rewards": SensorType(
"Rewards", "mdi:clipboard-list-outline", "n_of_tasks", ["reward"]
),
}
TASKS_MAP_ID = "id" TASKS_MAP_ID = "id"
TASKS_MAP = { TASKS_MAP = {
@ -166,6 +172,36 @@ TASKS_MAP = {
} }
TASK_SENSOR_DESCRIPTION: tuple[HabitipyTaskSensorEntityDescription, ...] = (
HabitipyTaskSensorEntityDescription(
key=HabitipySensorEntity.HABITS,
translation_key=HabitipySensorEntity.HABITS,
native_unit_of_measurement=UNIT_TASKS,
value_fn=lambda tasks: [r for r in tasks if r.get("type") == "habit"],
),
HabitipyTaskSensorEntityDescription(
key=HabitipySensorEntity.DAILIES,
translation_key=HabitipySensorEntity.DAILIES,
native_unit_of_measurement=UNIT_TASKS,
value_fn=lambda tasks: [r for r in tasks if r.get("type") == "daily"],
),
HabitipyTaskSensorEntityDescription(
key=HabitipySensorEntity.TODOS,
translation_key=HabitipySensorEntity.TODOS,
native_unit_of_measurement=UNIT_TASKS,
value_fn=lambda tasks: [
r for r in tasks if r.get("type") == "todo" and not r.get("completed")
],
),
HabitipyTaskSensorEntityDescription(
key=HabitipySensorEntity.REWARDS,
translation_key=HabitipySensorEntity.REWARDS,
native_unit_of_measurement=UNIT_TASKS,
value_fn=lambda tasks: [r for r in tasks if r.get("type") == "reward"],
),
)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: HabiticaConfigEntry, config_entry: HabiticaConfigEntry,
@ -173,121 +209,70 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the habitica sensors.""" """Set up the habitica sensors."""
name = config_entry.data[CONF_NAME]
coordinator = config_entry.runtime_data coordinator = config_entry.runtime_data
entities: list[SensorEntity] = [ entities: list[SensorEntity] = [
HabitipySensor(coordinator, description, config_entry) HabitipySensor(coordinator, description) for description in SENSOR_DESCRIPTIONS
for description in SENSOR_DESCRIPTIONS.values()
] ]
entities.extend( entities.extend(
HabitipyTaskSensor(name, task_type, coordinator, config_entry) HabitipyTaskSensor(coordinator, description)
for task_type in TASKS_TYPES for description in TASK_SENSOR_DESCRIPTION
) )
async_add_entities(entities, True) async_add_entities(entities, True)
class HabitipySensor(CoordinatorEntity[HabiticaDataUpdateCoordinator], SensorEntity): class HabitipySensor(HabiticaBase, SensorEntity):
"""A generic Habitica sensor.""" """A generic Habitica sensor."""
_attr_has_entity_name = True
entity_description: HabitipySensorEntityDescription entity_description: HabitipySensorEntityDescription
def __init__( def __init__(
self, self,
coordinator: HabiticaDataUpdateCoordinator, coordinator: HabiticaDataUpdateCoordinator,
entity_description: HabitipySensorEntityDescription, entity_description: HabitipySensorEntityDescription,
entry: ConfigEntry,
) -> None: ) -> None:
"""Initialize a generic Habitica sensor.""" """Initialize a generic Habitica sensor."""
super().__init__(coordinator, context=entity_description.value_path[0]) super().__init__(
if TYPE_CHECKING: coordinator,
assert entry.unique_id entity_description,
self.entity_description = entity_description context=entity_description.ctx,
self._attr_unique_id = f"{entry.unique_id}_{entity_description.key}"
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
manufacturer=MANUFACTURER,
model=NAME,
name=entry.data[CONF_NAME],
configuration_url=entry.data[CONF_URL],
identifiers={(DOMAIN, entry.unique_id)},
) )
@property @property
def native_value(self) -> StateType: def native_value(self) -> StateType:
"""Return the state of the device.""" """Return the state of the device."""
data = self.coordinator.data.user
for element in self.entity_description.value_path: return self.entity_description.value_fn(self.coordinator.data.user)
data = data[element]
return cast(StateType, data)
class HabitipyTaskSensor( class HabitipyTaskSensor(HabiticaBase, SensorEntity):
CoordinatorEntity[HabiticaDataUpdateCoordinator], SensorEntity
):
"""A Habitica task sensor.""" """A Habitica task sensor."""
def __init__(self, name, task_name, coordinator, entry): entity_description: HabitipyTaskSensorEntityDescription
"""Initialize a generic Habitica task."""
super().__init__(coordinator)
self._name = name
self._task_name = task_name
self._task_type = TASKS_TYPES[task_name]
self._state = None
self._attr_unique_id = f"{entry.unique_id}_{task_name}"
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
manufacturer=MANUFACTURER,
model=NAME,
name=entry.data[CONF_NAME],
configuration_url=entry.data[CONF_URL],
identifiers={(DOMAIN, entry.unique_id)},
)
@property @property
def icon(self): def native_value(self) -> StateType:
"""Return the icon to use in the frontend, if any."""
return self._task_type.icon
@property
def name(self):
"""Return the name of the task."""
return f"{DOMAIN}_{self._name}_{self._task_name}"
@property
def native_value(self):
"""Return the state of the device.""" """Return the state of the device."""
return len(
[ return len(self.entity_description.value_fn(self.coordinator.data.tasks))
task
for task in self.coordinator.data.tasks
if task.get("type") in self._task_type.path
and not task.get("completed")
]
)
@property @property
def extra_state_attributes(self): def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return the state attributes of all user tasks.""" """Return the state attributes of all user tasks."""
attrs = {} attrs = {}
# Map tasks to TASKS_MAP # Map tasks to TASKS_MAP
for received_task in self.coordinator.data.tasks: for received_task in self.entity_description.value_fn(
if received_task.get("type") in self._task_type.path: self.coordinator.data.tasks
task_id = received_task[TASKS_MAP_ID] ):
task = {} task_id = received_task[TASKS_MAP_ID]
for map_key, map_value in TASKS_MAP.items(): task = {}
if value := received_task.get(map_value): for map_key, map_value in TASKS_MAP.items():
task[map_key] = value if value := received_task.get(map_value):
attrs[task_id] = task task[map_key] = value
attrs[task_id] = task
return attrs return attrs
@property
def native_unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self._task_type.unit
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Raise issue when entity is registered and was not disabled.""" """Raise issue when entity is registered and was not disabled."""
if TYPE_CHECKING: if TYPE_CHECKING:
@ -297,19 +282,20 @@ class HabitipyTaskSensor(
): ):
if ( if (
self.enabled self.enabled
and self._task_name in ("todos", "dailys") and self.entity_description.key
in (HabitipySensorEntity.TODOS, HabitipySensorEntity.DAILIES)
and entity_used_in(self.hass, entity_id) and entity_used_in(self.hass, entity_id)
): ):
async_create_issue( async_create_issue(
self.hass, self.hass,
DOMAIN, DOMAIN,
f"deprecated_task_entity_{self._task_name}", f"deprecated_task_entity_{self.entity_description.key}",
breaks_in_ha_version="2025.2.0", breaks_in_ha_version="2025.2.0",
is_fixable=False, is_fixable=False,
severity=IssueSeverity.WARNING, severity=IssueSeverity.WARNING,
translation_key="deprecated_task_entity", translation_key="deprecated_task_entity",
translation_placeholders={ translation_placeholders={
"task_name": self._task_name, "task_name": str(self.name),
"entity": entity_id, "entity": entity_id,
}, },
) )
@ -317,6 +303,6 @@ class HabitipyTaskSensor(
async_delete_issue( async_delete_issue(
self.hass, self.hass,
DOMAIN, DOMAIN,
f"deprecated_task_entity_{self._task_name}", f"deprecated_task_entity_{self.entity_description.key}",
) )
await super().async_added_to_hass() await super().async_added_to_hass()

View File

@ -70,6 +70,18 @@
"wizard": "Mage", "wizard": "Mage",
"rogue": "Rogue" "rogue": "Rogue"
} }
},
"todos": {
"name": "To-Do's"
},
"dailys": {
"name": "Dailies"
},
"habits": {
"name": "Habits"
},
"rewards": {
"name": "Rewards"
} }
}, },
"switch": { "switch": {

View File

@ -5,22 +5,19 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from enum import StrEnum from enum import StrEnum
from typing import TYPE_CHECKING, Any from typing import Any
from homeassistant.components.switch import ( from homeassistant.components.switch import (
SwitchDeviceClass, SwitchDeviceClass,
SwitchEntity, SwitchEntity,
SwitchEntityDescription, SwitchEntityDescription,
) )
from homeassistant.const import CONF_NAME, CONF_URL
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import HabiticaConfigEntry from . import HabiticaConfigEntry
from .const import DOMAIN, MANUFACTURER, NAME
from .coordinator import HabiticaData, HabiticaDataUpdateCoordinator from .coordinator import HabiticaData, HabiticaDataUpdateCoordinator
from .entity import HabiticaBase
@dataclass(kw_only=True, frozen=True) @dataclass(kw_only=True, frozen=True)
@ -64,34 +61,11 @@ async def async_setup_entry(
) )
class HabiticaSwitch(CoordinatorEntity[HabiticaDataUpdateCoordinator], SwitchEntity): class HabiticaSwitch(HabiticaBase, SwitchEntity):
"""Representation of a Habitica Switch.""" """Representation of a Habitica Switch."""
_attr_has_entity_name = True
entity_description: HabiticaSwitchEntityDescription 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 @property
def is_on(self) -> bool | None: def is_on(self) -> bool | None:
"""Return the state of the device.""" """Return the state of the device."""

View File

@ -15,17 +15,16 @@ from homeassistant.components.todo import (
TodoListEntity, TodoListEntity,
TodoListEntityFeature, TodoListEntityFeature,
) )
from homeassistant.const import CONF_NAME, CONF_URL
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import HabiticaConfigEntry from . import HabiticaConfigEntry
from .const import ASSETS_URL, DOMAIN, MANUFACTURER, NAME from .const import ASSETS_URL, DOMAIN
from .coordinator import HabiticaDataUpdateCoordinator from .coordinator import HabiticaDataUpdateCoordinator
from .entity import HabiticaBase
from .util import next_due_date from .util import next_due_date
@ -63,35 +62,16 @@ async def async_setup_entry(
) )
class BaseHabiticaListEntity( class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
CoordinatorEntity[HabiticaDataUpdateCoordinator], TodoListEntity
):
"""Representation of Habitica task lists.""" """Representation of Habitica task lists."""
_attr_has_entity_name = True
def __init__( def __init__(
self, self,
coordinator: HabiticaDataUpdateCoordinator, coordinator: HabiticaDataUpdateCoordinator,
key: HabiticaTodoList,
) -> None: ) -> None:
"""Initialize HabiticaTodoListEntity.""" """Initialize HabiticaTodoListEntity."""
entry = coordinator.config_entry
if TYPE_CHECKING:
assert entry.unique_id
super().__init__(coordinator)
self._attr_unique_id = f"{entry.unique_id}_{key}" super().__init__(coordinator, self.entity_description)
self._attr_translation_key = key
self.idx = key
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
manufacturer=MANUFACTURER,
model=NAME,
name=entry.data[CONF_NAME],
configuration_url=entry.data[CONF_URL],
identifiers={(DOMAIN, entry.unique_id)},
)
async def async_delete_todo_items(self, uids: list[str]) -> None: async def async_delete_todo_items(self, uids: list[str]) -> None:
"""Delete Habitica tasks.""" """Delete Habitica tasks."""
@ -101,7 +81,7 @@ class BaseHabiticaListEntity(
except ClientResponseError as e: except ClientResponseError as e:
raise ServiceValidationError( raise ServiceValidationError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key=f"delete_{self.idx}_failed", translation_key=f"delete_{self.entity_description.key}_failed",
) from e ) from e
await self.coordinator.async_refresh() await self.coordinator.async_refresh()
@ -129,7 +109,7 @@ class BaseHabiticaListEntity(
except ClientResponseError as e: except ClientResponseError as e:
raise ServiceValidationError( raise ServiceValidationError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key=f"move_{self.idx}_item_failed", translation_key=f"move_{self.entity_description.key}_item_failed",
translation_placeholders={"pos": str(pos)}, translation_placeholders={"pos": str(pos)},
) from e ) from e
@ -145,7 +125,9 @@ class BaseHabiticaListEntity(
assert current_item assert current_item
assert item.due assert item.due
if self.idx is HabiticaTodoList.TODOS: # Only todos support a due date. if (
self.entity_description.key is HabiticaTodoList.TODOS
): # Only todos support a due date.
date = item.due.isoformat() date = item.due.isoformat()
else: else:
date = None date = None
@ -159,7 +141,7 @@ class BaseHabiticaListEntity(
except ClientResponseError as e: except ClientResponseError as e:
raise ServiceValidationError( raise ServiceValidationError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key=f"update_{self.idx}_item_failed", translation_key=f"update_{self.entity_description.key}_item_failed",
translation_placeholders={"name": item.summary or ""}, translation_placeholders={"name": item.summary or ""},
) from e ) from e
@ -185,7 +167,7 @@ class BaseHabiticaListEntity(
except ClientResponseError as e: except ClientResponseError as e:
raise ServiceValidationError( raise ServiceValidationError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key=f"score_{self.idx}_item_failed", translation_key=f"score_{self.entity_description.key}_item_failed",
translation_placeholders={"name": item.summary or ""}, translation_placeholders={"name": item.summary or ""},
) from e ) from e
@ -212,10 +194,10 @@ class HabiticaTodosListEntity(BaseHabiticaListEntity):
| TodoListEntityFeature.SET_DUE_DATE_ON_ITEM | TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM | TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
) )
entity_description = EntityDescription(
def __init__(self, coordinator: HabiticaDataUpdateCoordinator) -> None: key=HabiticaTodoList.TODOS,
"""Initialize HabiticaTodosListEntity.""" translation_key=HabiticaTodoList.TODOS,
super().__init__(coordinator, HabiticaTodoList.TODOS) )
@property @property
def todo_items(self) -> list[TodoItem]: def todo_items(self) -> list[TodoItem]:
@ -258,7 +240,7 @@ class HabiticaTodosListEntity(BaseHabiticaListEntity):
except ClientResponseError as e: except ClientResponseError as e:
raise ServiceValidationError( raise ServiceValidationError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key=f"create_{self.idx}_item_failed", translation_key=f"create_{self.entity_description.key}_item_failed",
translation_placeholders={"name": item.summary or ""}, translation_placeholders={"name": item.summary or ""},
) from e ) from e
@ -274,10 +256,10 @@ class HabiticaDailiesListEntity(BaseHabiticaListEntity):
| TodoListEntityFeature.SET_DUE_DATE_ON_ITEM | TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM | TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
) )
entity_description = EntityDescription(
def __init__(self, coordinator: HabiticaDataUpdateCoordinator) -> None: key=HabiticaTodoList.DAILIES,
"""Initialize HabiticaDailiesListEntity.""" translation_key=HabiticaTodoList.DAILIES,
super().__init__(coordinator, HabiticaTodoList.DAILIES) )
@property @property
def todo_items(self) -> list[TodoItem]: def todo_items(self) -> list[TodoItem]: