diff --git a/.strict-typing b/.strict-typing index a5a2127eb68b052ca6a1c3c5f23071134658cb79..1213a8b73b7a216b667055a5fd243efa9458f2cb 100644 --- a/.strict-typing +++ b/.strict-typing @@ -127,10 +127,11 @@ homeassistant.components.lookin.* homeassistant.components.luftdaten.* homeassistant.components.mailbox.* homeassistant.components.media_player.* +homeassistant.components.media_source.* homeassistant.components.mjpeg.* homeassistant.components.modbus.* homeassistant.components.modem_callerid.* -homeassistant.components.media_source.* +homeassistant.components.moon.* homeassistant.components.mysensors.* homeassistant.components.nam.* homeassistant.components.nanoleaf.* diff --git a/CODEOWNERS b/CODEOWNERS index 82930f875f42910cd2f2ba1e3ef3c0ecd51e6a1f..1c7256435355af6a17acae6ba69882a4e560f1ff 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -616,8 +616,8 @@ homeassistant/components/moehlenhoff_alpha2/* @j-a-n tests/components/moehlenhoff_alpha2/* @j-a-n homeassistant/components/monoprice/* @etsinko @OnFreund tests/components/monoprice/* @etsinko @OnFreund -homeassistant/components/moon/* @fabaff -tests/components/moon/* @fabaff +homeassistant/components/moon/* @fabaff @frenck +tests/components/moon/* @fabaff @frenck homeassistant/components/motion_blinds/* @starkillerOG tests/components/motion_blinds/* @starkillerOG homeassistant/components/motioneye/* @dermotduffy diff --git a/homeassistant/components/moon/__init__.py b/homeassistant/components/moon/__init__.py index f7049608dda90179b124282c63b735b1d9d049b8..0b36ba59198d3759b134e1136774915b1732afa6 100644 --- a/homeassistant/components/moon/__init__.py +++ b/homeassistant/components/moon/__init__.py @@ -1 +1,16 @@ -"""The moon component.""" +"""The Moon 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) diff --git a/homeassistant/components/moon/config_flow.py b/homeassistant/components/moon/config_flow.py new file mode 100644 index 0000000000000000000000000000000000000000..1f7c5715f9ea6103e2b0db37d53c281dcfa0155c --- /dev/null +++ b/homeassistant/components/moon/config_flow.py @@ -0,0 +1,37 @@ +"""Config flow to configure the Moon 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 +from homeassistant.data_entry_flow import FlowResult + +from .const import DEFAULT_NAME, DOMAIN + + +class MoonConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for Moon.""" + + 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=user_input.get(CONF_NAME, DEFAULT_NAME), + data={}, + ) + + return self.async_show_form(step_id="user", data_schema=vol.Schema({})) + + 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/moon/const.py b/homeassistant/components/moon/const.py new file mode 100644 index 0000000000000000000000000000000000000000..87c525758b95480df323677c1c94dbc7c37e8f25 --- /dev/null +++ b/homeassistant/components/moon/const.py @@ -0,0 +1,9 @@ +"""Constants for the Moon integration.""" +from typing import Final + +from homeassistant.const import Platform + +DOMAIN: Final = "moon" +PLATFORMS: Final = [Platform.SENSOR] + +DEFAULT_NAME: Final = "Moon" diff --git a/homeassistant/components/moon/manifest.json b/homeassistant/components/moon/manifest.json index 19fb952f59f013ceec9e34b97a318abc30d2c5fc..0402a87cf1a5d03a27098077f7301f715f0fb931 100644 --- a/homeassistant/components/moon/manifest.json +++ b/homeassistant/components/moon/manifest.json @@ -2,7 +2,8 @@ "domain": "moon", "name": "Moon", "documentation": "https://www.home-assistant.io/integrations/moon", - "codeowners": ["@fabaff"], + "codeowners": ["@fabaff", "@frenck"], "quality_scale": "internal", - "iot_class": "local_polling" + "iot_class": "local_polling", + "config_flow": true } diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index cd10d9168d9c619b672f9af2f41bd061213f3633..c5078771af8d86ed57d0c1c5d2ae5e071d8ab0cd 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -8,6 +8,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 from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -15,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -DEFAULT_NAME = "Moon" +from .const import DEFAULT_NAME, DOMAIN STATE_FIRST_QUARTER = "first_quarter" STATE_FULL_MOON = "full_moon" @@ -49,23 +50,37 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Moon sensor.""" - name: str = config[CONF_NAME] + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) - async_add_entities([MoonSensor(name)], True) + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the platform from config_entry.""" + async_add_entities([MoonSensorEntity(entry)], True) -class MoonSensor(SensorEntity): +class MoonSensorEntity(SensorEntity): """Representation of a Moon sensor.""" _attr_device_class = "moon__phase" - def __init__(self, name: str) -> None: + def __init__(self, entry: ConfigEntry) -> None: """Initialize the moon sensor.""" - self._attr_name = name + self._attr_name = entry.title + self._attr_unique_id = entry.entry_id - async def async_update(self): + async def async_update(self) -> None: """Get the time and updates the states.""" - today = dt_util.as_local(dt_util.utcnow()).date() + today = dt_util.now().date() state = moon.phase(today) if state < 0.5 or state > 27.5: diff --git a/homeassistant/components/moon/strings.json b/homeassistant/components/moon/strings.json new file mode 100644 index 0000000000000000000000000000000000000000..d5bb204a740748669a2990239809b647815c7fd4 --- /dev/null +++ b/homeassistant/components/moon/strings.json @@ -0,0 +1,13 @@ +{ + "title": "Moon", + "config": { + "step": { + "user": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + } +} diff --git a/homeassistant/components/moon/translations/en.json b/homeassistant/components/moon/translations/en.json new file mode 100644 index 0000000000000000000000000000000000000000..0f324f7b64b38d77891d8acafcc2addc6e4d2e2b --- /dev/null +++ b/homeassistant/components/moon/translations/en.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "user": { + "description": "Do you want to start set up?" + } + } + }, + "title": "Moon" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 5530c73ed5098f16c5acbd4e7a1284d105e60ae1..7a36cfebc7b83bee2659054678bd811386f77d1e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -203,6 +203,7 @@ FLOWS = [ "modern_forms", "moehlenhoff_alpha2", "monoprice", + "moon", "motion_blinds", "motioneye", "mqtt", diff --git a/mypy.ini b/mypy.ini index 045e9a4eee8091664ed5fe1e1ca0be0cce99fcc2..7a927a3c6a77ec389d2a7ed6f058e37d35f1aa9c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1198,6 +1198,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.media_source.*] +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.mjpeg.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1231,7 +1242,7 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.media_source.*] +[mypy-homeassistant.components.moon.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true diff --git a/tests/components/moon/conftest.py b/tests/components/moon/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..5c8157f257df5c2ef6ed63697617a38dc45027e4 --- /dev/null +++ b/tests/components/moon/conftest.py @@ -0,0 +1,27 @@ +"""Fixtures for Moon integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import patch + +import pytest + +from homeassistant.components.moon.const import DOMAIN + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Moon", + domain=DOMAIN, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[None, None, None]: + """Mock setting up a config entry.""" + with patch("homeassistant.components.moon.async_setup_entry", return_value=True): + yield diff --git a/tests/components/moon/test_config_flow.py b/tests/components/moon/test_config_flow.py new file mode 100644 index 0000000000000000000000000000000000000000..4bfb61166aa1680dcedd07cf42484e6707e91ec5 --- /dev/null +++ b/tests/components/moon/test_config_flow.py @@ -0,0 +1,72 @@ +"""Tests for the Moon config flow.""" +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.moon.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_NAME +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={}, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "Moon" + assert result2.get("data") == {} + + +@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} + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "single_instance_allowed" + + +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 Moon"}, + ) + + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("title") == "My Moon" + assert result.get("data") == {} diff --git a/tests/components/moon/test_init.py b/tests/components/moon/test_init.py new file mode 100644 index 0000000000000000000000000000000000000000..f0f7e5935458fae2dd1312970cc121a4038d1589 --- /dev/null +++ b/tests/components/moon/test_init.py @@ -0,0 +1,55 @@ +"""Tests for the Moon integration.""" +from unittest.mock import AsyncMock + +from homeassistant.components.moon.const import DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_NAME +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 Moon 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 Moon being set up from config via import.""" + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + { + SENSOR_DOMAIN: { + "platform": DOMAIN, + CONF_NAME: "My Moon", + } + }, + ) + 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 Moon" + assert entry.unique_id is None + assert entry.data == {} diff --git a/tests/components/moon/test_sensor.py b/tests/components/moon/test_sensor.py index 066620b1051bb97e88312a80ff60c685793302a6..bb9e5dcc1570dabd8aa5c837aec0b7257da37277 100644 --- a/tests/components/moon/test_sensor.py +++ b/tests/components/moon/test_sensor.py @@ -5,10 +5,6 @@ from unittest.mock import patch import pytest -from homeassistant.components.homeassistant import ( - DOMAIN as HA_DOMAIN, - SERVICE_UPDATE_ENTITY, -) from homeassistant.components.moon.sensor import ( MOON_ICONS, STATE_FIRST_QUARTER, @@ -20,9 +16,11 @@ from homeassistant.components.moon.sensor import ( STATE_WAXING_CRESCENT, STATE_WAXING_GIBBOUS, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ICON from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry @pytest.mark.parametrize( @@ -39,33 +37,27 @@ from homeassistant.setup import async_setup_component ], ) async def test_moon_day( - hass: HomeAssistant, moon_value: float, native_value: str, icon: str + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + moon_value: float, + native_value: str, + icon: str, ) -> None: """Test the Moon sensor.""" - config = {"sensor": {"platform": "moon"}} - - await async_setup_component(hass, HA_DOMAIN, {}) - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - assert hass.states.get("sensor.moon") + mock_config_entry.add_to_hass(hass) with patch( "homeassistant.components.moon.sensor.moon.phase", return_value=moon_value ): - await async_update_entity(hass, "sensor.moon") + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() state = hass.states.get("sensor.moon") + assert state assert state.state == native_value - assert state.attributes["icon"] == icon - + assert state.attributes[ATTR_ICON] == icon -async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None: - """Run an update action for an entity.""" - await hass.services.async_call( - HA_DOMAIN, - SERVICE_UPDATE_ENTITY, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - await hass.async_block_till_done() + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("sensor.moon") + assert entry + assert entry.unique_id == mock_config_entry.entry_id