mirror of
https://github.com/home-assistant/core
synced 2024-10-04 20:52:24 +00:00
Resolution center MVP (#74243)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
parent
405d323709
commit
0e3f7bc63a
|
@ -191,6 +191,7 @@ homeassistant.components.recollect_waste.*
|
||||||
homeassistant.components.recorder.*
|
homeassistant.components.recorder.*
|
||||||
homeassistant.components.remote.*
|
homeassistant.components.remote.*
|
||||||
homeassistant.components.renault.*
|
homeassistant.components.renault.*
|
||||||
|
homeassistant.components.resolution_center.*
|
||||||
homeassistant.components.ridwell.*
|
homeassistant.components.ridwell.*
|
||||||
homeassistant.components.rituals_perfume_genie.*
|
homeassistant.components.rituals_perfume_genie.*
|
||||||
homeassistant.components.roku.*
|
homeassistant.components.roku.*
|
||||||
|
|
|
@ -857,6 +857,8 @@ build.json @home-assistant/supervisor
|
||||||
/homeassistant/components/renault/ @epenet
|
/homeassistant/components/renault/ @epenet
|
||||||
/tests/components/renault/ @epenet
|
/tests/components/renault/ @epenet
|
||||||
/homeassistant/components/repetier/ @MTrab @ShadowBr0ther
|
/homeassistant/components/repetier/ @MTrab @ShadowBr0ther
|
||||||
|
/homeassistant/components/resolution_center/ @home-assistant/core
|
||||||
|
/tests/components/resolution_center/ @home-assistant/core
|
||||||
/homeassistant/components/rflink/ @javicalle
|
/homeassistant/components/rflink/ @javicalle
|
||||||
/tests/components/rflink/ @javicalle
|
/tests/components/rflink/ @javicalle
|
||||||
/homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221
|
/homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221
|
||||||
|
|
20
homeassistant/components/resolution_center/__init__.py
Normal file
20
homeassistant/components/resolution_center/__init__.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
"""The resolution center integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
|
from . import websocket_api
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .issue_handler import async_create_issue, async_delete_issue
|
||||||
|
from .issue_registry import async_load as async_load_issue_registry
|
||||||
|
|
||||||
|
__all__ = ["DOMAIN", "async_create_issue", "async_delete_issue"]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up Resolution Center."""
|
||||||
|
websocket_api.async_setup(hass)
|
||||||
|
await async_load_issue_registry(hass)
|
||||||
|
|
||||||
|
return True
|
3
homeassistant/components/resolution_center/const.py
Normal file
3
homeassistant/components/resolution_center/const.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
"""Constants for the Resolution Center integration."""
|
||||||
|
|
||||||
|
DOMAIN = "resolution_center"
|
62
homeassistant/components/resolution_center/issue_handler.py
Normal file
62
homeassistant/components/resolution_center/issue_handler.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
"""The resolution center integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
|
from .issue_registry import async_get as async_get_issue_registry
|
||||||
|
from .models import IssueSeverity
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_create_issue(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
domain: str,
|
||||||
|
issue_id: str,
|
||||||
|
*,
|
||||||
|
breaks_in_ha_version: str | None = None,
|
||||||
|
learn_more_url: str | None = None,
|
||||||
|
severity: IssueSeverity,
|
||||||
|
translation_key: str,
|
||||||
|
translation_placeholders: dict[str, str] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Create an issue, or replace an existing one."""
|
||||||
|
# Verify the breaks_in_ha_version is a valid version string
|
||||||
|
if breaks_in_ha_version:
|
||||||
|
AwesomeVersion(
|
||||||
|
breaks_in_ha_version,
|
||||||
|
ensure_strategy=AwesomeVersionStrategy.CALVER,
|
||||||
|
find_first_match=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
issue_registry = async_get_issue_registry(hass)
|
||||||
|
issue_registry.async_get_or_create(
|
||||||
|
domain,
|
||||||
|
issue_id,
|
||||||
|
breaks_in_ha_version=breaks_in_ha_version,
|
||||||
|
learn_more_url=learn_more_url,
|
||||||
|
severity=severity,
|
||||||
|
translation_key=translation_key,
|
||||||
|
translation_placeholders=translation_placeholders,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None:
|
||||||
|
"""Delete an issue.
|
||||||
|
|
||||||
|
It is not an error to delete an issue that does not exist.
|
||||||
|
"""
|
||||||
|
issue_registry = async_get_issue_registry(hass)
|
||||||
|
issue_registry.async_delete(domain, issue_id)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_dismiss_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None:
|
||||||
|
"""Dismiss an issue.
|
||||||
|
|
||||||
|
Will raise if the issue does not exist.
|
||||||
|
"""
|
||||||
|
issue_registry = async_get_issue_registry(hass)
|
||||||
|
issue_registry.async_dismiss(domain, issue_id)
|
164
homeassistant/components/resolution_center/issue_registry.py
Normal file
164
homeassistant/components/resolution_center/issue_registry.py
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
"""Persistently store issues raised by integrations."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from homeassistant.const import __version__ as ha_version
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.storage import Store
|
||||||
|
|
||||||
|
from .models import IssueSeverity
|
||||||
|
|
||||||
|
DATA_REGISTRY = "issue_registry"
|
||||||
|
STORAGE_KEY = "resolution_center.issue_registry"
|
||||||
|
STORAGE_VERSION = 1
|
||||||
|
SAVE_DELAY = 10
|
||||||
|
SAVED_FIELDS = ("dismissed_version", "domain", "issue_id")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class IssueEntry:
|
||||||
|
"""Issue Registry Entry."""
|
||||||
|
|
||||||
|
active: bool
|
||||||
|
breaks_in_ha_version: str | None
|
||||||
|
dismissed_version: str | None
|
||||||
|
domain: str
|
||||||
|
issue_id: str
|
||||||
|
learn_more_url: str | None
|
||||||
|
severity: IssueSeverity | None
|
||||||
|
translation_key: str | None
|
||||||
|
translation_placeholders: dict[str, str] | None
|
||||||
|
|
||||||
|
|
||||||
|
class IssueRegistry:
|
||||||
|
"""Class to hold a registry of issues."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
|
"""Initialize the issue registry."""
|
||||||
|
self.hass = hass
|
||||||
|
self.issues: dict[tuple[str, str], IssueEntry] = {}
|
||||||
|
self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_get_issue(self, domain: str, issue_id: str) -> IssueEntry | None:
|
||||||
|
"""Get issue by id."""
|
||||||
|
return self.issues.get((domain, issue_id))
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_get_or_create(
|
||||||
|
self,
|
||||||
|
domain: str,
|
||||||
|
issue_id: str,
|
||||||
|
*,
|
||||||
|
breaks_in_ha_version: str | None = None,
|
||||||
|
learn_more_url: str | None = None,
|
||||||
|
severity: IssueSeverity,
|
||||||
|
translation_key: str,
|
||||||
|
translation_placeholders: dict[str, str] | None = None,
|
||||||
|
) -> IssueEntry:
|
||||||
|
"""Get issue. Create if it doesn't exist."""
|
||||||
|
|
||||||
|
if (issue := self.async_get_issue(domain, issue_id)) is None:
|
||||||
|
issue = IssueEntry(
|
||||||
|
active=True,
|
||||||
|
breaks_in_ha_version=breaks_in_ha_version,
|
||||||
|
dismissed_version=None,
|
||||||
|
domain=domain,
|
||||||
|
issue_id=issue_id,
|
||||||
|
learn_more_url=learn_more_url,
|
||||||
|
severity=severity,
|
||||||
|
translation_key=translation_key,
|
||||||
|
translation_placeholders=translation_placeholders,
|
||||||
|
)
|
||||||
|
self.issues[(domain, issue_id)] = issue
|
||||||
|
self.async_schedule_save()
|
||||||
|
else:
|
||||||
|
issue = self.issues[(domain, issue_id)] = dataclasses.replace(
|
||||||
|
issue,
|
||||||
|
active=True,
|
||||||
|
breaks_in_ha_version=breaks_in_ha_version,
|
||||||
|
learn_more_url=learn_more_url,
|
||||||
|
severity=severity,
|
||||||
|
translation_key=translation_key,
|
||||||
|
translation_placeholders=translation_placeholders,
|
||||||
|
)
|
||||||
|
|
||||||
|
return issue
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_delete(self, domain: str, issue_id: str) -> None:
|
||||||
|
"""Delete issue."""
|
||||||
|
if self.issues.pop((domain, issue_id), None) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.async_schedule_save()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_dismiss(self, domain: str, issue_id: str) -> IssueEntry:
|
||||||
|
"""Dismiss issue."""
|
||||||
|
old = self.issues[(domain, issue_id)]
|
||||||
|
if old.dismissed_version == ha_version:
|
||||||
|
return old
|
||||||
|
|
||||||
|
issue = self.issues[(domain, issue_id)] = dataclasses.replace(
|
||||||
|
old,
|
||||||
|
dismissed_version=ha_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.async_schedule_save()
|
||||||
|
|
||||||
|
return issue
|
||||||
|
|
||||||
|
async def async_load(self) -> None:
|
||||||
|
"""Load the issue registry."""
|
||||||
|
data = await self._store.async_load()
|
||||||
|
|
||||||
|
issues: dict[tuple[str, str], IssueEntry] = {}
|
||||||
|
|
||||||
|
if isinstance(data, dict):
|
||||||
|
for issue in data["issues"]:
|
||||||
|
issues[(issue["domain"], issue["issue_id"])] = IssueEntry(
|
||||||
|
active=False,
|
||||||
|
breaks_in_ha_version=None,
|
||||||
|
dismissed_version=issue["dismissed_version"],
|
||||||
|
domain=issue["domain"],
|
||||||
|
issue_id=issue["issue_id"],
|
||||||
|
learn_more_url=None,
|
||||||
|
severity=None,
|
||||||
|
translation_key=None,
|
||||||
|
translation_placeholders=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.issues = issues
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_schedule_save(self) -> None:
|
||||||
|
"""Schedule saving the issue registry."""
|
||||||
|
self._store.async_delay_save(self._data_to_save, SAVE_DELAY)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _data_to_save(self) -> dict[str, list[dict[str, str | None]]]:
|
||||||
|
"""Return data of issue registry to store in a file."""
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
data["issues"] = [
|
||||||
|
{field: getattr(entry, field) for field in SAVED_FIELDS}
|
||||||
|
for entry in self.issues.values()
|
||||||
|
]
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_get(hass: HomeAssistant) -> IssueRegistry:
|
||||||
|
"""Get issue registry."""
|
||||||
|
return cast(IssueRegistry, hass.data[DATA_REGISTRY])
|
||||||
|
|
||||||
|
|
||||||
|
async def async_load(hass: HomeAssistant) -> None:
|
||||||
|
"""Load issue registry."""
|
||||||
|
assert DATA_REGISTRY not in hass.data
|
||||||
|
hass.data[DATA_REGISTRY] = IssueRegistry(hass)
|
||||||
|
await hass.data[DATA_REGISTRY].async_load()
|
7
homeassistant/components/resolution_center/manifest.json
Normal file
7
homeassistant/components/resolution_center/manifest.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"domain": "resolution_center",
|
||||||
|
"name": "Resolution Center",
|
||||||
|
"config_flow": false,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/resolution_center",
|
||||||
|
"codeowners": ["@home-assistant/core"]
|
||||||
|
}
|
12
homeassistant/components/resolution_center/models.py
Normal file
12
homeassistant/components/resolution_center/models.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
"""Models for Resolution Center."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.backports.enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
|
class IssueSeverity(StrEnum):
|
||||||
|
"""Issue severity."""
|
||||||
|
|
||||||
|
CRITICAL = "critical"
|
||||||
|
ERROR = "error"
|
||||||
|
WARNING = "warning"
|
62
homeassistant/components/resolution_center/websocket_api.py
Normal file
62
homeassistant/components/resolution_center/websocket_api.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
"""The resolution center websocket API."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components import websocket_api
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
|
from .issue_handler import async_dismiss_issue
|
||||||
|
from .issue_registry import async_get as async_get_issue_registry
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_setup(hass: HomeAssistant) -> None:
|
||||||
|
"""Set up the resolution center websocket API."""
|
||||||
|
websocket_api.async_register_command(hass, ws_dismiss_issue)
|
||||||
|
websocket_api.async_register_command(hass, ws_list_issues)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "resolution_center/dismiss_issue",
|
||||||
|
vol.Required("domain"): str,
|
||||||
|
vol.Required("issue_id"): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def ws_dismiss_issue(
|
||||||
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
|
||||||
|
) -> None:
|
||||||
|
"""Fix an issue."""
|
||||||
|
async_dismiss_issue(hass, msg["domain"], msg["issue_id"])
|
||||||
|
|
||||||
|
connection.send_result(msg["id"])
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "resolution_center/list_issues",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@callback
|
||||||
|
def ws_list_issues(
|
||||||
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
|
||||||
|
) -> None:
|
||||||
|
"""Return a list of issues."""
|
||||||
|
|
||||||
|
def ws_dict(kv_pairs: list[tuple[Any, Any]]) -> dict[Any, Any]:
|
||||||
|
result = {k: v for k, v in kv_pairs if k != "active"}
|
||||||
|
result["dismissed"] = result["dismissed_version"] is not None
|
||||||
|
return result
|
||||||
|
|
||||||
|
issue_registry = async_get_issue_registry(hass)
|
||||||
|
issues = [
|
||||||
|
dataclasses.asdict(issue, dict_factory=ws_dict)
|
||||||
|
for issue in issue_registry.issues.values()
|
||||||
|
]
|
||||||
|
|
||||||
|
connection.send_result(msg["id"], {"issues": issues})
|
11
mypy.ini
11
mypy.ini
|
@ -1864,6 +1864,17 @@ no_implicit_optional = true
|
||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = true
|
warn_unreachable = true
|
||||||
|
|
||||||
|
[mypy-homeassistant.components.resolution_center.*]
|
||||||
|
check_untyped_defs = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
disallow_subclassing_any = true
|
||||||
|
disallow_untyped_calls = true
|
||||||
|
disallow_untyped_decorators = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
no_implicit_optional = true
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unreachable = true
|
||||||
|
|
||||||
[mypy-homeassistant.components.ridwell.*]
|
[mypy-homeassistant.components.ridwell.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
|
|
@ -80,6 +80,7 @@ NO_IOT_CLASS = [
|
||||||
"proxy",
|
"proxy",
|
||||||
"python_script",
|
"python_script",
|
||||||
"raspberry_pi",
|
"raspberry_pi",
|
||||||
|
"resolution_center",
|
||||||
"safe_mode",
|
"safe_mode",
|
||||||
"script",
|
"script",
|
||||||
"search",
|
"search",
|
||||||
|
|
1
tests/components/resolution_center/__init__.py
Normal file
1
tests/components/resolution_center/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for the resolution center integration."""
|
346
tests/components/resolution_center/test_init.py
Normal file
346
tests/components/resolution_center/test_init.py
Normal file
|
@ -0,0 +1,346 @@
|
||||||
|
"""Test the resolution center websocket API."""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.resolution_center import (
|
||||||
|
async_create_issue,
|
||||||
|
async_delete_issue,
|
||||||
|
)
|
||||||
|
from homeassistant.components.resolution_center.const import DOMAIN
|
||||||
|
from homeassistant.components.resolution_center.issue_handler import async_dismiss_issue
|
||||||
|
from homeassistant.const import __version__ as ha_version
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
||||||
|
"""Test creating and updating issues."""
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
|
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
await client.send_json({"id": 1, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {"issues": []}
|
||||||
|
|
||||||
|
issues = [
|
||||||
|
{
|
||||||
|
"breaks_in_ha_version": "2022.9.0dev0",
|
||||||
|
"domain": "test",
|
||||||
|
"issue_id": "issue_1",
|
||||||
|
"learn_more_url": "https://theuselessweb.com",
|
||||||
|
"severity": "error",
|
||||||
|
"translation_key": "abc_123",
|
||||||
|
"translation_placeholders": {"abc": "123"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"breaks_in_ha_version": "2022.8",
|
||||||
|
"domain": "test",
|
||||||
|
"issue_id": "issue_2",
|
||||||
|
"learn_more_url": "https://theuselessweb.com/abc",
|
||||||
|
"severity": "other",
|
||||||
|
"translation_key": "even_worse",
|
||||||
|
"translation_placeholders": {"def": "456"},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for issue in issues:
|
||||||
|
async_create_issue(
|
||||||
|
hass,
|
||||||
|
issue["domain"],
|
||||||
|
issue["issue_id"],
|
||||||
|
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
||||||
|
learn_more_url=issue["learn_more_url"],
|
||||||
|
severity=issue["severity"],
|
||||||
|
translation_key=issue["translation_key"],
|
||||||
|
translation_placeholders=issue["translation_placeholders"],
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.send_json({"id": 2, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {
|
||||||
|
"issues": [
|
||||||
|
dict(
|
||||||
|
issue,
|
||||||
|
dismissed=False,
|
||||||
|
dismissed_version=None,
|
||||||
|
)
|
||||||
|
for issue in issues
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update an issue
|
||||||
|
async_create_issue(
|
||||||
|
hass,
|
||||||
|
issues[0]["domain"],
|
||||||
|
issues[0]["issue_id"],
|
||||||
|
breaks_in_ha_version=issues[0]["breaks_in_ha_version"],
|
||||||
|
learn_more_url="blablabla",
|
||||||
|
severity=issues[0]["severity"],
|
||||||
|
translation_key=issues[0]["translation_key"],
|
||||||
|
translation_placeholders=issues[0]["translation_placeholders"],
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.send_json({"id": 3, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"]["issues"][0] == dict(
|
||||||
|
issues[0],
|
||||||
|
dismissed=False,
|
||||||
|
dismissed_version=None,
|
||||||
|
learn_more_url="blablabla",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("ha_version", ("2022.9.cat", "In the future: 2023.1.1"))
|
||||||
|
async def test_create_issue_invalid_version(
|
||||||
|
hass: HomeAssistant, hass_ws_client, ha_version
|
||||||
|
) -> None:
|
||||||
|
"""Test creating an issue with invalid breaks in version."""
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
|
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
issue = {
|
||||||
|
"breaks_in_ha_version": ha_version,
|
||||||
|
"domain": "test",
|
||||||
|
"issue_id": "issue_1",
|
||||||
|
"learn_more_url": "https://theuselessweb.com",
|
||||||
|
"severity": "error",
|
||||||
|
"translation_key": "abc_123",
|
||||||
|
"translation_placeholders": {"abc": "123"},
|
||||||
|
}
|
||||||
|
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
async_create_issue(
|
||||||
|
hass,
|
||||||
|
issue["domain"],
|
||||||
|
issue["issue_id"],
|
||||||
|
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
||||||
|
learn_more_url=issue["learn_more_url"],
|
||||||
|
severity=issue["severity"],
|
||||||
|
translation_key=issue["translation_key"],
|
||||||
|
translation_placeholders=issue["translation_placeholders"],
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.send_json({"id": 1, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {"issues": []}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
||||||
|
"""Test dismissing issues."""
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
|
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
await client.send_json({"id": 1, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {"issues": []}
|
||||||
|
|
||||||
|
issues = [
|
||||||
|
{
|
||||||
|
"breaks_in_ha_version": "2022.9",
|
||||||
|
"domain": "test",
|
||||||
|
"issue_id": "issue_1",
|
||||||
|
"learn_more_url": "https://theuselessweb.com",
|
||||||
|
"severity": "error",
|
||||||
|
"translation_key": "abc_123",
|
||||||
|
"translation_placeholders": {"abc": "123"},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for issue in issues:
|
||||||
|
async_create_issue(
|
||||||
|
hass,
|
||||||
|
issue["domain"],
|
||||||
|
issue["issue_id"],
|
||||||
|
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
||||||
|
learn_more_url=issue["learn_more_url"],
|
||||||
|
severity=issue["severity"],
|
||||||
|
translation_key=issue["translation_key"],
|
||||||
|
translation_placeholders=issue["translation_placeholders"],
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.send_json({"id": 2, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {
|
||||||
|
"issues": [
|
||||||
|
dict(
|
||||||
|
issue,
|
||||||
|
dismissed=False,
|
||||||
|
dismissed_version=None,
|
||||||
|
)
|
||||||
|
for issue in issues
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Dismiss a non-existing issue
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
async_dismiss_issue(hass, issues[0]["domain"], "no_such_issue")
|
||||||
|
|
||||||
|
await client.send_json({"id": 3, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {
|
||||||
|
"issues": [
|
||||||
|
dict(
|
||||||
|
issue,
|
||||||
|
dismissed=False,
|
||||||
|
dismissed_version=None,
|
||||||
|
)
|
||||||
|
for issue in issues
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Dismiss an existing issue
|
||||||
|
async_dismiss_issue(hass, issues[0]["domain"], issues[0]["issue_id"])
|
||||||
|
|
||||||
|
await client.send_json({"id": 4, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {
|
||||||
|
"issues": [
|
||||||
|
dict(
|
||||||
|
issue,
|
||||||
|
dismissed=True,
|
||||||
|
dismissed_version=ha_version,
|
||||||
|
)
|
||||||
|
for issue in issues
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Dismiss the same issue again
|
||||||
|
async_dismiss_issue(hass, issues[0]["domain"], issues[0]["issue_id"])
|
||||||
|
|
||||||
|
await client.send_json({"id": 5, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {
|
||||||
|
"issues": [
|
||||||
|
dict(
|
||||||
|
issue,
|
||||||
|
dismissed=True,
|
||||||
|
dismissed_version=ha_version,
|
||||||
|
)
|
||||||
|
for issue in issues
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update a dismissed issue
|
||||||
|
async_create_issue(
|
||||||
|
hass,
|
||||||
|
issues[0]["domain"],
|
||||||
|
issues[0]["issue_id"],
|
||||||
|
breaks_in_ha_version=issues[0]["breaks_in_ha_version"],
|
||||||
|
learn_more_url="blablabla",
|
||||||
|
severity=issues[0]["severity"],
|
||||||
|
translation_key=issues[0]["translation_key"],
|
||||||
|
translation_placeholders=issues[0]["translation_placeholders"],
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.send_json({"id": 6, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"]["issues"][0] == dict(
|
||||||
|
issues[0],
|
||||||
|
dismissed=True,
|
||||||
|
dismissed_version=ha_version,
|
||||||
|
learn_more_url="blablabla",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
||||||
|
"""Test we can delete an issue."""
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
|
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
issues = [
|
||||||
|
{
|
||||||
|
"breaks_in_ha_version": "2022.9",
|
||||||
|
"domain": "fake_integration",
|
||||||
|
"issue_id": "issue_1",
|
||||||
|
"learn_more_url": "https://theuselessweb.com",
|
||||||
|
"severity": "error",
|
||||||
|
"translation_key": "abc_123",
|
||||||
|
"translation_placeholders": {"abc": "123"},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for issue in issues:
|
||||||
|
async_create_issue(
|
||||||
|
hass,
|
||||||
|
issue["domain"],
|
||||||
|
issue["issue_id"],
|
||||||
|
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
||||||
|
learn_more_url=issue["learn_more_url"],
|
||||||
|
severity=issue["severity"],
|
||||||
|
translation_key=issue["translation_key"],
|
||||||
|
translation_placeholders=issue["translation_placeholders"],
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.send_json({"id": 1, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {
|
||||||
|
"issues": [
|
||||||
|
dict(
|
||||||
|
issue,
|
||||||
|
dismissed=False,
|
||||||
|
dismissed_version=None,
|
||||||
|
)
|
||||||
|
for issue in issues
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Delete a non-existing issue
|
||||||
|
async_delete_issue(hass, issues[0]["domain"], "no_such_issue")
|
||||||
|
|
||||||
|
await client.send_json({"id": 2, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {
|
||||||
|
"issues": [
|
||||||
|
dict(
|
||||||
|
issue,
|
||||||
|
dismissed=False,
|
||||||
|
dismissed_version=None,
|
||||||
|
)
|
||||||
|
for issue in issues
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Delete an existing issue
|
||||||
|
async_delete_issue(hass, issues[0]["domain"], issues[0]["issue_id"])
|
||||||
|
|
||||||
|
await client.send_json({"id": 3, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {"issues": []}
|
||||||
|
|
||||||
|
# Delete the same issue again
|
||||||
|
async_delete_issue(hass, issues[0]["domain"], issues[0]["issue_id"])
|
||||||
|
|
||||||
|
await client.send_json({"id": 4, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {"issues": []}
|
92
tests/components/resolution_center/test_issue_registry.py
Normal file
92
tests/components/resolution_center/test_issue_registry.py
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
"""Test the resolution center websocket API."""
|
||||||
|
from homeassistant.components.resolution_center import (
|
||||||
|
async_create_issue,
|
||||||
|
issue_registry,
|
||||||
|
)
|
||||||
|
from homeassistant.components.resolution_center.const import DOMAIN
|
||||||
|
from homeassistant.components.resolution_center.issue_handler import async_dismiss_issue
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.common import flush_store
|
||||||
|
|
||||||
|
|
||||||
|
async def test_load_issues(hass: HomeAssistant) -> None:
|
||||||
|
"""Make sure that we can load/save data correctly."""
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
|
|
||||||
|
issues = [
|
||||||
|
{
|
||||||
|
"breaks_in_ha_version": "2022.9",
|
||||||
|
"domain": "test",
|
||||||
|
"issue_id": "issue_1",
|
||||||
|
"learn_more_url": "https://theuselessweb.com",
|
||||||
|
"severity": "error",
|
||||||
|
"translation_key": "abc_123",
|
||||||
|
"translation_placeholders": {"abc": "123"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"breaks_in_ha_version": "2022.8",
|
||||||
|
"domain": "test",
|
||||||
|
"issue_id": "issue_2",
|
||||||
|
"learn_more_url": "https://theuselessweb.com/abc",
|
||||||
|
"severity": "other",
|
||||||
|
"translation_key": "even_worse",
|
||||||
|
"translation_placeholders": {"def": "456"},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for issue in issues:
|
||||||
|
async_create_issue(
|
||||||
|
hass,
|
||||||
|
issue["domain"],
|
||||||
|
issue["issue_id"],
|
||||||
|
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
||||||
|
learn_more_url=issue["learn_more_url"],
|
||||||
|
severity=issue["severity"],
|
||||||
|
translation_key=issue["translation_key"],
|
||||||
|
translation_placeholders=issue["translation_placeholders"],
|
||||||
|
)
|
||||||
|
async_dismiss_issue(hass, issues[0]["domain"], issues[0]["issue_id"])
|
||||||
|
|
||||||
|
registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY]
|
||||||
|
assert len(registry.issues) == 2
|
||||||
|
issue1 = registry.async_get_issue("test", "issue_1")
|
||||||
|
issue2 = registry.async_get_issue("test", "issue_2")
|
||||||
|
|
||||||
|
registry2 = issue_registry.IssueRegistry(hass)
|
||||||
|
await flush_store(registry._store)
|
||||||
|
await registry2.async_load()
|
||||||
|
|
||||||
|
assert list(registry.issues) == list(registry2.issues)
|
||||||
|
|
||||||
|
issue1_registry2 = registry2.async_get_issue("test", "issue_1")
|
||||||
|
assert issue1_registry2.dismissed_version == issue1.dismissed_version
|
||||||
|
issue2_registry2 = registry2.async_get_issue("test", "issue_2")
|
||||||
|
assert issue2_registry2.dismissed_version == issue2.dismissed_version
|
||||||
|
|
||||||
|
|
||||||
|
async def test_loading_issues_from_storage(hass: HomeAssistant, hass_storage) -> None:
|
||||||
|
"""Test loading stored issues on start."""
|
||||||
|
hass_storage[issue_registry.STORAGE_KEY] = {
|
||||||
|
"version": issue_registry.STORAGE_VERSION,
|
||||||
|
"data": {
|
||||||
|
"issues": [
|
||||||
|
{
|
||||||
|
"dismissed_version": "2022.7.0.dev0",
|
||||||
|
"domain": "test",
|
||||||
|
"issue_id": "issue_1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dismissed_version": None,
|
||||||
|
"domain": "test",
|
||||||
|
"issue_id": "issue_2",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
|
|
||||||
|
registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY]
|
||||||
|
assert len(registry.issues) == 2
|
151
tests/components/resolution_center/test_websocket_api.py
Normal file
151
tests/components/resolution_center/test_websocket_api.py
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
"""Test the resolution center websocket API."""
|
||||||
|
from homeassistant.components.resolution_center import async_create_issue
|
||||||
|
from homeassistant.components.resolution_center.const import DOMAIN
|
||||||
|
from homeassistant.const import __version__ as ha_version
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
|
||||||
|
async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
||||||
|
"""Test we can dismiss an issue."""
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
|
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
issues = [
|
||||||
|
{
|
||||||
|
"breaks_in_ha_version": "2022.9",
|
||||||
|
"domain": "test",
|
||||||
|
"issue_id": "issue_1",
|
||||||
|
"learn_more_url": "https://theuselessweb.com",
|
||||||
|
"severity": "error",
|
||||||
|
"translation_key": "abc_123",
|
||||||
|
"translation_placeholders": {"abc": "123"},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for issue in issues:
|
||||||
|
async_create_issue(
|
||||||
|
hass,
|
||||||
|
issue["domain"],
|
||||||
|
issue["issue_id"],
|
||||||
|
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
||||||
|
learn_more_url=issue["learn_more_url"],
|
||||||
|
severity=issue["severity"],
|
||||||
|
translation_key=issue["translation_key"],
|
||||||
|
translation_placeholders=issue["translation_placeholders"],
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.send_json({"id": 1, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {
|
||||||
|
"issues": [
|
||||||
|
dict(
|
||||||
|
issue,
|
||||||
|
dismissed=False,
|
||||||
|
dismissed_version=None,
|
||||||
|
)
|
||||||
|
for issue in issues
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.send_json(
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "resolution_center/dismiss_issue",
|
||||||
|
"domain": "test",
|
||||||
|
"issue_id": "no_such_issue",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert not msg["success"]
|
||||||
|
|
||||||
|
await client.send_json(
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "resolution_center/dismiss_issue",
|
||||||
|
"domain": "test",
|
||||||
|
"issue_id": "issue_1",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] is None
|
||||||
|
|
||||||
|
await client.send_json({"id": 4, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {
|
||||||
|
"issues": [
|
||||||
|
dict(
|
||||||
|
issue,
|
||||||
|
dismissed=True,
|
||||||
|
dismissed_version=ha_version,
|
||||||
|
)
|
||||||
|
for issue in issues
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None:
|
||||||
|
"""Test we can list issues."""
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
|
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
await client.send_json({"id": 1, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {"issues": []}
|
||||||
|
|
||||||
|
issues = [
|
||||||
|
{
|
||||||
|
"breaks_in_ha_version": "2022.9",
|
||||||
|
"domain": "test",
|
||||||
|
"issue_id": "issue_1",
|
||||||
|
"learn_more_url": "https://theuselessweb.com",
|
||||||
|
"severity": "error",
|
||||||
|
"translation_key": "abc_123",
|
||||||
|
"translation_placeholders": {"abc": "123"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"breaks_in_ha_version": "2022.8",
|
||||||
|
"domain": "test",
|
||||||
|
"issue_id": "issue_2",
|
||||||
|
"learn_more_url": "https://theuselessweb.com/abc",
|
||||||
|
"severity": "other",
|
||||||
|
"translation_key": "even_worse",
|
||||||
|
"translation_placeholders": {"def": "456"},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for issue in issues:
|
||||||
|
async_create_issue(
|
||||||
|
hass,
|
||||||
|
issue["domain"],
|
||||||
|
issue["issue_id"],
|
||||||
|
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
||||||
|
learn_more_url=issue["learn_more_url"],
|
||||||
|
severity=issue["severity"],
|
||||||
|
translation_key=issue["translation_key"],
|
||||||
|
translation_placeholders=issue["translation_placeholders"],
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.send_json({"id": 2, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {
|
||||||
|
"issues": [
|
||||||
|
dict(
|
||||||
|
issue,
|
||||||
|
dismissed=False,
|
||||||
|
dismissed_version=None,
|
||||||
|
)
|
||||||
|
for issue in issues
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in a new issue