diff --git a/CODEOWNERS b/CODEOWNERS index f954675f4d4..fdea411d208 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -550,8 +550,8 @@ build.json @home-assistant/supervisor /tests/components/group/ @home-assistant/core /homeassistant/components/guardian/ @bachya /tests/components/guardian/ @bachya -/homeassistant/components/habitica/ @ASMfreaK @leikoilja -/tests/components/habitica/ @ASMfreaK @leikoilja +/homeassistant/components/habitica/ @ASMfreaK @leikoilja @tr4nt0r +/tests/components/habitica/ @ASMfreaK @leikoilja @tr4nt0r /homeassistant/components/hardkernel/ @home-assistant/core /tests/components/hardkernel/ @home-assistant/core /homeassistant/components/hardware/ @home-assistant/core diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index f05bc9c1713..34736116a26 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -30,10 +30,11 @@ from .const import ( EVENT_API_CALL_SUCCESS, SERVICE_API_CALL, ) -from .sensor import SENSORS_TYPES _LOGGER = logging.getLogger(__name__) +SENSORS_TYPES = ["name", "hp", "maxHealth", "mp", "maxMP", "exp", "toNextLevel", "lvl"] + INSTANCE_SCHEMA = vol.All( cv.deprecated(CONF_SENSORS), vol.Schema( diff --git a/homeassistant/components/habitica/const.py b/homeassistant/components/habitica/const.py index 1379f0a6447..13babdf458a 100644 --- a/homeassistant/components/habitica/const.py +++ b/homeassistant/components/habitica/const.py @@ -15,3 +15,6 @@ ATTR_ARGS = "args" # event constants EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success" ATTR_DATA = "data" + +MANUFACTURER = "HabitRPG, Inc." +NAME = "Habitica" diff --git a/homeassistant/components/habitica/icons.json b/homeassistant/components/habitica/icons.json index 4e5831c4e82..5a722ce6f4b 100644 --- a/homeassistant/components/habitica/icons.json +++ b/homeassistant/components/habitica/icons.json @@ -1,4 +1,50 @@ { + "entity": { + "sensor": { + "display_name": { + "default": "mdi:account-circle" + }, + "health": { + "default": "mdi:heart", + "state": { + "0": "mdi:skull-outline" + } + }, + "health_max": { + "default": "mdi:heart" + }, + "mana": { + "default": "mdi:flask", + "state": { + "0": "mdi:flask-empty-outline" + } + }, + "mana_max": { + "default": "mdi:flask" + }, + "experience": { + "default": "mdi:star-four-points" + }, + "experience_max": { + "default": "mdi:star-four-points" + }, + "level": { + "default": "mdi:crown-circle" + }, + "gold": { + "default": "mdi:sack" + }, + "class": { + "default": "mdi:sword", + "state": { + "warrior": "mdi:sword", + "healer": "mdi:shield", + "wizard": "mdi:wizard-hat", + "rogue": "mdi:ninja" + } + } + } + }, "services": { "api_call": "mdi:console" } diff --git a/homeassistant/components/habitica/manifest.json b/homeassistant/components/habitica/manifest.json index f5f746c979d..1250e6d223f 100644 --- a/homeassistant/components/habitica/manifest.json +++ b/homeassistant/components/habitica/manifest.json @@ -1,7 +1,7 @@ { "domain": "habitica", "name": "Habitica", - "codeowners": ["@ASMfreaK", "@leikoilja"], + "codeowners": ["@ASMfreaK", "@leikoilja", "@tr4nt0r"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/habitica", "iot_class": "cloud_polling", diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index 4d48ec199ec..7ced7cbf192 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -3,42 +3,123 @@ from __future__ import annotations from collections import namedtuple +from dataclasses import dataclass from datetime import timedelta +from enum import StrEnum from http import HTTPStatus import logging +from typing import TYPE_CHECKING, Any from aiohttp import ClientResponseError -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME +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.util import Throttle -from .const import DOMAIN +from .const import DOMAIN, MANUFACTURER, NAME _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) -SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"]) -SENSORS_TYPES = { - "name": SensorType("Name", None, None, ["profile", "name"]), - "hp": SensorType("HP", "mdi:heart", "HP", ["stats", "hp"]), - "maxHealth": SensorType("max HP", "mdi:heart", "HP", ["stats", "maxHealth"]), - "mp": SensorType("Mana", "mdi:auto-fix", "MP", ["stats", "mp"]), - "maxMP": SensorType("max Mana", "mdi:auto-fix", "MP", ["stats", "maxMP"]), - "exp": SensorType("EXP", "mdi:star", "EXP", ["stats", "exp"]), - "toNextLevel": SensorType("Next Lvl", "mdi:star", "EXP", ["stats", "toNextLevel"]), - "lvl": SensorType( - "Lvl", "mdi:arrow-up-bold-circle-outline", "Lvl", ["stats", "lvl"] +@dataclass(kw_only=True, frozen=True) +class HabitipySensorEntityDescription(SensorEntityDescription): + """Habitipy Sensor Description.""" + + value_path: list[str] + + +class HabitipySensorEntity(StrEnum): + """Habitipy Entities.""" + + DISPLAY_NAME = "display_name" + HEALTH = "health" + HEALTH_MAX = "health_max" + MANA = "mana" + MANA_MAX = "mana_max" + EXPERIENCE = "experience" + EXPERIENCE_MAX = "experience_max" + LEVEL = "level" + GOLD = "gold" + CLASS = "class" + + +SENSOR_DESCRIPTIONS: dict[str, HabitipySensorEntityDescription] = { + HabitipySensorEntity.DISPLAY_NAME: HabitipySensorEntityDescription( + key=HabitipySensorEntity.DISPLAY_NAME, + translation_key=HabitipySensorEntity.DISPLAY_NAME, + value_path=["profile", "name"], + ), + HabitipySensorEntity.HEALTH: HabitipySensorEntityDescription( + key=HabitipySensorEntity.HEALTH, + translation_key=HabitipySensorEntity.HEALTH, + native_unit_of_measurement="HP", + suggested_display_precision=0, + value_path=["stats", "hp"], + ), + HabitipySensorEntity.HEALTH_MAX: HabitipySensorEntityDescription( + key=HabitipySensorEntity.HEALTH_MAX, + translation_key=HabitipySensorEntity.HEALTH_MAX, + native_unit_of_measurement="HP", + entity_registry_enabled_default=False, + value_path=["stats", "maxHealth"], + ), + HabitipySensorEntity.MANA: HabitipySensorEntityDescription( + key=HabitipySensorEntity.MANA, + translation_key=HabitipySensorEntity.MANA, + native_unit_of_measurement="MP", + suggested_display_precision=0, + value_path=["stats", "mp"], + ), + HabitipySensorEntity.MANA_MAX: HabitipySensorEntityDescription( + key=HabitipySensorEntity.MANA_MAX, + translation_key=HabitipySensorEntity.MANA_MAX, + native_unit_of_measurement="MP", + value_path=["stats", "maxMP"], + ), + HabitipySensorEntity.EXPERIENCE: HabitipySensorEntityDescription( + key=HabitipySensorEntity.EXPERIENCE, + translation_key=HabitipySensorEntity.EXPERIENCE, + native_unit_of_measurement="XP", + value_path=["stats", "exp"], + ), + HabitipySensorEntity.EXPERIENCE_MAX: HabitipySensorEntityDescription( + key=HabitipySensorEntity.EXPERIENCE_MAX, + translation_key=HabitipySensorEntity.EXPERIENCE_MAX, + native_unit_of_measurement="XP", + value_path=["stats", "toNextLevel"], + ), + HabitipySensorEntity.LEVEL: HabitipySensorEntityDescription( + key=HabitipySensorEntity.LEVEL, + translation_key=HabitipySensorEntity.LEVEL, + value_path=["stats", "lvl"], + ), + HabitipySensorEntity.GOLD: HabitipySensorEntityDescription( + key=HabitipySensorEntity.GOLD, + translation_key=HabitipySensorEntity.GOLD, + native_unit_of_measurement="GP", + suggested_display_precision=2, + value_path=["stats", "gp"], + ), + HabitipySensorEntity.CLASS: HabitipySensorEntityDescription( + key=HabitipySensorEntity.CLASS, + translation_key=HabitipySensorEntity.CLASS, + value_path=["stats", "class"], + device_class=SensorDeviceClass.ENUM, + options=["warrior", "healer", "wizard", "rogue"], ), - "gp": SensorType("Gold", "mdi:circle-multiple", "Gold", ["stats", "gp"]), - "class": SensorType("Class", "mdi:sword", None, ["stats", "class"]), } +SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"]) TASKS_TYPES = { "habits": SensorType( "Habits", "mdi:clipboard-list-outline", "n_of_tasks", ["habits"] @@ -92,10 +173,12 @@ async def async_setup_entry( await sensor_data.update() entities: list[SensorEntity] = [ - HabitipySensor(name, sensor_type, sensor_data) for sensor_type in SENSORS_TYPES + HabitipySensor(sensor_data, description, config_entry) + for description in SENSOR_DESCRIPTIONS.values() ] entities.extend( - HabitipyTaskSensor(name, task_type, sensor_data) for task_type in TASKS_TYPES + HabitipyTaskSensor(name, task_type, sensor_data, config_entry) + for task_type in TASKS_TYPES ) async_add_entities(entities, True) @@ -103,7 +186,9 @@ async def async_setup_entry( class HabitipyData: """Habitica API user data cache.""" - def __init__(self, api): + tasks: dict[str, Any] + + def __init__(self, api) -> None: """Habitica API user data cache.""" self.api = api self.data = None @@ -153,53 +238,59 @@ class HabitipyData: class HabitipySensor(SensorEntity): """A generic Habitica sensor.""" - def __init__(self, name, sensor_name, updater): + _attr_has_entity_name = True + entity_description: HabitipySensorEntityDescription + + def __init__( + self, + coordinator, + entity_description: HabitipySensorEntityDescription, + entry: ConfigEntry, + ) -> None: """Initialize a generic Habitica sensor.""" - self._name = name - self._sensor_name = sensor_name - self._sensor_type = SENSORS_TYPES[sensor_name] - self._state = None - self._updater = updater + super().__init__() + if TYPE_CHECKING: + assert entry.unique_id + self.coordinator = coordinator + self.entity_description = entity_description + 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)}, + ) async def async_update(self) -> None: - """Update Condition and Forecast.""" - await self._updater.update() - data = self._updater.data - for element in self._sensor_type.path: + """Update Sensor state.""" + await self.coordinator.update() + data = self.coordinator.data + for element in self.entity_description.value_path: data = data[element] - self._state = data - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return self._sensor_type.icon - - @property - def name(self): - """Return the name of the sensor.""" - return f"{DOMAIN}_{self._name}_{self._sensor_name}" - - @property - def native_value(self): - """Return the state of the device.""" - return self._state - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._sensor_type.unit + self._attr_native_value = data class HabitipyTaskSensor(SensorEntity): """A Habitica task sensor.""" - def __init__(self, name, task_name, updater): + def __init__(self, name, task_name, updater, entry): """Initialize a generic Habitica task.""" self._name = name self._task_name = task_name self._task_type = TASKS_TYPES[task_name] self._state = None self._updater = updater + 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)}, + ) async def async_update(self) -> None: """Update Condition and Forecast.""" diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index 8dacb0e6321..6be2bd7ed09 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -19,6 +19,46 @@ } } }, + "entity": { + "sensor": { + "display_name": { + "name": "Display name" + }, + "health": { + "name": "Health" + }, + "health_max": { + "name": "Max. health" + }, + "mana": { + "name": "Mana" + }, + "mana_max": { + "name": "Max. mana" + }, + "experience": { + "name": "Experience" + }, + "experience_max": { + "name": "Next level" + }, + "level": { + "name": "Level" + }, + "gold": { + "name": "Gold" + }, + "class": { + "name": "Class", + "state": { + "warrior": "Warrior", + "healer": "Healer", + "wizard": "Mage", + "rogue": "Rogue" + } + } + } + }, "services": { "api_call": { "name": "API name",