Skip to content

Commit

Permalink
Deprecate the map integration (home-assistant#113215)
Browse files Browse the repository at this point in the history
* Deprecate the map integration

* Revert changes in DashboardsCollection._async_load_data

* Add option to allow single word in dashboard URL

* Update tests

* Translate title

* Add icon

* Improve test coverage
  • Loading branch information
emontnemery authored Mar 14, 2024
1 parent fef2d7d commit a16ea3d
Show file tree
Hide file tree
Showing 19 changed files with 480 additions and 36 deletions.
1 change: 0 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,6 @@ omit =
homeassistant/components/lyric/climate.py
homeassistant/components/lyric/sensor.py
homeassistant/components/mailgun/notify.py
homeassistant/components/map/*
homeassistant/components/mastodon/notify.py
homeassistant/components/matrix/__init__.py
homeassistant/components/matrix/notify.py
Expand Down
35 changes: 34 additions & 1 deletion homeassistant/components/lovelace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import voluptuous as vol

from homeassistant.components import frontend, websocket_api
from homeassistant.components import frontend, onboarding, websocket_api
from homeassistant.config import (
async_hass_config_yaml,
async_process_component_and_handle_errors,
Expand All @@ -14,11 +14,13 @@
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import collection, config_validation as cv
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.translation import async_get_translations
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_integration

from . import dashboard, resources, websocket
from .const import ( # noqa: F401
CONF_ALLOW_SINGLE_WORD,
CONF_ICON,
CONF_REQUIRE_ADMIN,
CONF_SHOW_IN_SIDEBAR,
Expand Down Expand Up @@ -201,6 +203,9 @@ async def storage_dashboard_changed(change_type, item_id, item):
# Process storage dashboards
dashboards_collection = dashboard.DashboardsCollection(hass)

# This can be removed when the map integration is removed
hass.data[DOMAIN]["dashboards_collection"] = dashboards_collection

dashboards_collection.async_add_listener(storage_dashboard_changed)
await dashboards_collection.async_load()

Expand All @@ -212,6 +217,12 @@ async def storage_dashboard_changed(change_type, item_id, item):
STORAGE_DASHBOARD_UPDATE_FIELDS,
).async_setup(hass, create_list=False)

def create_map_dashboard():
hass.async_create_task(_create_map_dashboard(hass))

if not onboarding.async_is_onboarded(hass):
onboarding.async_add_listener(hass, create_map_dashboard)

return True


Expand Down Expand Up @@ -249,3 +260,25 @@ def _register_panel(hass, url_path, mode, config, update):
kwargs["sidebar_icon"] = config.get(CONF_ICON, DEFAULT_ICON)

frontend.async_register_built_in_panel(hass, DOMAIN, **kwargs)


async def _create_map_dashboard(hass: HomeAssistant):
translations = await async_get_translations(
hass, hass.config.language, "dashboard", {onboarding.DOMAIN}
)
title = translations["component.onboarding.dashboard.map.title"]

dashboards_collection: dashboard.DashboardsCollection = hass.data[DOMAIN][
"dashboards_collection"
]
await dashboards_collection.async_create_item(
{
CONF_ALLOW_SINGLE_WORD: True,
CONF_ICON: "mdi:map",
CONF_TITLE: title,
CONF_URL_PATH: "map",
}
)

map_store: dashboard.LovelaceStorage = hass.data[DOMAIN]["dashboards"]["map"]
await map_store.async_save({"strategy": {"type": "map"}})
3 changes: 3 additions & 0 deletions homeassistant/components/lovelace/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
MODE_AUTO = "auto-gen"

LOVELACE_CONFIG_FILE = "ui-lovelace.yaml"
CONF_ALLOW_SINGLE_WORD = "allow_single_word"
CONF_URL_PATH = "url_path"
CONF_RESOURCE_TYPE_WS = "res_type"

Expand Down Expand Up @@ -75,6 +76,8 @@
# For now we write "storage" as all modes.
# In future we can adjust this to be other modes.
vol.Optional(CONF_MODE, default=MODE_STORAGE): MODE_STORAGE,
# Set to allow adding dashboard without hyphen
vol.Optional(CONF_ALLOW_SINGLE_WORD): bool,
}

STORAGE_DASHBOARD_UPDATE_FIELDS = DASHBOARD_BASE_UPDATE_FIELDS
Expand Down
9 changes: 7 additions & 2 deletions homeassistant/components/lovelace/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from homeassistant.util.yaml import Secrets, load_yaml_dict

from .const import (
CONF_ALLOW_SINGLE_WORD,
CONF_ICON,
CONF_URL_PATH,
DOMAIN,
Expand Down Expand Up @@ -234,10 +235,14 @@ def __init__(self, hass):

async def _process_create_data(self, data: dict) -> dict:
"""Validate the config is valid."""
if "-" not in data[CONF_URL_PATH]:
url_path = data[CONF_URL_PATH]

allow_single_word = data.pop(CONF_ALLOW_SINGLE_WORD, False)

if not allow_single_word and "-" not in url_path:
raise vol.Invalid("Url path needs to contain a hyphen (-)")

if data[CONF_URL_PATH] in self.hass.data[DATA_PANELS]:
if url_path in self.hass.data[DATA_PANELS]:
raise vol.Invalid("Panel url path needs to be unique")

return self.CREATE_SCHEMA(data)
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/lovelace/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"domain": "lovelace",
"name": "Dashboards",
"codeowners": ["@home-assistant/frontend"],
"dependencies": ["onboarding"],
"documentation": "https://www.home-assistant.io/integrations/lovelace",
"integration_type": "system",
"quality_scale": "internal"
Expand Down
46 changes: 41 additions & 5 deletions homeassistant/components/map/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,52 @@
"""Support for showing device locations."""

from homeassistant.components import frontend
from homeassistant.core import HomeAssistant
from homeassistant.components import onboarding
from homeassistant.components.lovelace import _create_map_dashboard
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.storage import Store
from homeassistant.helpers.typing import ConfigType

DOMAIN = "map"

CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)

STORAGE_KEY = DOMAIN
STORAGE_VERSION_MAJOR = 1


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Register the built-in map panel."""
frontend.async_register_built_in_panel(hass, "map", "map", "hass:tooltip-account")
"""Create a map panel."""

if DOMAIN in config:
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2024.10.0",
is_fixable=False,
is_persistent=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "map",
},
)

store: Store[dict[str, bool]] = Store(
hass,
STORAGE_VERSION_MAJOR,
STORAGE_KEY,
)
data = await store.async_load()
if data:
return True

if onboarding.async_is_onboarded(hass):
await _create_map_dashboard(hass)

await store.async_save({"migrated": True})

return True
2 changes: 1 addition & 1 deletion homeassistant/components/map/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"domain": "map",
"name": "Map",
"codeowners": [],
"dependencies": ["frontend"],
"dependencies": ["frontend", "lovelace"],
"documentation": "https://www.home-assistant.io/integrations/map",
"integration_type": "system",
"quality_scale": "internal"
Expand Down
53 changes: 43 additions & 10 deletions homeassistant/components/onboarding/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

from __future__ import annotations

from typing import TYPE_CHECKING
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, TypedDict

from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
Expand All @@ -26,15 +28,30 @@
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)


class OnboadingStorage(Store[dict[str, list[str]]]):
@dataclass
class OnboardingData:
"""Container for onboarding data."""

listeners: list[Callable[[], None]]
onboarded: bool
steps: OnboardingStoreData


class OnboardingStoreData(TypedDict):
"""Onboarding store data."""

done: list[str]


class OnboardingStorage(Store[OnboardingStoreData]):
"""Store onboarding data."""

async def _async_migrate_func(
self,
old_major_version: int,
old_minor_version: int,
old_data: dict[str, list[str]],
) -> dict[str, list[str]]:
old_data: OnboardingStoreData,
) -> OnboardingStoreData:
"""Migrate to the new version."""
# From version 1 -> 2, we automatically mark the integration step done
if old_major_version < 2:
Expand All @@ -50,21 +67,37 @@ async def _async_migrate_func(
@callback
def async_is_onboarded(hass: HomeAssistant) -> bool:
"""Return if Home Assistant has been onboarded."""
data = hass.data.get(DOMAIN)
return data is None or data is True
data: OnboardingData | None = hass.data.get(DOMAIN)
return data is None or data.onboarded is True


@bind_hass
@callback
def async_is_user_onboarded(hass: HomeAssistant) -> bool:
"""Return if a user has been created as part of onboarding."""
return async_is_onboarded(hass) or STEP_USER in hass.data[DOMAIN]["done"]
return async_is_onboarded(hass) or STEP_USER in hass.data[DOMAIN].steps["done"]


@callback
def async_add_listener(hass: HomeAssistant, listener: Callable[[], None]) -> None:
"""Add a listener to be called when onboarding is complete."""
data: OnboardingData | None = hass.data.get(DOMAIN)

if not data:
# Onboarding not active
return

if data.onboarded:
listener()
return

data.listeners.append(listener)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the onboarding component."""
store = OnboadingStorage(hass, STORAGE_VERSION, STORAGE_KEY, private=True)
data: dict[str, list[str]] | None
store = OnboardingStorage(hass, STORAGE_VERSION, STORAGE_KEY, private=True)
data: OnboardingStoreData | None
if (data := await store.async_load()) is None:
data = {"done": []}

Expand All @@ -88,7 +121,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if set(data["done"]) == set(STEPS):
return True

hass.data[DOMAIN] = data
hass.data[DOMAIN] = OnboardingData([], False, data)

await views.async_setup(hass, data, store)

Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/onboarding/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
"living_room": "Living Room",
"bedroom": "Bedroom",
"kitchen": "Kitchen"
},
"dashboard": {
"map": { "title": "Map" }
}
}
15 changes: 9 additions & 6 deletions homeassistant/components/onboarding/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from homeassistant.util.async_ import create_eager_task

if TYPE_CHECKING:
from . import OnboadingStorage
from . import OnboardingData, OnboardingStorage, OnboardingStoreData

from .const import (
DEFAULT_AREAS,
Expand All @@ -40,7 +40,7 @@


async def async_setup(
hass: HomeAssistant, data: dict[str, list[str]], store: OnboadingStorage
hass: HomeAssistant, data: OnboardingStoreData, store: OnboardingStorage
) -> None:
"""Set up the onboarding view."""
hass.http.register_view(OnboardingView(data, store))
Expand All @@ -58,7 +58,7 @@ class OnboardingView(HomeAssistantView):
url = "/api/onboarding"
name = "api:onboarding"

def __init__(self, data: dict[str, list[str]], store: OnboadingStorage) -> None:
def __init__(self, data: OnboardingStoreData, store: OnboardingStorage) -> None:
"""Initialize the onboarding view."""
self._store = store
self._data = data
Expand All @@ -77,7 +77,7 @@ class InstallationTypeOnboardingView(HomeAssistantView):
url = "/api/onboarding/installation_type"
name = "api:onboarding:installation_type"

def __init__(self, data: dict[str, list[str]]) -> None:
def __init__(self, data: OnboardingStoreData) -> None:
"""Initialize the onboarding installation type view."""
self._data = data

Expand All @@ -96,7 +96,7 @@ class _BaseOnboardingView(HomeAssistantView):

step: str

def __init__(self, data: dict[str, list[str]], store: OnboadingStorage) -> None:
def __init__(self, data: OnboardingStoreData, store: OnboardingStorage) -> None:
"""Initialize the onboarding view."""
self._store = store
self._data = data
Expand All @@ -113,7 +113,10 @@ async def _async_mark_done(self, hass: HomeAssistant) -> None:
await self._store.async_save(self._data)

if set(self._data["done"]) == set(STEPS):
hass.data[DOMAIN] = True
data: OnboardingData = hass.data[DOMAIN]
data.onboarded = True
for listener in data.listeners:
listener()


class UserOnboardingView(_BaseOnboardingView):
Expand Down
5 changes: 4 additions & 1 deletion script/hassfest/translations.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,10 @@ def device_class_validator(value: str) -> str:


ONBOARDING_SCHEMA = vol.Schema(
{vol.Required("area"): {str: translation_value_validator}}
{
vol.Required("area"): {str: translation_value_validator},
vol.Required("dashboard"): {str: {"title": translation_value_validator}},
}
)


Expand Down
Loading

0 comments on commit a16ea3d

Please sign in to comment.