1 """Persistently store issues raised by integrations."""
3 from __future__
import annotations
6 from datetime
import datetime
7 from enum
import StrEnum
9 from typing
import Any, Literal, TypedDict, cast
11 from awesomeversion
import AwesomeVersion, AwesomeVersionStrategy
20 from .registry
import BaseRegistry
21 from .singleton
import singleton
22 from .storage
import Store
24 DATA_REGISTRY: HassKey[IssueRegistry] =
HassKey(
"issue_registry")
25 EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED: EventType[EventIssueRegistryUpdatedData] = (
26 EventType(
"repairs_issue_registry_updated")
28 STORAGE_KEY =
"repairs.issue_registry"
29 STORAGE_VERSION_MAJOR = 1
30 STORAGE_VERSION_MINOR = 2
34 """Event data for when the issue registry is updated."""
36 action: Literal[
"create",
"remove",
"update"]
49 @dataclasses.dataclass(slots=True, frozen=True)
51 """Issue Registry Entry."""
54 breaks_in_ha_version: str |
None
56 data: dict[str, str | int | float |
None] |
None
57 dismissed_version: str |
None
59 is_fixable: bool |
None
62 issue_domain: str |
None
64 learn_more_url: str |
None
65 severity: IssueSeverity |
None
66 translation_key: str |
None
67 translation_placeholders: dict[str, str] |
None
70 """Return a JSON serializable representation for storage."""
72 "created": self.created.isoformat(),
73 "dismissed_version": self.dismissed_version,
74 "domain": self.domain,
75 "is_persistent":
False,
76 "issue_id": self.issue_id,
78 if not self.is_persistent:
82 "breaks_in_ha_version": self.breaks_in_ha_version,
84 "is_fixable": self.is_fixable,
85 "is_persistent":
True,
86 "issue_domain": self.issue_domain,
87 "issue_id": self.issue_id,
88 "learn_more_url": self.learn_more_url,
89 "severity": self.severity,
90 "translation_key": self.translation_key,
91 "translation_placeholders": self.translation_placeholders,
96 """Store entity registry data."""
99 self, old_major_version: int, old_minor_version: int, old_data: dict[str, Any]
101 """Migrate to the new version."""
102 if old_major_version == 1
and old_minor_version < 2:
104 for issue
in old_data[
"issues"]:
105 issue[
"is_persistent"] =
False
110 """Class to hold a registry of issues."""
113 """Initialize the issue registry."""
115 self.
issuesissues: dict[tuple[str, str], IssueEntry] = {}
118 STORAGE_VERSION_MAJOR,
121 minor_version=STORAGE_VERSION_MINOR,
126 """Get issue by id."""
127 return self.
issuesissues.
get((domain, issue_id))
135 breaks_in_ha_version: str |
None =
None,
136 data: dict[str, str | int | float |
None] |
None =
None,
139 issue_domain: str |
None =
None,
140 learn_more_url: str |
None =
None,
141 severity: IssueSeverity,
142 translation_key: str,
143 translation_placeholders: dict[str, str] |
None =
None,
145 """Get issue. Create if it doesn't exist."""
146 self.
hasshass.verify_event_loop_thread(
"issue_registry.async_get_or_create")
147 if (issue := self.
async_get_issueasync_get_issue(domain, issue_id))
is None:
150 breaks_in_ha_version=breaks_in_ha_version,
151 created=dt_util.utcnow(),
153 dismissed_version=
None,
155 is_fixable=is_fixable,
156 is_persistent=is_persistent,
157 issue_domain=issue_domain,
159 learn_more_url=learn_more_url,
161 translation_key=translation_key,
162 translation_placeholders=translation_placeholders,
164 self.
issuesissues[(domain, issue_id)] = issue
165 self.async_schedule_save()
166 self.
hasshass.bus.async_fire_internal(
167 EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED,
175 replacement = dataclasses.replace(
178 breaks_in_ha_version=breaks_in_ha_version,
180 is_fixable=is_fixable,
181 is_persistent=is_persistent,
182 issue_domain=issue_domain,
183 learn_more_url=learn_more_url,
185 translation_key=translation_key,
186 translation_placeholders=translation_placeholders,
189 if replacement != issue:
190 issue = self.
issuesissues[(domain, issue_id)] = replacement
191 self.async_schedule_save()
192 self.
hasshass.bus.async_fire_internal(
193 EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED,
206 self.
hasshass.verify_event_loop_thread(
"issue_registry.async_delete")
207 if self.
issuesissues.pop((domain, issue_id),
None)
is None:
210 self.async_schedule_save()
211 self.
hasshass.bus.async_fire_internal(
212 EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED,
221 def async_ignore(self, domain: str, issue_id: str, ignore: bool) -> IssueEntry:
223 self.
hasshass.verify_event_loop_thread(
"issue_registry.async_ignore")
224 old = self.
issuesissues[(domain, issue_id)]
225 dismissed_version = ha_version
if ignore
else None
226 if old.dismissed_version == dismissed_version:
229 issue = self.
issuesissues[(domain, issue_id)] = dataclasses.replace(
231 dismissed_version=dismissed_version,
234 self.async_schedule_save()
235 self.
hasshass.bus.async_fire_internal(
236 EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED,
248 """Make the registry read-only.
250 This method is irreversible.
255 """Load the issue registry."""
258 issues: dict[tuple[str, str], IssueEntry] = {}
260 if isinstance(data, dict):
261 for issue
in data[
"issues"]:
262 created = cast(datetime, dt_util.parse_datetime(issue[
"created"]))
263 if issue[
"is_persistent"]:
264 issues[(issue[
"domain"], issue[
"issue_id"])] =
IssueEntry(
266 breaks_in_ha_version=issue[
"breaks_in_ha_version"],
269 dismissed_version=issue[
"dismissed_version"],
270 domain=issue[
"domain"],
271 is_fixable=issue[
"is_fixable"],
272 is_persistent=issue[
"is_persistent"],
273 issue_id=issue[
"issue_id"],
274 issue_domain=issue[
"issue_domain"],
275 learn_more_url=issue[
"learn_more_url"],
276 severity=issue[
"severity"],
277 translation_key=issue[
"translation_key"],
278 translation_placeholders=issue[
"translation_placeholders"],
281 issues[(issue[
"domain"], issue[
"issue_id"])] =
IssueEntry(
283 breaks_in_ha_version=
None,
286 dismissed_version=issue[
"dismissed_version"],
287 domain=issue[
"domain"],
289 is_persistent=issue[
"is_persistent"],
290 issue_id=issue[
"issue_id"],
294 translation_key=
None,
295 translation_placeholders=
None,
302 """Return data of issue registry to store in a file."""
305 data[
"issues"] = [entry.to_json()
for entry
in self.
issuesissues.values()]
311 @singleton(DATA_REGISTRY)
313 """Get issue registry."""
317 async
def async_load(hass: HomeAssistant, *, read_only: bool =
False) ->
None:
318 """Load issue registry."""
322 return await ir.async_load()
331 breaks_in_ha_version: str |
None =
None,
332 data: dict[str, str | int | float |
None] |
None =
None,
334 is_persistent: bool =
False,
335 issue_domain: str |
None =
None,
336 learn_more_url: str |
None =
None,
337 severity: IssueSeverity,
338 translation_key: str,
339 translation_placeholders: dict[str, str] |
None =
None,
341 """Create an issue, or replace an existing one."""
343 if breaks_in_ha_version:
345 breaks_in_ha_version,
346 ensure_strategy=AwesomeVersionStrategy.CALVER,
350 issue_registry.async_get_or_create(
353 breaks_in_ha_version=breaks_in_ha_version,
355 is_fixable=is_fixable,
356 is_persistent=is_persistent,
357 issue_domain=issue_domain,
358 learn_more_url=learn_more_url,
360 translation_key=translation_key,
361 translation_placeholders=translation_placeholders,
370 breaks_in_ha_version: str |
None =
None,
371 data: dict[str, str | int | float |
None] |
None =
None,
373 is_persistent: bool =
False,
374 issue_domain: str |
None =
None,
375 learn_more_url: str |
None =
None,
376 severity: IssueSeverity,
377 translation_key: str,
378 translation_placeholders: dict[str, str] |
None =
None,
380 """Create an issue, or replace an existing one."""
381 return run_callback_threadsafe(
388 breaks_in_ha_version=breaks_in_ha_version,
390 is_fixable=is_fixable,
391 is_persistent=is_persistent,
392 issue_domain=issue_domain,
393 learn_more_url=learn_more_url,
395 translation_key=translation_key,
396 translation_placeholders=translation_placeholders,
405 It is not an error to delete an issue that does not exist.
408 issue_registry.async_delete(domain, issue_id)
411 def delete_issue(hass: HomeAssistant, domain: str, issue_id: str) ->
None:
414 It is not an error to delete an issue that does not exist.
416 return run_callback_threadsafe(
417 hass.loop, async_delete_issue, hass, domain, issue_id
423 hass: HomeAssistant, domain: str, issue_id: str, ignore: bool
427 Will raise if the issue does not exist.
430 issue_registry.async_ignore(domain, issue_id, ignore)
dict[str, Any] to_json(self)
dict[str, Any] _async_migrate_func(self, int old_major_version, int old_minor_version, dict[str, Any] old_data)
None async_delete(self, str domain, str issue_id)
None make_read_only(self)
IssueEntry|None async_get_issue(self, str domain, str issue_id)
None __init__(self, HomeAssistant hass)
IssueEntry async_get_or_create(self, str domain, str issue_id, *str|None breaks_in_ha_version=None, dict[str, str|int|float|None]|None data=None, bool is_fixable, bool is_persistent, str|None issue_domain=None, str|None learn_more_url=None, IssueSeverity severity, str translation_key, dict[str, str]|None translation_placeholders=None)
IssueEntry async_ignore(self, str domain, str issue_id, bool ignore)
dict[str, list[dict[str, str|None]]] _data_to_save(self)
web.Response get(self, web.Request request, str config_key)
None delete_issue(HomeAssistant hass, str domain, str issue_id)
IssueRegistry async_get(HomeAssistant hass)
None async_create_issue(HomeAssistant hass, str domain, str issue_id, *str|None breaks_in_ha_version=None, dict[str, str|int|float|None]|None data=None, bool is_fixable, bool is_persistent=False, str|None issue_domain=None, str|None learn_more_url=None, IssueSeverity severity, str translation_key, dict[str, str]|None translation_placeholders=None)
None create_issue(HomeAssistant hass, str domain, str issue_id, *str|None breaks_in_ha_version=None, dict[str, str|int|float|None]|None data=None, bool is_fixable, bool is_persistent=False, str|None issue_domain=None, str|None learn_more_url=None, IssueSeverity severity, str translation_key, dict[str, str]|None translation_placeholders=None)
None async_load(HomeAssistant hass, *bool read_only=False)
None async_ignore_issue(HomeAssistant hass, str domain, str issue_id, bool ignore)
None async_delete_issue(HomeAssistant hass, str domain, str issue_id)