Add icons and translations to Habitica (#116204)

* refactor habitica sensors, add strings and icon translations

* Change sensor names

* remove max_health as it is a fixed value

* remove SENSOR_TYPES

* removed wrong sensor

* Move Data coordinator to separate module

* add coordinator.py to coveragerc

* add deprecation warning for task sensors

* remove unused imports and logger

* Revert "add deprecation warning for task sensors"

This reverts commit 9e58053f3bb8b34b8e22d525bfd1ff55610f4581.

* Update homeassistant/components/habitica/strings.json

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/habitica/strings.json

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Revert "Move Data coordinator to separate module"

This reverts commit f5c8c3c886a868b2ed50ad2098fe3cb1ccc01c62.

* Revert "add coordinator.py to coveragerc"

This reverts commit 8ae07a4786db786a73fc527e525813147d1c5ec4.

* rename Mana max. to Max. mana

* deprecation for yaml import

* move SensorType definition before TASK_TYPES

* Revert "deprecation for yaml import"

This reverts commit 2a1d58ee5ff7d4f1a19b7593cb7f56afde4e1d9d.

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Mr. Bubbles 2024-04-29 12:51:38 +02:00 committed by GitHub
parent b426c4133d
commit 0b8838cab8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 236 additions and 55 deletions

View File

@ -550,8 +550,8 @@ build.json @home-assistant/supervisor
/tests/components/group/ @home-assistant/core /tests/components/group/ @home-assistant/core
/homeassistant/components/guardian/ @bachya /homeassistant/components/guardian/ @bachya
/tests/components/guardian/ @bachya /tests/components/guardian/ @bachya
/homeassistant/components/habitica/ @ASMfreaK @leikoilja /homeassistant/components/habitica/ @ASMfreaK @leikoilja @tr4nt0r
/tests/components/habitica/ @ASMfreaK @leikoilja /tests/components/habitica/ @ASMfreaK @leikoilja @tr4nt0r
/homeassistant/components/hardkernel/ @home-assistant/core /homeassistant/components/hardkernel/ @home-assistant/core
/tests/components/hardkernel/ @home-assistant/core /tests/components/hardkernel/ @home-assistant/core
/homeassistant/components/hardware/ @home-assistant/core /homeassistant/components/hardware/ @home-assistant/core

View File

@ -30,10 +30,11 @@ from .const import (
EVENT_API_CALL_SUCCESS, EVENT_API_CALL_SUCCESS,
SERVICE_API_CALL, SERVICE_API_CALL,
) )
from .sensor import SENSORS_TYPES
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SENSORS_TYPES = ["name", "hp", "maxHealth", "mp", "maxMP", "exp", "toNextLevel", "lvl"]
INSTANCE_SCHEMA = vol.All( INSTANCE_SCHEMA = vol.All(
cv.deprecated(CONF_SENSORS), cv.deprecated(CONF_SENSORS),
vol.Schema( vol.Schema(

View File

@ -15,3 +15,6 @@ ATTR_ARGS = "args"
# event constants # event constants
EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success" EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success"
ATTR_DATA = "data" ATTR_DATA = "data"
MANUFACTURER = "HabitRPG, Inc."
NAME = "Habitica"

View File

@ -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": { "services": {
"api_call": "mdi:console" "api_call": "mdi:console"
} }

View File

@ -1,7 +1,7 @@
{ {
"domain": "habitica", "domain": "habitica",
"name": "Habitica", "name": "Habitica",
"codeowners": ["@ASMfreaK", "@leikoilja"], "codeowners": ["@ASMfreaK", "@leikoilja", "@tr4nt0r"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/habitica", "documentation": "https://www.home-assistant.io/integrations/habitica",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",

View File

@ -3,42 +3,123 @@
from __future__ import annotations from __future__ import annotations
from collections import namedtuple from collections import namedtuple
from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from enum import StrEnum
from http import HTTPStatus from http import HTTPStatus
import logging import logging
from typing import TYPE_CHECKING, Any
from aiohttp import ClientResponseError 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.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.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.util import Throttle from homeassistant.util import Throttle
from .const import DOMAIN from .const import DOMAIN, MANUFACTURER, NAME
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"])
SENSORS_TYPES = { @dataclass(kw_only=True, frozen=True)
"name": SensorType("Name", None, None, ["profile", "name"]), class HabitipySensorEntityDescription(SensorEntityDescription):
"hp": SensorType("HP", "mdi:heart", "HP", ["stats", "hp"]), """Habitipy Sensor Description."""
"maxHealth": SensorType("max HP", "mdi:heart", "HP", ["stats", "maxHealth"]),
"mp": SensorType("Mana", "mdi:auto-fix", "MP", ["stats", "mp"]), value_path: list[str]
"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"]), class HabitipySensorEntity(StrEnum):
"lvl": SensorType( """Habitipy Entities."""
"Lvl", "mdi:arrow-up-bold-circle-outline", "Lvl", ["stats", "lvl"]
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 = { TASKS_TYPES = {
"habits": SensorType( "habits": SensorType(
"Habits", "mdi:clipboard-list-outline", "n_of_tasks", ["habits"] "Habits", "mdi:clipboard-list-outline", "n_of_tasks", ["habits"]
@ -92,10 +173,12 @@ async def async_setup_entry(
await sensor_data.update() await sensor_data.update()
entities: list[SensorEntity] = [ 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( 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) async_add_entities(entities, True)
@ -103,7 +186,9 @@ async def async_setup_entry(
class HabitipyData: class HabitipyData:
"""Habitica API user data cache.""" """Habitica API user data cache."""
def __init__(self, api): tasks: dict[str, Any]
def __init__(self, api) -> None:
"""Habitica API user data cache.""" """Habitica API user data cache."""
self.api = api self.api = api
self.data = None self.data = None
@ -153,53 +238,59 @@ class HabitipyData:
class HabitipySensor(SensorEntity): class HabitipySensor(SensorEntity):
"""A generic Habitica sensor.""" """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.""" """Initialize a generic Habitica sensor."""
self._name = name super().__init__()
self._sensor_name = sensor_name if TYPE_CHECKING:
self._sensor_type = SENSORS_TYPES[sensor_name] assert entry.unique_id
self._state = None self.coordinator = coordinator
self._updater = updater 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: async def async_update(self) -> None:
"""Update Condition and Forecast.""" """Update Sensor state."""
await self._updater.update() await self.coordinator.update()
data = self._updater.data data = self.coordinator.data
for element in self._sensor_type.path: for element in self.entity_description.value_path:
data = data[element] data = data[element]
self._state = data self._attr_native_value = 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
class HabitipyTaskSensor(SensorEntity): class HabitipyTaskSensor(SensorEntity):
"""A Habitica task sensor.""" """A Habitica task sensor."""
def __init__(self, name, task_name, updater): def __init__(self, name, task_name, updater, entry):
"""Initialize a generic Habitica task.""" """Initialize a generic Habitica task."""
self._name = name self._name = name
self._task_name = task_name self._task_name = task_name
self._task_type = TASKS_TYPES[task_name] self._task_type = TASKS_TYPES[task_name]
self._state = None self._state = None
self._updater = updater 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: async def async_update(self) -> None:
"""Update Condition and Forecast.""" """Update Condition and Forecast."""

View File

@ -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": { "services": {
"api_call": { "api_call": {
"name": "API name", "name": "API name",