Add config flow to Season (#67413)

This commit is contained in:
Franck Nijhof 2022-03-07 18:58:29 +01:00 committed by GitHub
parent 7041bc797a
commit 9d42a425fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 356 additions and 67 deletions

View File

@ -859,6 +859,8 @@ homeassistant/components/script/* @home-assistant/core
tests/components/script/* @home-assistant/core
homeassistant/components/search/* @home-assistant/core
tests/components/search/* @home-assistant/core
homeassistant/components/season/* @frenck
tests/components/season/* @frenck
homeassistant/components/select/* @home-assistant/core
tests/components/select/* @home-assistant/core
homeassistant/components/sense/* @kbickar

View File

@ -1 +1,16 @@
"""The season integration."""
"""The Season integration."""
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 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 a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -0,0 +1,48 @@
"""Config flow to configure the Season integration."""
from __future__ import annotations
from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_NAME, CONF_TYPE
from homeassistant.data_entry_flow import FlowResult
from .const import DEFAULT_NAME, DOMAIN, TYPE_ASTRONOMICAL, TYPE_METEOROLOGICAL
class SeasonConfigFlow(ConfigFlow, domain=DOMAIN):
"""Config flow for Season."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initialized by the user."""
if user_input is not None:
await self.async_set_unique_id(user_input[CONF_TYPE])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input.get(CONF_NAME, DEFAULT_NAME),
data={CONF_TYPE: user_input[CONF_TYPE]},
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_TYPE, default=TYPE_ASTRONOMICAL): vol.In(
{
TYPE_ASTRONOMICAL: "Astronomical",
TYPE_METEOROLOGICAL: "Meteorological",
}
)
},
),
)
async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult:
"""Handle import from configuration.yaml."""
return await self.async_step_user(user_input)

View File

@ -0,0 +1,14 @@
"""Constants for the Season integration."""
from typing import Final
from homeassistant.const import Platform
DOMAIN: Final = "season"
PLATFORMS: Final = [Platform.SENSOR]
DEFAULT_NAME: Final = "Season"
TYPE_ASTRONOMICAL: Final = "astronomical"
TYPE_METEOROLOGICAL: Final = "meteorological"
VALID_TYPES: Final = [TYPE_ASTRONOMICAL, TYPE_METEOROLOGICAL]

View File

@ -3,8 +3,9 @@
"name": "Season",
"documentation": "https://www.home-assistant.io/integrations/season",
"requirements": ["ephem==4.1.2"],
"codeowners": [],
"codeowners": ["@frenck"],
"quality_scale": "internal",
"iot_class": "local_polling",
"loggers": ["ephem"]
"loggers": ["ephem"],
"config_flow": true
}

View File

@ -1,8 +1,7 @@
"""Support for tracking which astronomical or meteorological season it is."""
"""Support for Season sensors."""
from __future__ import annotations
from datetime import date, datetime
import logging
import ephem
import voluptuous as vol
@ -11,6 +10,7 @@ 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_NAME, CONF_TYPE
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
@ -18,9 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.dt import utcnow
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Season"
from .const import DEFAULT_NAME, DOMAIN, TYPE_ASTRONOMICAL, VALID_TYPES
EQUATOR = "equator"
@ -32,11 +30,6 @@ STATE_SPRING = "spring"
STATE_SUMMER = "summer"
STATE_WINTER = "winter"
TYPE_ASTRONOMICAL = "astronomical"
TYPE_METEOROLOGICAL = "meteorological"
VALID_TYPES = [TYPE_ASTRONOMICAL, TYPE_METEOROLOGICAL]
HEMISPHERE_SEASON_SWAP = {
STATE_WINTER: STATE_SUMMER,
STATE_SPRING: STATE_AUTUMN,
@ -60,25 +53,35 @@ PLATFORM_SCHEMA = PARENT_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:
"""Display the current season."""
_type: str = config[CONF_TYPE]
name: str = config[CONF_NAME]
"""Set up the season sensor platform."""
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 platform from config entry."""
hemisphere = EQUATOR
if hass.config.latitude < 0:
hemisphere = SOUTHERN
elif hass.config.latitude > 0:
hemisphere = NORTHERN
else:
hemisphere = EQUATOR
_LOGGER.debug(_type)
add_entities([Season(hemisphere, _type, name)], True)
async_add_entities([SeasonSensorEntity(entry, hemisphere)], True)
def get_season(
@ -100,14 +103,13 @@ def get_season(
autumn_start = spring_start.replace(month=9)
winter_start = spring_start.replace(month=12)
season = STATE_WINTER
if spring_start <= current_date < summer_start:
season = STATE_SPRING
elif summer_start <= current_date < autumn_start:
season = STATE_SUMMER
elif autumn_start <= current_date < winter_start:
season = STATE_AUTUMN
elif winter_start <= current_date or spring_start > current_date:
season = STATE_WINTER
# If user is located in the southern hemisphere swap the season
if hemisphere == NORTHERN:
@ -115,16 +117,17 @@ def get_season(
return HEMISPHERE_SEASON_SWAP.get(season)
class Season(SensorEntity):
class SeasonSensorEntity(SensorEntity):
"""Representation of the current season."""
_attr_device_class = "season__season"
def __init__(self, hemisphere: str, season_tracking_type: str, name: str) -> None:
def __init__(self, entry: ConfigEntry, hemisphere: str) -> None:
"""Initialize the season."""
self._attr_name = name
self._attr_name = entry.title
self._attr_unique_id = entry.entry_id
self.hemisphere = hemisphere
self.type = season_tracking_type
self.type = entry.data[CONF_TYPE]
def update(self) -> None:
"""Update season."""

View File

@ -0,0 +1,14 @@
{
"config": {
"step": {
"user": {
"data": {
"type": "Type of season definition"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
}
}

View File

@ -0,0 +1,14 @@
{
"config": {
"abort": {
"already_configured": "Service is already configured"
},
"step": {
"user": {
"data": {
"type": "Type of season definition"
}
}
}
}
}

View File

@ -5,12 +5,6 @@
"spring": "Spring",
"summer": "Summer",
"winter": "Winter"
},
"season__season__": {
"autumn": "Autumn",
"spring": "Spring",
"summer": "Summer",
"winter": "Winter"
}
}
}

View File

@ -283,6 +283,7 @@ FLOWS = [
"ruckus_unleashed",
"samsungtv",
"screenlogic",
"season",
"sense",
"senseme",
"sensibo",

View File

@ -0,0 +1,30 @@
"""Fixtures for Season integration tests."""
from __future__ import annotations
from collections.abc import Generator
from unittest.mock import patch
import pytest
from homeassistant.components.season.const import DOMAIN, TYPE_ASTRONOMICAL
from homeassistant.const import CONF_TYPE
from tests.common import MockConfigEntry
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
title="Season",
domain=DOMAIN,
data={CONF_TYPE: TYPE_ASTRONOMICAL},
unique_id=TYPE_ASTRONOMICAL,
)
@pytest.fixture
def mock_setup_entry() -> Generator[None, None, None]:
"""Mock setting up a config entry."""
with patch("homeassistant.components.season.async_setup_entry", return_value=True):
yield

View File

@ -0,0 +1,76 @@
"""Tests for the Season config flow."""
from unittest.mock import MagicMock
import pytest
from homeassistant.components.season.const import (
DOMAIN,
TYPE_ASTRONOMICAL,
TYPE_METEOROLOGICAL,
)
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
from homeassistant.const import CONF_NAME, CONF_TYPE
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
RESULT_TYPE_FORM,
)
from tests.common import MockConfigEntry
async def test_full_user_flow(
hass: HomeAssistant,
mock_setup_entry: MagicMock,
) -> None:
"""Test the full user configuration flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result.get("type") == RESULT_TYPE_FORM
assert result.get("step_id") == SOURCE_USER
assert "flow_id" in result
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_TYPE: TYPE_ASTRONOMICAL},
)
assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
assert result2.get("title") == "Season"
assert result2.get("data") == {CONF_TYPE: TYPE_ASTRONOMICAL}
@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT])
async def test_single_instance_allowed(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
source: str,
) -> None:
"""Test we abort if already setup."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": source}, data={CONF_TYPE: TYPE_ASTRONOMICAL}
)
assert result.get("type") == RESULT_TYPE_ABORT
assert result.get("reason") == "already_configured"
async def test_import_flow(
hass: HomeAssistant,
mock_setup_entry: MagicMock,
) -> None:
"""Test the import configuration flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={CONF_NAME: "My Seasons", CONF_TYPE: TYPE_METEOROLOGICAL},
)
assert result.get("type") == RESULT_TYPE_CREATE_ENTRY
assert result.get("title") == "My Seasons"
assert result.get("data") == {CONF_TYPE: TYPE_METEOROLOGICAL}

View File

@ -0,0 +1,55 @@
"""Tests for the Season integration."""
from unittest.mock import AsyncMock
from homeassistant.components.season.const import DOMAIN, TYPE_ASTRONOMICAL
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_NAME, CONF_TYPE
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
async def test_load_unload_config_entry(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the Season configuration entry loading/unloading."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert not hass.data.get(DOMAIN)
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
async def test_import_config(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
) -> None:
"""Test Season being set up from config via import."""
assert await async_setup_component(
hass,
SENSOR_DOMAIN,
{
SENSOR_DOMAIN: {
"platform": DOMAIN,
CONF_NAME: "My Season",
}
},
)
await hass.async_block_till_done()
config_entries = hass.config_entries.async_entries(DOMAIN)
assert len(config_entries) == 1
entry = config_entries[0]
assert entry.title == "My Season"
assert entry.unique_id == TYPE_ASTRONOMICAL
assert entry.data == {CONF_TYPE: TYPE_ASTRONOMICAL}

View File

@ -1,19 +1,21 @@
"""The tests for the Season sensor platform."""
"""The tests for the Season integration."""
from datetime import datetime
from unittest.mock import patch
from freezegun import freeze_time
import pytest
from homeassistant.components.season.const import TYPE_ASTRONOMICAL, TYPE_METEOROLOGICAL
from homeassistant.components.season.sensor import (
STATE_AUTUMN,
STATE_SPRING,
STATE_SUMMER,
STATE_WINTER,
TYPE_ASTRONOMICAL,
TYPE_METEOROLOGICAL,
)
from homeassistant.const import STATE_UNKNOWN
from homeassistant.setup import async_setup_component
from homeassistant.const import CONF_TYPE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry
HEMISPHERE_NORTHERN = {
"homeassistant": {"latitude": 48.864716, "longitude": 2.349014},
@ -65,60 +67,80 @@ def idfn(val):
@pytest.mark.parametrize("type,day,expected", NORTHERN_PARAMETERS, ids=idfn)
async def test_season_northern_hemisphere(hass, type, day, expected):
async def test_season_northern_hemisphere(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
type: str,
day: datetime,
expected: str,
) -> None:
"""Test that season should be summer."""
hass.config.latitude = HEMISPHERE_NORTHERN["homeassistant"]["latitude"]
mock_config_entry.add_to_hass(hass)
hass.config_entries.async_update_entry(
mock_config_entry, unique_id=type, data={CONF_TYPE: type}
)
config = {
**HEMISPHERE_NORTHERN,
"sensor": {"platform": "season", "type": type},
}
with patch("homeassistant.components.season.sensor.utcnow", return_value=day):
assert await async_setup_component(hass, "sensor", config)
with freeze_time(day):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.season")
assert state
assert state.state == expected
entity_registry = er.async_get(hass)
entry = entity_registry.async_get("sensor.season")
assert entry
assert entry.unique_id == mock_config_entry.entry_id
@pytest.mark.parametrize("type,day,expected", SOUTHERN_PARAMETERS, ids=idfn)
async def test_season_southern_hemisphere(hass, type, day, expected):
async def test_season_southern_hemisphere(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
type: str,
day: datetime,
expected: str,
) -> None:
"""Test that season should be summer."""
hass.config.latitude = HEMISPHERE_SOUTHERN["homeassistant"]["latitude"]
mock_config_entry.add_to_hass(hass)
hass.config_entries.async_update_entry(
mock_config_entry, unique_id=type, data={CONF_TYPE: type}
)
config = {
**HEMISPHERE_SOUTHERN,
"sensor": {"platform": "season", "type": type},
}
with patch("homeassistant.components.season.sensor.utcnow", return_value=day):
assert await async_setup_component(hass, "sensor", config)
with freeze_time(day):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.season")
assert state
assert state.state == expected
entity_registry = er.async_get(hass)
entry = entity_registry.async_get("sensor.season")
assert entry
assert entry.unique_id == mock_config_entry.entry_id
async def test_season_equator(hass):
async def test_season_equator(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that season should be unknown for equator."""
hass.config.latitude = HEMISPHERE_EQUATOR["homeassistant"]["latitude"]
day = datetime(2017, 9, 3, 0, 0)
mock_config_entry.add_to_hass(hass)
with patch("homeassistant.components.season.sensor.utcnow", return_value=day):
assert await async_setup_component(hass, "sensor", HEMISPHERE_EQUATOR)
with freeze_time(datetime(2017, 9, 3, 0, 0)):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.season")
assert state
assert state.state == STATE_UNKNOWN
async def test_setup_hemisphere_empty(hass):
"""Test platform setup of missing latlong."""
hass.config.latitude = None
assert await async_setup_component(hass, "sensor", HEMISPHERE_EMPTY)
await hass.async_block_till_done()
assert hass.config.as_dict()["latitude"] is None
entity_registry = er.async_get(hass)
entry = entity_registry.async_get("sensor.season")
assert entry
assert entry.unique_id == mock_config_entry.entry_id