Home Assistant Unofficial Reference 2024.12.1
button.py
Go to the documentation of this file.
1 """Habitica button platform."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from dataclasses import dataclass
7 from enum import StrEnum
8 from http import HTTPStatus
9 from typing import Any
10 
11 from aiohttp import ClientResponseError
12 
14  DOMAIN as BUTTON_DOMAIN,
15  ButtonEntity,
16  ButtonEntityDescription,
17 )
18 from homeassistant.core import HomeAssistant, callback
19 from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
20 from homeassistant.helpers import entity_registry as er
21 from homeassistant.helpers.entity_platform import AddEntitiesCallback
22 
23 from .const import ASSETS_URL, DOMAIN, HEALER, MAGE, ROGUE, WARRIOR
24 from .coordinator import HabiticaData, HabiticaDataUpdateCoordinator
25 from .entity import HabiticaBase
26 from .types import HabiticaConfigEntry
27 
28 PARALLEL_UPDATES = 1
29 
30 
31 @dataclass(kw_only=True, frozen=True)
33  """Describes Habitica button entity."""
34 
35  press_fn: Callable[[HabiticaDataUpdateCoordinator], Any]
36  available_fn: Callable[[HabiticaData], bool] | None = None
37  class_needed: str | None = None
38  entity_picture: str | None = None
39 
40 
41 class HabitipyButtonEntity(StrEnum):
42  """Habitica button entities."""
43 
44  RUN_CRON = "run_cron"
45  BUY_HEALTH_POTION = "buy_health_potion"
46  ALLOCATE_ALL_STAT_POINTS = "allocate_all_stat_points"
47  REVIVE = "revive"
48  MPHEAL = "mpheal"
49  EARTH = "earth"
50  FROST = "frost"
51  DEFENSIVE_STANCE = "defensive_stance"
52  VALOROUS_PRESENCE = "valorous_presence"
53  INTIMIDATE = "intimidate"
54  TOOLS_OF_TRADE = "tools_of_trade"
55  STEALTH = "stealth"
56  HEAL = "heal"
57  PROTECT_AURA = "protect_aura"
58  BRIGHTNESS = "brightness"
59  HEAL_ALL = "heal_all"
60 
61 
62 BUTTON_DESCRIPTIONS: tuple[HabiticaButtonEntityDescription, ...] = (
64  key=HabitipyButtonEntity.RUN_CRON,
65  translation_key=HabitipyButtonEntity.RUN_CRON,
66  press_fn=lambda coordinator: coordinator.api.cron.post(),
67  available_fn=lambda data: data.user["needsCron"],
68  ),
70  key=HabitipyButtonEntity.BUY_HEALTH_POTION,
71  translation_key=HabitipyButtonEntity.BUY_HEALTH_POTION,
72  press_fn=(
73  lambda coordinator: coordinator.api["user"]["buy-health-potion"].post()
74  ),
75  available_fn=(
76  lambda data: data.user["stats"]["gp"] >= 25
77  and data.user["stats"]["hp"] < 50
78  ),
79  entity_picture="shop_potion.png",
80  ),
82  key=HabitipyButtonEntity.ALLOCATE_ALL_STAT_POINTS,
83  translation_key=HabitipyButtonEntity.ALLOCATE_ALL_STAT_POINTS,
84  press_fn=lambda coordinator: coordinator.api["user"]["allocate-now"].post(),
85  available_fn=(
86  lambda data: data.user["preferences"].get("automaticAllocation") is True
87  and data.user["stats"]["points"] > 0
88  ),
89  ),
91  key=HabitipyButtonEntity.REVIVE,
92  translation_key=HabitipyButtonEntity.REVIVE,
93  press_fn=lambda coordinator: coordinator.api["user"]["revive"].post(),
94  available_fn=lambda data: data.user["stats"]["hp"] == 0,
95  ),
96 )
97 
98 
99 CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = (
101  key=HabitipyButtonEntity.MPHEAL,
102  translation_key=HabitipyButtonEntity.MPHEAL,
103  press_fn=lambda coordinator: coordinator.api.user.class_.cast["mpheal"].post(),
104  available_fn=(
105  lambda data: data.user["stats"]["lvl"] >= 12
106  and data.user["stats"]["mp"] >= 30
107  ),
108  class_needed=MAGE,
109  entity_picture="shop_mpheal.png",
110  ),
112  key=HabitipyButtonEntity.EARTH,
113  translation_key=HabitipyButtonEntity.EARTH,
114  press_fn=lambda coordinator: coordinator.api.user.class_.cast["earth"].post(),
115  available_fn=(
116  lambda data: data.user["stats"]["lvl"] >= 13
117  and data.user["stats"]["mp"] >= 35
118  ),
119  class_needed=MAGE,
120  entity_picture="shop_earth.png",
121  ),
123  key=HabitipyButtonEntity.FROST,
124  translation_key=HabitipyButtonEntity.FROST,
125  press_fn=lambda coordinator: coordinator.api.user.class_.cast["frost"].post(),
126  # chilling frost can only be cast once per day (streaks buff is false)
127  available_fn=(
128  lambda data: data.user["stats"]["lvl"] >= 14
129  and data.user["stats"]["mp"] >= 40
130  and not data.user["stats"]["buffs"]["streaks"]
131  ),
132  class_needed=MAGE,
133  entity_picture="shop_frost.png",
134  ),
136  key=HabitipyButtonEntity.DEFENSIVE_STANCE,
137  translation_key=HabitipyButtonEntity.DEFENSIVE_STANCE,
138  press_fn=(
139  lambda coordinator: coordinator.api.user.class_.cast[
140  "defensiveStance"
141  ].post()
142  ),
143  available_fn=(
144  lambda data: data.user["stats"]["lvl"] >= 12
145  and data.user["stats"]["mp"] >= 25
146  ),
147  class_needed=WARRIOR,
148  entity_picture="shop_defensiveStance.png",
149  ),
151  key=HabitipyButtonEntity.VALOROUS_PRESENCE,
152  translation_key=HabitipyButtonEntity.VALOROUS_PRESENCE,
153  press_fn=(
154  lambda coordinator: coordinator.api.user.class_.cast[
155  "valorousPresence"
156  ].post()
157  ),
158  available_fn=(
159  lambda data: data.user["stats"]["lvl"] >= 13
160  and data.user["stats"]["mp"] >= 20
161  ),
162  class_needed=WARRIOR,
163  entity_picture="shop_valorousPresence.png",
164  ),
166  key=HabitipyButtonEntity.INTIMIDATE,
167  translation_key=HabitipyButtonEntity.INTIMIDATE,
168  press_fn=(
169  lambda coordinator: coordinator.api.user.class_.cast["intimidate"].post()
170  ),
171  available_fn=(
172  lambda data: data.user["stats"]["lvl"] >= 14
173  and data.user["stats"]["mp"] >= 15
174  ),
175  class_needed=WARRIOR,
176  entity_picture="shop_intimidate.png",
177  ),
179  key=HabitipyButtonEntity.TOOLS_OF_TRADE,
180  translation_key=HabitipyButtonEntity.TOOLS_OF_TRADE,
181  press_fn=(
182  lambda coordinator: coordinator.api.user.class_.cast["toolsOfTrade"].post()
183  ),
184  available_fn=(
185  lambda data: data.user["stats"]["lvl"] >= 13
186  and data.user["stats"]["mp"] >= 25
187  ),
188  class_needed=ROGUE,
189  entity_picture="shop_toolsOfTrade.png",
190  ),
192  key=HabitipyButtonEntity.STEALTH,
193  translation_key=HabitipyButtonEntity.STEALTH,
194  press_fn=(
195  lambda coordinator: coordinator.api.user.class_.cast["stealth"].post()
196  ),
197  # Stealth buffs stack and it can only be cast if the amount of
198  # unfinished dailies is smaller than the amount of buffs
199  available_fn=(
200  lambda data: data.user["stats"]["lvl"] >= 14
201  and data.user["stats"]["mp"] >= 45
202  and data.user["stats"]["buffs"]["stealth"]
203  < len(
204  [
205  r
206  for r in data.tasks
207  if r.get("type") == "daily"
208  and r.get("isDue") is True
209  and r.get("completed") is False
210  ]
211  )
212  ),
213  class_needed=ROGUE,
214  entity_picture="shop_stealth.png",
215  ),
217  key=HabitipyButtonEntity.HEAL,
218  translation_key=HabitipyButtonEntity.HEAL,
219  press_fn=lambda coordinator: coordinator.api.user.class_.cast["heal"].post(),
220  available_fn=(
221  lambda data: data.user["stats"]["lvl"] >= 11
222  and data.user["stats"]["mp"] >= 15
223  and data.user["stats"]["hp"] < 50
224  ),
225  class_needed=HEALER,
226  entity_picture="shop_heal.png",
227  ),
229  key=HabitipyButtonEntity.BRIGHTNESS,
230  translation_key=HabitipyButtonEntity.BRIGHTNESS,
231  press_fn=(
232  lambda coordinator: coordinator.api.user.class_.cast["brightness"].post()
233  ),
234  available_fn=(
235  lambda data: data.user["stats"]["lvl"] >= 12
236  and data.user["stats"]["mp"] >= 15
237  ),
238  class_needed=HEALER,
239  entity_picture="shop_brightness.png",
240  ),
242  key=HabitipyButtonEntity.PROTECT_AURA,
243  translation_key=HabitipyButtonEntity.PROTECT_AURA,
244  press_fn=(
245  lambda coordinator: coordinator.api.user.class_.cast["protectAura"].post()
246  ),
247  available_fn=(
248  lambda data: data.user["stats"]["lvl"] >= 13
249  and data.user["stats"]["mp"] >= 30
250  ),
251  class_needed=HEALER,
252  entity_picture="shop_protectAura.png",
253  ),
255  key=HabitipyButtonEntity.HEAL_ALL,
256  translation_key=HabitipyButtonEntity.HEAL_ALL,
257  press_fn=lambda coordinator: coordinator.api.user.class_.cast["healAll"].post(),
258  available_fn=(
259  lambda data: data.user["stats"]["lvl"] >= 14
260  and data.user["stats"]["mp"] >= 25
261  ),
262  class_needed=HEALER,
263  entity_picture="shop_healAll.png",
264  ),
265 )
266 
267 
269  hass: HomeAssistant,
270  entry: HabiticaConfigEntry,
271  async_add_entities: AddEntitiesCallback,
272 ) -> None:
273  """Set up buttons from a config entry."""
274 
275  coordinator = entry.runtime_data
276  skills_added: set[str] = set()
277 
278  @callback
279  def add_entities() -> None:
280  """Add or remove a skillset based on the player's class."""
281 
282  nonlocal skills_added
283  buttons = []
284  entity_registry = er.async_get(hass)
285 
286  for description in CLASS_SKILLS:
287  if (
288  coordinator.data.user["stats"]["lvl"] >= 10
289  and coordinator.data.user["flags"]["classSelected"]
290  and not coordinator.data.user["preferences"]["disableClasses"]
291  and description.class_needed == coordinator.data.user["stats"]["class"]
292  ):
293  if description.key not in skills_added:
294  buttons.append(HabiticaButton(coordinator, description))
295  skills_added.add(description.key)
296  elif description.key in skills_added:
297  if entity_id := entity_registry.async_get_entity_id(
298  BUTTON_DOMAIN,
299  DOMAIN,
300  f"{coordinator.config_entry.unique_id}_{description.key}",
301  ):
302  entity_registry.async_remove(entity_id)
303  skills_added.remove(description.key)
304 
305  if buttons:
306  async_add_entities(buttons)
307 
308  coordinator.async_add_listener(add_entities)
309  add_entities()
310 
312  HabiticaButton(coordinator, description) for description in BUTTON_DESCRIPTIONS
313  )
314 
315 
317  """Representation of a Habitica button."""
318 
319  entity_description: HabiticaButtonEntityDescription
320 
321  async def async_press(self) -> None:
322  """Handle the button press."""
323  try:
324  await self.entity_descriptionentity_description.press_fn(self.coordinator)
325  except ClientResponseError as e:
326  if e.status == HTTPStatus.TOO_MANY_REQUESTS:
328  translation_domain=DOMAIN,
329  translation_key="setup_rate_limit_exception",
330  ) from e
331  if e.status == HTTPStatus.UNAUTHORIZED:
333  translation_domain=DOMAIN,
334  translation_key="service_call_unallowed",
335  ) from e
336  raise HomeAssistantError(
337  translation_domain=DOMAIN,
338  translation_key="service_call_exception",
339  ) from e
340  else:
341  await self.coordinator.async_request_refresh()
342 
343  @property
344  def available(self) -> bool:
345  """Is entity available."""
346  if not super().available:
347  return False
348  if self.entity_descriptionentity_description.available_fn:
349  return self.entity_descriptionentity_description.available_fn(self.coordinator.data)
350  return True
351 
352  @property
353  def entity_picture(self) -> str | None:
354  """Return the entity picture to use in the frontend, if any."""
355  if entity_picture := self.entity_descriptionentity_description.entity_picture:
356  return f"{ASSETS_URL}{entity_picture}"
357  return None
None add_entities(AsusWrtRouter router, AddEntitiesCallback async_add_entities, set[str] tracked)
web.Response post(self, web.Request request, str config_key)
Definition: view.py:101
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_setup_entry(HomeAssistant hass, HabiticaConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: button.py:272