Add config flow to Brottsplatskartan (#70233)

* Brottsplatskartan Config Flow

* Fix import

* Modify sensor

* Mod version

* Mod version 2

* has_entity_name

* Fix api constructor

* Switch to issue for depr.

* Fix docstrings

* Minor cleaning

* Fix argument for bpk constructor

* remove translations

* Fix tests

* reset config

* uuid to conftest

* hassfest

* depr version

* unique id

* reset not linked changes

* review comments

* fix area none

* relevant changes

* depr version

* slim test

* unique_id

* create_entry

* review comments and tests

* fix init test

* review comments
This commit is contained in:
G Johansson
2023-04-12 11:05:24 +02:00
committed by GitHub
parent a409da947f
commit 00847ee4bc
14 changed files with 492 additions and 25 deletions

View File

@ -170,6 +170,8 @@ build.json @home-assistant/supervisor
/tests/components/broadlink/ @danielhiversen @felipediel @L-I-Am
/homeassistant/components/brother/ @bieniu
/tests/components/brother/ @bieniu
/homeassistant/components/brottsplatskartan/ @gjohansson-ST
/tests/components/brottsplatskartan/ @gjohansson-ST
/homeassistant/components/brunt/ @eavanvalkenburg
/tests/components/brunt/ @eavanvalkenburg
/homeassistant/components/bsblan/ @liudger

View File

@ -1 +1,21 @@
"""The brottsplatskartan component."""
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import PLATFORMS
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up brottsplatskartan from a config entry."""
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload brottsplatskartan config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -0,0 +1,97 @@
"""Adds config flow for Brottsplatskartan integration."""
from __future__ import annotations
from typing import Any
import uuid
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import selector
from .const import AREAS, CONF_APP_ID, CONF_AREA, DEFAULT_NAME, DOMAIN
DATA_SCHEMA = vol.Schema(
{
vol.Optional(CONF_LOCATION): selector.LocationSelector(
selector.LocationSelectorConfig(radius=False, icon="")
),
vol.Optional(CONF_AREA, default="none"): selector.SelectSelector(
selector.SelectSelectorConfig(
options=AREAS,
mode=selector.SelectSelectorMode.DROPDOWN,
translation_key="areas",
)
),
}
)
class BPKConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Brottsplatskartan integration."""
VERSION = 1
async def async_step_import(self, config: dict[str, Any]) -> FlowResult:
"""Import a configuration from config.yaml."""
if config.get(CONF_LATITUDE):
config[CONF_LOCATION] = {
CONF_LATITUDE: config[CONF_LATITUDE],
CONF_LONGITUDE: config[CONF_LONGITUDE],
}
if not config.get(CONF_AREA):
config[CONF_AREA] = "none"
else:
config[CONF_AREA] = config[CONF_AREA][0]
return await self.async_step_user(user_input=config)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the user step."""
errors: dict[str, str] = {}
if user_input is not None:
latitude: float | None = None
longitude: float | None = None
area: str | None = (
user_input[CONF_AREA] if user_input[CONF_AREA] != "none" else None
)
if area:
name = f"{DEFAULT_NAME} {area}"
elif location := user_input.get(CONF_LOCATION):
lat: float = location[CONF_LATITUDE]
long: float = location[CONF_LONGITUDE]
latitude = lat
longitude = long
name = f"{DEFAULT_NAME} {round(latitude, 2)}, {round(longitude, 2)}"
else:
latitude = self.hass.config.latitude
longitude = self.hass.config.longitude
name = f"{DEFAULT_NAME} HOME"
app = f"ha-{uuid.getnode()}"
self._async_abort_entries_match(
{CONF_AREA: area, CONF_LATITUDE: latitude, CONF_LONGITUDE: longitude}
)
return self.async_create_entry(
title=name,
data={
CONF_LATITUDE: latitude,
CONF_LONGITUDE: longitude,
CONF_AREA: area,
CONF_APP_ID: app,
},
)
return self.async_show_form(
step_id="user",
data_schema=DATA_SCHEMA,
errors=errors,
)

View File

@ -2,13 +2,19 @@
import logging
from homeassistant.const import Platform
DOMAIN = "brottsplatskartan"
PLATFORMS = [Platform.SENSOR]
LOGGER = logging.getLogger(__package__)
CONF_AREA = "area"
CONF_APP_ID = "app_id"
DEFAULT_NAME = "Brottsplatskartan"
AREAS = [
"N/A",
"none",
"Blekinge län",
"Dalarnas län",
"Gotlands län",

View File

@ -1,7 +1,8 @@
{
"domain": "brottsplatskartan",
"name": "Brottsplatskartan",
"codeowners": [],
"codeowners": ["@gjohansson-ST"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/brottsplatskartan",
"iot_class": "cloud_polling",
"loggers": ["brottsplatskartan"],

View File

@ -3,23 +3,29 @@ from __future__ import annotations
from collections import defaultdict
from datetime import timedelta
import uuid
import brottsplatskartan
from brottsplatskartan import ATTRIBUTION, BrottsplatsKartan
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.components.sensor import (
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
SensorEntity,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import AREAS, CONF_AREA, DEFAULT_NAME, LOGGER
from .const import AREAS, CONF_APP_ID, CONF_AREA, DEFAULT_NAME, DOMAIN, LOGGER
SCAN_INTERVAL = timedelta(minutes=30)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(
{
vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude,
vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude,
@ -29,39 +35,65 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
)
def setup_platform(
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Brottsplatskartan platform."""
area = config.get(CONF_AREA)
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
name = config[CONF_NAME]
# Every Home Assistant instance should have their own unique
# app parameter: https://brottsplatskartan.se/sida/api
app = f"ha-{uuid.getnode()}"
bpk = brottsplatskartan.BrottsplatsKartan(
app=app, area=area, latitude=latitude, longitude=longitude
async_create_issue(
hass,
DOMAIN,
"deprecated_yaml",
breaks_in_ha_version="2023.7.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
)
add_entities([BrottsplatskartanSensor(bpk, name)], True)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Brottsplatskartan sensor entry."""
area = entry.data.get(CONF_AREA)
latitude = entry.data.get(CONF_LATITUDE)
longitude = entry.data.get(CONF_LONGITUDE)
app = entry.data[CONF_APP_ID]
name = entry.title
bpk = BrottsplatsKartan(app=app, area=area, latitude=latitude, longitude=longitude)
async_add_entities([BrottsplatskartanSensor(bpk, name, entry.entry_id)], True)
class BrottsplatskartanSensor(SensorEntity):
"""Representation of a Brottsplatskartan Sensor."""
_attr_attribution = brottsplatskartan.ATTRIBUTION
_attr_attribution = ATTRIBUTION
_attr_has_entity_name = True
def __init__(self, bpk: brottsplatskartan.BrottsplatsKartan, name: str) -> None:
def __init__(self, bpk: BrottsplatsKartan, name: str, entry_id: str) -> None:
"""Initialize the Brottsplatskartan sensor."""
self._brottsplatskartan = bpk
self._attr_name = name
self._attr_unique_id = entry_id
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, entry_id)},
manufacturer="Brottsplatskartan",
name=name,
)
def update(self) -> None:
"""Update device state."""

View File

@ -0,0 +1,32 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"step": {
"user": {
"data": {
"location": "[%key:common::config_flow::data::location%]",
"area": "Area"
},
"data_description": {
"location": "Put marker on location to cover within 5km radius",
"area": "If area is selected, any marked location is ignored"
}
}
}
},
"issues": {
"deprecated_yaml": {
"title": "The Brottsplatskartan YAML configuration is being removed",
"description": "Configuring Brottsplatskartan using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Brottsplatskartan YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
}
},
"selector": {
"areas": {
"options": {
"none": "No area"
}
}
}
}

View File

@ -68,6 +68,7 @@ FLOWS = {
"braviatv",
"broadlink",
"brother",
"brottsplatskartan",
"brunt",
"bsblan",
"bthome",

View File

@ -663,7 +663,7 @@
"brottsplatskartan": {
"name": "Brottsplatskartan",
"integration_type": "hub",
"config_flow": false,
"config_flow": true,
"iot_class": "cloud_polling"
},
"browser": {

View File

@ -401,6 +401,9 @@ broadlink==0.18.3
# homeassistant.components.brother
brother==2.3.0
# homeassistant.components.brottsplatskartan
brottsplatskartan==0.0.1
# homeassistant.components.brunt
brunt==1.2.0

View File

@ -0,0 +1 @@
"""Tests for the Brottsplatskartan integration."""

View File

@ -0,0 +1,25 @@
"""Test fixtures for Brottplatskartan."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.brottsplatskartan.async_setup_entry",
return_value=True,
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture(autouse=True)
def uuid_generator() -> Generator[AsyncMock, None, None]:
"""Generate uuid for app-id."""
with patch(
"homeassistant.components.brottsplatskartan.config_flow.uuid.getnode",
return_value="1234567890",
) as uuid_generator:
yield uuid_generator

View File

@ -0,0 +1,209 @@
"""Test the Brottsplatskartan config flow."""
from __future__ import annotations
from unittest.mock import patch
import pytest
from homeassistant import config_entries
from homeassistant.components.brottsplatskartan.const import CONF_AREA, DOMAIN
from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
async def test_form(hass: HomeAssistant) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["errors"] == {}
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_AREA: "none",
},
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["title"] == "Brottsplatskartan HOME"
assert result2["data"] == {
"area": None,
"latitude": hass.config.latitude,
"longitude": hass.config.longitude,
"app_id": "ha-1234567890",
}
async def test_form_location(hass: HomeAssistant) -> None:
"""Test we get the form using location."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["errors"] == {}
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_AREA: "none",
CONF_LOCATION: {
CONF_LATITUDE: 59.32,
CONF_LONGITUDE: 18.06,
},
},
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["title"] == "Brottsplatskartan 59.32, 18.06"
assert result2["data"] == {
"area": None,
"latitude": 59.32,
"longitude": 18.06,
"app_id": "ha-1234567890",
}
async def test_form_area(hass: HomeAssistant) -> None:
"""Test we get the form using area."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["errors"] == {}
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_LOCATION: {
CONF_LATITUDE: 59.32,
CONF_LONGITUDE: 18.06,
},
CONF_AREA: "Stockholms län",
},
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["title"] == "Brottsplatskartan Stockholms län"
assert result2["data"] == {
"latitude": None,
"longitude": None,
"area": "Stockholms län",
"app_id": "ha-1234567890",
}
async def test_import_flow_success(hass: HomeAssistant) -> None:
"""Test a successful import of yaml."""
with patch(
"homeassistant.components.brottsplatskartan.sensor.BrottsplatsKartan",
):
result2 = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={},
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["title"] == "Brottsplatskartan HOME"
assert result2["data"] == {
"latitude": hass.config.latitude,
"longitude": hass.config.longitude,
"area": None,
"app_id": "ha-1234567890",
}
async def test_import_flow_location_success(hass: HomeAssistant) -> None:
"""Test a successful import of yaml with location."""
with patch(
"homeassistant.components.brottsplatskartan.sensor.BrottsplatsKartan",
):
result2 = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={
CONF_LATITUDE: 59.32,
CONF_LONGITUDE: 18.06,
},
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["title"] == "Brottsplatskartan 59.32, 18.06"
assert result2["data"] == {
"latitude": 59.32,
"longitude": 18.06,
"area": None,
"app_id": "ha-1234567890",
}
async def test_import_flow_location_area_success(hass: HomeAssistant) -> None:
"""Test a successful import of yaml with location and area."""
with patch(
"homeassistant.components.brottsplatskartan.sensor.BrottsplatsKartan",
):
result2 = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={
CONF_LATITUDE: 59.32,
CONF_LONGITUDE: 18.06,
CONF_AREA: ["Blekinge län"],
},
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["title"] == "Brottsplatskartan Blekinge län"
assert result2["data"] == {
"latitude": None,
"longitude": None,
"area": "Blekinge län",
"app_id": "ha-1234567890",
}
async def test_import_flow_already_exist(hass: HomeAssistant) -> None:
"""Test import of yaml already exist."""
MockConfigEntry(
domain=DOMAIN,
data={
"latitude": hass.config.latitude,
"longitude": hass.config.longitude,
"area": None,
"app_id": "ha-1234567890",
},
unique_id="bpk-home",
).add_to_hass(hass)
with patch(
"homeassistant.components.brottsplatskartan.sensor.BrottsplatsKartan",
):
result3 = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={},
)
await hass.async_block_till_done()
assert result3["type"] == FlowResultType.ABORT
assert result3["reason"] == "already_configured"

View File

@ -0,0 +1,38 @@
"""Test Brottsplatskartan component setup process."""
from __future__ import annotations
from unittest.mock import patch
from homeassistant.components.brottsplatskartan.const import DOMAIN
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def test_load_unload_entry(hass: HomeAssistant) -> None:
"""Test load and unload entry."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
"latitude": hass.config.latitude,
"longitude": hass.config.longitude,
"area": None,
"app_id": "ha-1234567890",
},
title="BPK-HOME",
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.brottsplatskartan.sensor.BrottsplatsKartan",
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.bpk_home")
assert state
await hass.config_entries.async_remove(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.bpk_home")
assert not state