Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for Habitica sensors."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable, Mapping
6 from dataclasses import dataclass
7 from enum import StrEnum
8 import logging
9 from typing import TYPE_CHECKING, Any
10 
12  DOMAIN as SENSOR_DOMAIN,
13  SensorDeviceClass,
14  SensorEntity,
15  SensorEntityDescription,
16 )
17 from homeassistant.core import HomeAssistant
18 from homeassistant.helpers import entity_registry as er
19 from homeassistant.helpers.entity_platform import AddEntitiesCallback
21  IssueSeverity,
22  async_create_issue,
23  async_delete_issue,
24 )
25 from homeassistant.helpers.typing import StateType
26 
27 from .const import ASSETS_URL, DOMAIN, UNIT_TASKS
28 from .entity import HabiticaBase
29 from .types import HabiticaConfigEntry
30 from .util import entity_used_in, get_attribute_points, get_attributes_total
31 
32 _LOGGER = logging.getLogger(__name__)
33 
34 
35 @dataclass(kw_only=True, frozen=True)
37  """Habitipy Sensor Description."""
38 
39  value_fn: Callable[[dict[str, Any], dict[str, Any]], StateType]
40  attributes_fn: (
41  Callable[[dict[str, Any], dict[str, Any]], dict[str, Any] | None] | None
42  ) = None
43  entity_picture: str | None = None
44 
45 
46 @dataclass(kw_only=True, frozen=True)
48  """Habitipy Task Sensor Description."""
49 
50  value_fn: Callable[[list[dict[str, Any]]], list[dict[str, Any]]]
51 
52 
53 class HabitipySensorEntity(StrEnum):
54  """Habitipy Entities."""
55 
56  DISPLAY_NAME = "display_name"
57  HEALTH = "health"
58  HEALTH_MAX = "health_max"
59  MANA = "mana"
60  MANA_MAX = "mana_max"
61  EXPERIENCE = "experience"
62  EXPERIENCE_MAX = "experience_max"
63  LEVEL = "level"
64  GOLD = "gold"
65  CLASS = "class"
66  HABITS = "habits"
67  DAILIES = "dailys"
68  TODOS = "todos"
69  REWARDS = "rewards"
70  GEMS = "gems"
71  TRINKETS = "trinkets"
72  STRENGTH = "strength"
73  INTELLIGENCE = "intelligence"
74  CONSTITUTION = "constitution"
75  PERCEPTION = "perception"
76 
77 
78 SENSOR_DESCRIPTIONS: tuple[HabitipySensorEntityDescription, ...] = (
80  key=HabitipySensorEntity.DISPLAY_NAME,
81  translation_key=HabitipySensorEntity.DISPLAY_NAME,
82  value_fn=lambda user, _: user.get("profile", {}).get("name"),
83  ),
85  key=HabitipySensorEntity.HEALTH,
86  translation_key=HabitipySensorEntity.HEALTH,
87  native_unit_of_measurement="HP",
88  suggested_display_precision=0,
89  value_fn=lambda user, _: user.get("stats", {}).get("hp"),
90  ),
92  key=HabitipySensorEntity.HEALTH_MAX,
93  translation_key=HabitipySensorEntity.HEALTH_MAX,
94  native_unit_of_measurement="HP",
95  entity_registry_enabled_default=False,
96  value_fn=lambda user, _: user.get("stats", {}).get("maxHealth"),
97  ),
99  key=HabitipySensorEntity.MANA,
100  translation_key=HabitipySensorEntity.MANA,
101  native_unit_of_measurement="MP",
102  suggested_display_precision=0,
103  value_fn=lambda user, _: user.get("stats", {}).get("mp"),
104  ),
106  key=HabitipySensorEntity.MANA_MAX,
107  translation_key=HabitipySensorEntity.MANA_MAX,
108  native_unit_of_measurement="MP",
109  value_fn=lambda user, _: user.get("stats", {}).get("maxMP"),
110  ),
112  key=HabitipySensorEntity.EXPERIENCE,
113  translation_key=HabitipySensorEntity.EXPERIENCE,
114  native_unit_of_measurement="XP",
115  value_fn=lambda user, _: user.get("stats", {}).get("exp"),
116  ),
118  key=HabitipySensorEntity.EXPERIENCE_MAX,
119  translation_key=HabitipySensorEntity.EXPERIENCE_MAX,
120  native_unit_of_measurement="XP",
121  value_fn=lambda user, _: user.get("stats", {}).get("toNextLevel"),
122  ),
124  key=HabitipySensorEntity.LEVEL,
125  translation_key=HabitipySensorEntity.LEVEL,
126  value_fn=lambda user, _: user.get("stats", {}).get("lvl"),
127  ),
129  key=HabitipySensorEntity.GOLD,
130  translation_key=HabitipySensorEntity.GOLD,
131  native_unit_of_measurement="GP",
132  suggested_display_precision=2,
133  value_fn=lambda user, _: user.get("stats", {}).get("gp"),
134  ),
136  key=HabitipySensorEntity.CLASS,
137  translation_key=HabitipySensorEntity.CLASS,
138  value_fn=lambda user, _: user.get("stats", {}).get("class"),
139  device_class=SensorDeviceClass.ENUM,
140  options=["warrior", "healer", "wizard", "rogue"],
141  ),
143  key=HabitipySensorEntity.GEMS,
144  translation_key=HabitipySensorEntity.GEMS,
145  value_fn=lambda user, _: user.get("balance", 0) * 4,
146  suggested_display_precision=0,
147  native_unit_of_measurement="gems",
148  entity_picture="shop_gem.png",
149  ),
151  key=HabitipySensorEntity.TRINKETS,
152  translation_key=HabitipySensorEntity.TRINKETS,
153  value_fn=(
154  lambda user, _: user.get("purchased", {})
155  .get("plan", {})
156  .get("consecutive", {})
157  .get("trinkets", 0)
158  ),
159  suggested_display_precision=0,
160  native_unit_of_measurement="⧖",
161  entity_picture="notif_subscriber_reward.png",
162  ),
164  key=HabitipySensorEntity.STRENGTH,
165  translation_key=HabitipySensorEntity.STRENGTH,
166  value_fn=lambda user, content: get_attributes_total(user, content, "str"),
167  attributes_fn=lambda user, content: get_attribute_points(user, content, "str"),
168  suggested_display_precision=0,
169  native_unit_of_measurement="STR",
170  ),
172  key=HabitipySensorEntity.INTELLIGENCE,
173  translation_key=HabitipySensorEntity.INTELLIGENCE,
174  value_fn=lambda user, content: get_attributes_total(user, content, "int"),
175  attributes_fn=lambda user, content: get_attribute_points(user, content, "int"),
176  suggested_display_precision=0,
177  native_unit_of_measurement="INT",
178  ),
180  key=HabitipySensorEntity.PERCEPTION,
181  translation_key=HabitipySensorEntity.PERCEPTION,
182  value_fn=lambda user, content: get_attributes_total(user, content, "per"),
183  attributes_fn=lambda user, content: get_attribute_points(user, content, "per"),
184  suggested_display_precision=0,
185  native_unit_of_measurement="PER",
186  ),
188  key=HabitipySensorEntity.CONSTITUTION,
189  translation_key=HabitipySensorEntity.CONSTITUTION,
190  value_fn=lambda user, content: get_attributes_total(user, content, "con"),
191  attributes_fn=lambda user, content: get_attribute_points(user, content, "con"),
192  suggested_display_precision=0,
193  native_unit_of_measurement="CON",
194  ),
195 )
196 
197 
198 TASKS_MAP_ID = "id"
199 TASKS_MAP = {
200  "repeat": "repeat",
201  "challenge": "challenge",
202  "group": "group",
203  "frequency": "frequency",
204  "every_x": "everyX",
205  "streak": "streak",
206  "up": "up",
207  "down": "down",
208  "counter_up": "counterUp",
209  "counter_down": "counterDown",
210  "next_due": "nextDue",
211  "yester_daily": "yesterDaily",
212  "completed": "completed",
213  "collapse_checklist": "collapseChecklist",
214  "type": "type",
215  "notes": "notes",
216  "tags": "tags",
217  "value": "value",
218  "priority": "priority",
219  "start_date": "startDate",
220  "days_of_month": "daysOfMonth",
221  "weeks_of_month": "weeksOfMonth",
222  "created_at": "createdAt",
223  "text": "text",
224  "is_due": "isDue",
225 }
226 
227 
228 TASK_SENSOR_DESCRIPTION: tuple[HabitipyTaskSensorEntityDescription, ...] = (
230  key=HabitipySensorEntity.HABITS,
231  translation_key=HabitipySensorEntity.HABITS,
232  native_unit_of_measurement=UNIT_TASKS,
233  value_fn=lambda tasks: [r for r in tasks if r.get("type") == "habit"],
234  ),
236  key=HabitipySensorEntity.DAILIES,
237  translation_key=HabitipySensorEntity.DAILIES,
238  native_unit_of_measurement=UNIT_TASKS,
239  value_fn=lambda tasks: [r for r in tasks if r.get("type") == "daily"],
240  entity_registry_enabled_default=False,
241  ),
243  key=HabitipySensorEntity.TODOS,
244  translation_key=HabitipySensorEntity.TODOS,
245  native_unit_of_measurement=UNIT_TASKS,
246  value_fn=lambda tasks: [
247  r for r in tasks if r.get("type") == "todo" and not r.get("completed")
248  ],
249  entity_registry_enabled_default=False,
250  ),
252  key=HabitipySensorEntity.REWARDS,
253  translation_key=HabitipySensorEntity.REWARDS,
254  native_unit_of_measurement=UNIT_TASKS,
255  value_fn=lambda tasks: [r for r in tasks if r.get("type") == "reward"],
256  ),
257 )
258 
259 
261  hass: HomeAssistant,
262  config_entry: HabiticaConfigEntry,
263  async_add_entities: AddEntitiesCallback,
264 ) -> None:
265  """Set up the habitica sensors."""
266 
267  coordinator = config_entry.runtime_data
268 
269  entities: list[SensorEntity] = [
270  HabitipySensor(coordinator, description) for description in SENSOR_DESCRIPTIONS
271  ]
272  entities.extend(
273  HabitipyTaskSensor(coordinator, description)
274  for description in TASK_SENSOR_DESCRIPTION
275  )
276  async_add_entities(entities, True)
277 
278 
280  """A generic Habitica sensor."""
281 
282  entity_description: HabitipySensorEntityDescription
283 
284  @property
285  def native_value(self) -> StateType:
286  """Return the state of the device."""
287 
288  return self.entity_descriptionentity_description.value_fn(
289  self.coordinator.data.user, self.coordinator.content
290  )
291 
292  @property
293  def extra_state_attributes(self) -> dict[str, float | None] | None:
294  """Return entity specific state attributes."""
295  if func := self.entity_descriptionentity_description.attributes_fn:
296  return func(self.coordinator.data.user, self.coordinator.content)
297  return None
298 
299  @property
300  def entity_picture(self) -> str | None:
301  """Return the entity picture to use in the frontend, if any."""
302  if entity_picture := self.entity_descriptionentity_description.entity_picture:
303  return f"{ASSETS_URL}{entity_picture}"
304  return None
305 
306 
308  """A Habitica task sensor."""
309 
310  entity_description: HabitipyTaskSensorEntityDescription
311 
312  @property
313  def native_value(self) -> StateType:
314  """Return the state of the device."""
315 
316  return len(self.entity_descriptionentity_description.value_fn(self.coordinator.data.tasks))
317 
318  @property
319  def extra_state_attributes(self) -> Mapping[str, Any] | None:
320  """Return the state attributes of all user tasks."""
321  attrs = {}
322 
323  # Map tasks to TASKS_MAP
324  for received_task in self.entity_descriptionentity_description.value_fn(
325  self.coordinator.data.tasks
326  ):
327  task_id = received_task[TASKS_MAP_ID]
328  task = {}
329  for map_key, map_value in TASKS_MAP.items():
330  if value := received_task.get(map_value):
331  task[map_key] = value
332  attrs[task_id] = task
333  return attrs
334 
335  async def async_added_to_hass(self) -> None:
336  """Raise issue when entity is registered and was not disabled."""
337  if TYPE_CHECKING:
338  assert self.unique_idunique_id
339  if entity_id := er.async_get(self.hasshasshass).async_get_entity_id(
340  SENSOR_DOMAIN, DOMAIN, self.unique_idunique_id
341  ):
342  if (
343  self.enabledenabled
344  and self.entity_descriptionentity_description.key
345  in (HabitipySensorEntity.TODOS, HabitipySensorEntity.DAILIES)
346  and entity_used_in(self.hasshasshass, entity_id)
347  ):
349  self.hasshasshass,
350  DOMAIN,
351  f"deprecated_task_entity_{self.entity_description.key}",
352  breaks_in_ha_version="2025.2.0",
353  is_fixable=False,
354  severity=IssueSeverity.WARNING,
355  translation_key="deprecated_task_entity",
356  translation_placeholders={
357  "task_name": str(self.namenamename),
358  "entity": entity_id,
359  },
360  )
361  else:
363  self.hasshasshass,
364  DOMAIN,
365  f"deprecated_task_entity_{self.entity_description.key}",
366  )
367  await super().async_added_to_hass()
dict[str, float|None]|None extra_state_attributes(self)
Definition: sensor.py:293
str|UndefinedType|None name(self)
Definition: entity.py:738
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_setup_entry(HomeAssistant hass, HabiticaConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:264
list[str] entity_used_in(HomeAssistant hass, str entity_id)
Definition: util.py:76
dict[str, float] get_attribute_points(dict[str, Any] user, dict[str, Any] content, str attribute)
Definition: util.py:147
int get_attributes_total(dict[str, Any] user, dict[str, Any] content, str attribute)
Definition: util.py:187
None async_create_issue(HomeAssistant hass, str entry_id)
Definition: repairs.py:69
None async_delete_issue(HomeAssistant hass, str entry_id)
Definition: repairs.py:85