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 commitpull/116374/head9e58053f3b
. * 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 commitf5c8c3c886
. * Revert "add coordinator.py to coveragerc" This reverts commit8ae07a4786
. * rename Mana max. to Max. mana * deprecation for yaml import * move SensorType definition before TASK_TYPES * Revert "deprecation for yaml import" This reverts commit2a1d58ee5f
. --------- Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
parent
b426c4133d
commit
0b8838cab8
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue