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 9e58053f3b.

* 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 f5c8c3c886.

* Revert "add coordinator.py to coveragerc"

This reverts commit 8ae07a4786.

* rename Mana max. to Max. mana

* deprecation for yaml import

* move SensorType definition before TASK_TYPES

* Revert "deprecation for yaml import"

This reverts commit 2a1d58ee5f.

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
pull/116374/head
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
/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

View File

@ -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(

View File

@ -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"

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

View File

@ -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",

View File

@ -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."""

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