From 4ac92d755e4b802aa1a9802b1996938a128b036c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 30 Jun 2023 12:58:07 +0200 Subject: [PATCH] Add config flow for zodiac (#95447) * Add config flow for zodiac * Add config flow for zodiac * Fix feedback --- homeassistant/components/zodiac/__init__.py | 29 +++++++- .../components/zodiac/config_flow.py | 31 ++++++++ homeassistant/components/zodiac/const.py | 1 + homeassistant/components/zodiac/manifest.json | 3 +- homeassistant/components/zodiac/sensor.py | 27 ++++--- homeassistant/components/zodiac/strings.json | 16 +++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 4 +- tests/components/zodiac/test_config_flow.py | 70 +++++++++++++++++++ tests/components/zodiac/test_sensor.py | 12 +++- 10 files changed, 177 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/zodiac/config_flow.py create mode 100644 tests/components/zodiac/test_config_flow.py diff --git a/homeassistant/components/zodiac/__init__.py b/homeassistant/components/zodiac/__init__.py index 35d4d2eefbf..892bcac5bf9 100644 --- a/homeassistant/components/zodiac/__init__.py +++ b/homeassistant/components/zodiac/__init__.py @@ -1,9 +1,10 @@ """The zodiac component.""" import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -16,8 +17,32 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the zodiac component.""" + + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2024.1.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + hass.async_create_task( - async_load_platform(hass, Platform.SENSOR, DOMAIN, {}, config) + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) ) return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Load a config entry.""" + await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR]) + 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, [Platform.SENSOR]) diff --git a/homeassistant/components/zodiac/config_flow.py b/homeassistant/components/zodiac/config_flow.py new file mode 100644 index 00000000000..ebc0a819d1d --- /dev/null +++ b/homeassistant/components/zodiac/config_flow.py @@ -0,0 +1,31 @@ +"""Config flow to configure the Zodiac integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult + +from .const import DEFAULT_NAME, DOMAIN + + +class ZodiacConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for Zodiac.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if user_input is not None: + return self.async_create_entry(title=DEFAULT_NAME, data={}) + + return self.async_show_form(step_id="user") + + 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) diff --git a/homeassistant/components/zodiac/const.py b/homeassistant/components/zodiac/const.py index c3e7f13d5e3..f50e108c2aa 100644 --- a/homeassistant/components/zodiac/const.py +++ b/homeassistant/components/zodiac/const.py @@ -1,5 +1,6 @@ """Constants for Zodiac.""" DOMAIN = "zodiac" +DEFAULT_NAME = "Zodiac" # Signs SIGN_ARIES = "aries" diff --git a/homeassistant/components/zodiac/manifest.json b/homeassistant/components/zodiac/manifest.json index ceacbf1645a..88f3d7fadef 100644 --- a/homeassistant/components/zodiac/manifest.json +++ b/homeassistant/components/zodiac/manifest.json @@ -2,7 +2,8 @@ "domain": "zodiac", "name": "Zodiac", "codeowners": ["@JulienTant"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zodiac", - "iot_class": "local_polling", + "iot_class": "calculated", "quality_scale": "silver" } diff --git a/homeassistant/components/zodiac/sensor.py b/homeassistant/components/zodiac/sensor.py index f63c844701f..d9b306da4dd 100644 --- a/homeassistant/components/zodiac/sensor.py +++ b/homeassistant/components/zodiac/sensor.py @@ -2,14 +2,17 @@ from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.dt import as_local, utcnow from .const import ( ATTR_ELEMENT, ATTR_MODALITY, + DEFAULT_NAME, DOMAIN, ELEMENT_AIR, ELEMENT_EARTH, @@ -159,23 +162,21 @@ ZODIAC_ICONS = { } -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Zodiac sensor platform.""" - if discovery_info is None: - return + """Initialize the entries.""" - async_add_entities([ZodiacSensor()], True) + async_add_entities([ZodiacSensor(entry_id=entry.entry_id)], True) class ZodiacSensor(SensorEntity): """Representation of a Zodiac sensor.""" - _attr_name = "Zodiac" + _attr_name = None + _attr_has_entity_name = True _attr_device_class = SensorDeviceClass.ENUM _attr_options = [ SIGN_AQUARIUS, @@ -194,6 +195,14 @@ class ZodiacSensor(SensorEntity): _attr_translation_key = "sign" _attr_unique_id = DOMAIN + def __init__(self, entry_id: str) -> None: + """Initialize Zodiac sensor.""" + self._attr_device_info = DeviceInfo( + name=DEFAULT_NAME, + identifiers={(DOMAIN, entry_id)}, + entry_type=DeviceEntryType.SERVICE, + ) + async def async_update(self) -> None: """Get the time and updates the state.""" today = as_local(utcnow()).date() diff --git a/homeassistant/components/zodiac/strings.json b/homeassistant/components/zodiac/strings.json index cbae6ead433..8cf0e22237e 100644 --- a/homeassistant/components/zodiac/strings.json +++ b/homeassistant/components/zodiac/strings.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "user": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + }, "entity": { "sensor": { "sign": { @@ -18,5 +28,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "The Zodiac YAML configuration is being removed", + "description": "Configuring Zodiac using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Zodiac YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3145c5cdc49..2925ea3425c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -534,6 +534,7 @@ FLOWS = { "zerproc", "zeversolar", "zha", + "zodiac", "zwave_js", "zwave_me", ], diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 26833d62368..98571b6905e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6541,8 +6541,8 @@ "zodiac": { "name": "Zodiac", "integration_type": "hub", - "config_flow": false, - "iot_class": "local_polling" + "config_flow": true, + "iot_class": "calculated" }, "zoneminder": { "name": "ZoneMinder", diff --git a/tests/components/zodiac/test_config_flow.py b/tests/components/zodiac/test_config_flow.py new file mode 100644 index 00000000000..18a512e0b45 --- /dev/null +++ b/tests/components/zodiac/test_config_flow.py @@ -0,0 +1,70 @@ +"""Tests for the Zodiac config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.zodiac.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_full_user_flow(hass: HomeAssistant) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "user" + + with patch( + "homeassistant.components.zodiac.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + + assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("title") == "Zodiac" + assert result.get("data") == {} + assert result.get("options") == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT]) +async def test_single_instance_allowed( + hass: HomeAssistant, + source: str, +) -> None: + """Test we abort if already setup.""" + mock_config_entry = MockConfigEntry(domain=DOMAIN) + + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source} + ) + + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "single_instance_allowed" + + +async def test_import_flow( + hass: HomeAssistant, +) -> None: + """Test the import configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={}, + ) + + assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("title") == "Zodiac" + assert result.get("data") == {} + assert result.get("options") == {} diff --git a/tests/components/zodiac/test_sensor.py b/tests/components/zodiac/test_sensor.py index dbb1d2739a5..9fa151c87d5 100644 --- a/tests/components/zodiac/test_sensor.py +++ b/tests/components/zodiac/test_sensor.py @@ -24,6 +24,8 @@ from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.common import MockConfigEntry + DAY1 = datetime(2020, 11, 15, tzinfo=dt_util.UTC) DAY2 = datetime(2020, 4, 20, tzinfo=dt_util.UTC) DAY3 = datetime(2020, 4, 21, tzinfo=dt_util.UTC) @@ -37,13 +39,17 @@ DAY3 = datetime(2020, 4, 21, tzinfo=dt_util.UTC) (DAY3, SIGN_TAURUS, ELEMENT_EARTH, MODALITY_FIXED), ], ) -async def test_zodiac_day(hass: HomeAssistant, now, sign, element, modality) -> None: +async def test_zodiac_day( + hass: HomeAssistant, now: datetime, sign: str, element: str, modality: str +) -> None: """Test the zodiac sensor.""" hass.config.set_time_zone("UTC") - config = {DOMAIN: {}} + MockConfigEntry( + domain=DOMAIN, + ).add_to_hass(hass) with patch("homeassistant.components.zodiac.sensor.utcnow", return_value=now): - assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() state = hass.states.get("sensor.zodiac")