From 2c09f72c3350e9a3f2074247d137e2392b052765 Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Fri, 24 May 2024 15:04:17 +0300 Subject: [PATCH] Add config flow to Jewish Calendar (#84464) * Initial commit * add basic tests (will probably fail) * Set basic UID for now * Various improvements * use new naming convention? * bit by bit, still not working tho * Add tz selection * Remove failing tests * update unique_id * add the tests again * revert to previous binary_sensor test * remove translations * apply suggestions * remove const.py * Address review * revert changes * Initial fixes for tests * Initial commit * add basic tests (will probably fail) * Set basic UID for now * Various improvements * use new naming convention? * bit by bit, still not working tho * Add tz selection * Remove failing tests * update unique_id * add the tests again * revert to previous binary_sensor test * remove translations * apply suggestions * remove const.py * Address review * revert changes * Fix bad merges in rebase * Get tests to run again * Fixes due to fails in ruff/pylint * Fix binary sensor tests * Fix config flow tests * Fix sensor tests * Apply review * Adjust candle lights * Apply suggestion * revert unrelated change * Address some of the comments * We should only allow a single jewish calendar config entry * Make data schema easier to read * Add test and confirm only single entry is allowed * Move OPTIONS_SCHEMA to top of file * Add options test * Simplify import tests * Test import end2end * Use a single async_forward_entry_setups statement * Revert schema updates for YAML schema * Remove unneeded brackets * Remove CONF_NAME from config_flow * Assign hass.data[DOMAIN][config_entry.entry_id] to a local variable before creating the sensors * Data doesn't have a name remove slugifying of it * Test that the entry has been created correctly * Simplify setup_entry * Use suggested values helper and flatten location dictionary * Remove the string for name exists as this error doesn't exist * Remove name from config entry * Remove _attr_has_entity_name - will be added in a subsequent PR * Don't override entity id's - we'll fixup the naming later * Make location optional - will by default revert to the user's home location * Update homeassistant/components/jewish_calendar/strings.json Co-authored-by: Erik Montnemery * No need for local lat/long variable * Return name attribute, will deal with it in another PR * Revert unique_id changes, will deal with this in a subsequent PR * Add time zone data description * Don't break the YAML config until the user has removed it. * Cleanup initial config flow test --------- Co-authored-by: Tsvi Mostovicz Co-authored-by: Erik Montnemery --- .../components/jewish_calendar/__init__.py | 112 ++++++++++---- .../jewish_calendar/binary_sensor.py | 34 +++-- .../components/jewish_calendar/config_flow.py | 135 +++++++++++++++++ .../components/jewish_calendar/manifest.json | 4 +- .../components/jewish_calendar/sensor.py | 40 +++-- .../components/jewish_calendar/strings.json | 37 +++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 5 +- tests/components/jewish_calendar/__init__.py | 4 +- tests/components/jewish_calendar/conftest.py | 28 ++++ .../jewish_calendar/test_binary_sensor.py | 89 +++++------ .../jewish_calendar/test_config_flow.py | 138 ++++++++++++++++++ .../components/jewish_calendar/test_sensor.py | 99 +++++-------- 13 files changed, 544 insertions(+), 182 deletions(-) create mode 100644 homeassistant/components/jewish_calendar/config_flow.py create mode 100644 homeassistant/components/jewish_calendar/strings.json create mode 100644 tests/components/jewish_calendar/conftest.py create mode 100644 tests/components/jewish_calendar/test_config_flow.py diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index 1ce5386d2c2..e1178851e83 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -5,41 +5,54 @@ from __future__ import annotations from hdate import Location import voluptuous as vol -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, Platform -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ( + CONF_ELEVATION, + CONF_LANGUAGE, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + CONF_TIME_ZONE, + Platform, +) +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType DOMAIN = "jewish_calendar" -PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] - CONF_DIASPORA = "diaspora" -CONF_LANGUAGE = "language" CONF_CANDLE_LIGHT_MINUTES = "candle_lighting_minutes_before_sunset" CONF_HAVDALAH_OFFSET_MINUTES = "havdalah_minutes_after_sunset" - -CANDLE_LIGHT_DEFAULT = 18 - DEFAULT_NAME = "Jewish Calendar" +DEFAULT_CANDLE_LIGHT = 18 +DEFAULT_DIASPORA = False +DEFAULT_HAVDALAH_OFFSET_MINUTES = 0 +DEFAULT_LANGUAGE = "english" + +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( + DOMAIN: vol.All( + cv.deprecated(DOMAIN), { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_DIASPORA, default=False): cv.boolean, + vol.Optional(CONF_DIASPORA, default=DEFAULT_DIASPORA): cv.boolean, vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, - vol.Optional(CONF_LANGUAGE, default="english"): vol.In( + vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In( ["hebrew", "english"] ), vol.Optional( - CONF_CANDLE_LIGHT_MINUTES, default=CANDLE_LIGHT_DEFAULT + CONF_CANDLE_LIGHT_MINUTES, default=DEFAULT_CANDLE_LIGHT ): int, # Default of 0 means use 8.5 degrees / 'three_stars' time. - vol.Optional(CONF_HAVDALAH_OFFSET_MINUTES, default=0): int, - } + vol.Optional( + CONF_HAVDALAH_OFFSET_MINUTES, + default=DEFAULT_HAVDALAH_OFFSET_MINUTES, + ): int, + }, ) }, extra=vol.ALLOW_EXTRA, @@ -72,37 +85,72 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if DOMAIN not in config: return True - name = config[DOMAIN][CONF_NAME] - language = config[DOMAIN][CONF_LANGUAGE] + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + is_fixable=False, + breaks_in_ha_version="2024.10.0", + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": DEFAULT_NAME, + }, + ) - latitude = config[DOMAIN].get(CONF_LATITUDE, hass.config.latitude) - longitude = config[DOMAIN].get(CONF_LONGITUDE, hass.config.longitude) - diaspora = config[DOMAIN][CONF_DIASPORA] + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] + ) + ) - candle_lighting_offset = config[DOMAIN][CONF_CANDLE_LIGHT_MINUTES] - havdalah_offset = config[DOMAIN][CONF_HAVDALAH_OFFSET_MINUTES] + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up a configuration entry for Jewish calendar.""" + language = config_entry.data.get(CONF_LANGUAGE, DEFAULT_LANGUAGE) + diaspora = config_entry.data.get(CONF_DIASPORA, DEFAULT_DIASPORA) + candle_lighting_offset = config_entry.data.get( + CONF_CANDLE_LIGHT_MINUTES, DEFAULT_CANDLE_LIGHT + ) + havdalah_offset = config_entry.data.get( + CONF_HAVDALAH_OFFSET_MINUTES, DEFAULT_HAVDALAH_OFFSET_MINUTES + ) location = Location( - latitude=latitude, - longitude=longitude, - timezone=hass.config.time_zone, + name=hass.config.location_name, diaspora=diaspora, + latitude=config_entry.data.get(CONF_LATITUDE, hass.config.latitude), + longitude=config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), + altitude=config_entry.data.get(CONF_ELEVATION, hass.config.elevation), + timezone=config_entry.data.get(CONF_TIME_ZONE, hass.config.time_zone), ) prefix = get_unique_prefix( location, language, candle_lighting_offset, havdalah_offset ) - hass.data[DOMAIN] = { - "location": location, - "name": name, + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = { "language": language, + "diaspora": diaspora, + "location": location, "candle_lighting_offset": candle_lighting_offset, "havdalah_offset": havdalah_offset, - "diaspora": diaspora, "prefix": prefix, } - for platform in PLATFORMS: - hass.async_create_task(async_load_platform(hass, platform, DOMAIN, {}, config)) - + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index 8566cb22814..b01dbc2652e 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -6,6 +6,7 @@ from collections.abc import Callable from dataclasses import dataclass import datetime as dt from datetime import datetime +from typing import Any import hdate from hdate.zmanim import Zmanim @@ -14,13 +15,14 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import event from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -from . import DOMAIN +from . import DEFAULT_NAME, DOMAIN @dataclass(frozen=True) @@ -63,15 +65,25 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Jewish Calendar binary sensor devices.""" - if discovery_info is None: - return + """Set up the Jewish calendar binary sensors from YAML. + The YAML platform config is automatically + imported to a config entry, this method can be removed + when YAML support is removed. + """ + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Jewish Calendar binary sensors.""" async_add_entities( - [ - JewishCalendarBinarySensor(hass.data[DOMAIN], description) - for description in BINARY_SENSORS - ] + JewishCalendarBinarySensor( + hass.data[DOMAIN][config_entry.entry_id], description + ) + for description in BINARY_SENSORS ) @@ -83,13 +95,13 @@ class JewishCalendarBinarySensor(BinarySensorEntity): def __init__( self, - data: dict[str, str | bool | int | float], + data: dict[str, Any], description: JewishCalendarBinarySensorEntityDescription, ) -> None: """Initialize the binary sensor.""" self.entity_description = description - self._attr_name = f"{data['name']} {description.name}" - self._attr_unique_id = f"{data['prefix']}_{description.key}" + self._attr_name = f"{DEFAULT_NAME} {description.name}" + self._attr_unique_id = f'{data["prefix"]}_{description.key}' self._location = data["location"] self._hebrew = data["language"] == "hebrew" self._candle_lighting_offset = data["candle_lighting_offset"] diff --git a/homeassistant/components/jewish_calendar/config_flow.py b/homeassistant/components/jewish_calendar/config_flow.py new file mode 100644 index 00000000000..5632b7cd584 --- /dev/null +++ b/homeassistant/components/jewish_calendar/config_flow.py @@ -0,0 +1,135 @@ +"""Config flow for Jewish calendar integration.""" + +from __future__ import annotations + +import logging +from typing import Any +import zoneinfo + +import voluptuous as vol + +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlowWithConfigEntry, +) +from homeassistant.const import ( + CONF_ELEVATION, + CONF_LANGUAGE, + CONF_LATITUDE, + CONF_LOCATION, + CONF_LONGITUDE, + CONF_TIME_ZONE, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.selector import ( + BooleanSelector, + LocationSelector, + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, +) +from homeassistant.helpers.typing import ConfigType + +DOMAIN = "jewish_calendar" +CONF_DIASPORA = "diaspora" +CONF_CANDLE_LIGHT_MINUTES = "candle_lighting_minutes_before_sunset" +CONF_HAVDALAH_OFFSET_MINUTES = "havdalah_minutes_after_sunset" +DEFAULT_NAME = "Jewish Calendar" +DEFAULT_CANDLE_LIGHT = 18 +DEFAULT_DIASPORA = False +DEFAULT_HAVDALAH_OFFSET_MINUTES = 0 +DEFAULT_LANGUAGE = "english" + +LANGUAGE = [ + SelectOptionDict(value="hebrew", label="Hebrew"), + SelectOptionDict(value="english", label="English"), +] + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Optional(CONF_CANDLE_LIGHT_MINUTES, default=DEFAULT_CANDLE_LIGHT): int, + vol.Optional( + CONF_HAVDALAH_OFFSET_MINUTES, default=DEFAULT_HAVDALAH_OFFSET_MINUTES + ): int, + } +) + + +_LOGGER = logging.getLogger(__name__) + + +def _get_data_schema(hass: HomeAssistant) -> vol.Schema: + default_location = { + CONF_LATITUDE: hass.config.latitude, + CONF_LONGITUDE: hass.config.longitude, + } + return vol.Schema( + { + vol.Required(CONF_DIASPORA, default=DEFAULT_DIASPORA): BooleanSelector(), + vol.Required(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): SelectSelector( + SelectSelectorConfig(options=LANGUAGE) + ), + vol.Optional(CONF_LOCATION, default=default_location): LocationSelector(), + vol.Optional(CONF_ELEVATION, default=hass.config.elevation): int, + vol.Optional(CONF_TIME_ZONE, default=hass.config.time_zone): SelectSelector( + SelectSelectorConfig( + options=sorted(zoneinfo.available_timezones()), + ) + ), + } + ) + + +class JewishCalendarConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Jewish calendar.""" + + VERSION = 1 + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowWithConfigEntry: + """Get the options flow for this handler.""" + return JewishCalendarOptionsFlowHandler(config_entry) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + if user_input is not None: + if CONF_LOCATION in user_input: + user_input[CONF_LATITUDE] = user_input[CONF_LOCATION][CONF_LATITUDE] + user_input[CONF_LONGITUDE] = user_input[CONF_LOCATION][CONF_LONGITUDE] + return self.async_create_entry(title=DEFAULT_NAME, data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=self.add_suggested_values_to_schema( + _get_data_schema(self.hass), user_input + ), + ) + + async def async_step_import( + self, import_config: ConfigType | None + ) -> ConfigFlowResult: + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(import_config) + + +class JewishCalendarOptionsFlowHandler(OptionsFlowWithConfigEntry): + """Handle Jewish Calendar options.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> ConfigFlowResult: + """Manage the Jewish Calendar options.""" + if user_input is not None: + return self.async_create_entry(data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=self.add_suggested_values_to_schema( + OPTIONS_SCHEMA, self.config_entry.options + ), + ) diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index 0473391abc8..20eb28929bd 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -2,8 +2,10 @@ "domain": "jewish_calendar", "name": "Jewish Calendar", "codeowners": ["@tsvi"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", "iot_class": "calculated", "loggers": ["hdate"], - "requirements": ["hdate==0.10.8"] + "requirements": ["hdate==0.10.8"], + "single_config_entry": true } diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index edbc7bf0c22..1616dc589d7 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -1,4 +1,4 @@ -"""Platform to retrieve Jewish calendar information for Home Assistant.""" +"""Support for Jewish calendar sensors.""" from __future__ import annotations @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import SUN_EVENT_SUNSET from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -21,11 +22,11 @@ from homeassistant.helpers.sun import get_astral_event_date from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -from . import DOMAIN +from . import DEFAULT_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) -INFO_SENSORS = ( +INFO_SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="date", name="Date", @@ -53,7 +54,7 @@ INFO_SENSORS = ( ), ) -TIME_SENSORS = ( +TIME_SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="first_light", name="Alot Hashachar", # codespell:ignore alot @@ -148,17 +149,24 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Jewish calendar sensor platform.""" - if discovery_info is None: - return + """Set up the Jewish calendar sensors from YAML. - sensors = [ - JewishCalendarSensor(hass.data[DOMAIN], description) - for description in INFO_SENSORS - ] + The YAML platform config is automatically + imported to a config entry, this method can be removed + when YAML support is removed. + """ + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Jewish calendar sensors .""" + entry = hass.data[DOMAIN][config_entry.entry_id] + sensors = [JewishCalendarSensor(entry, description) for description in INFO_SENSORS] sensors.extend( - JewishCalendarTimeSensor(hass.data[DOMAIN], description) - for description in TIME_SENSORS + JewishCalendarTimeSensor(entry, description) for description in TIME_SENSORS ) async_add_entities(sensors) @@ -169,13 +177,13 @@ class JewishCalendarSensor(SensorEntity): def __init__( self, - data: dict[str, str | bool | int | float], + data: dict[str, Any], description: SensorEntityDescription, ) -> None: """Initialize the Jewish calendar sensor.""" self.entity_description = description - self._attr_name = f"{data['name']} {description.name}" - self._attr_unique_id = f"{data['prefix']}_{description.key}" + self._attr_name = f"{DEFAULT_NAME} {description.name}" + self._attr_unique_id = f'{data["prefix"]}_{description.key}' self._location = data["location"] self._hebrew = data["language"] == "hebrew" self._candle_lighting_offset = data["candle_lighting_offset"] diff --git a/homeassistant/components/jewish_calendar/strings.json b/homeassistant/components/jewish_calendar/strings.json new file mode 100644 index 00000000000..ce659cc0d06 --- /dev/null +++ b/homeassistant/components/jewish_calendar/strings.json @@ -0,0 +1,37 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "diaspora": "Outside of Israel?", + "language": "Language for Holidays and Dates", + "location": "[%key:common::config_flow::data::location%]", + "elevation": "[%key:common::config_flow::data::elevation%]", + "time_zone": "Time Zone" + }, + "data_description": { + "time_zone": "If you specify a location, make sure to specify the time zone for correct calendar times calculations" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "options": { + "step": { + "init": { + "title": "Configure options for Jewish Calendar", + "data": { + "candle_lighting_minutes_before_sunset": "Minutes before sunset for candle lighthing", + "havdalah_minutes_after_sunset": "Minutes after sunset for Havdalah" + }, + "data_description": { + "candle_lighting_minutes_before_sunset": "Defaults to 18 minutes. In Israel you probably want to use 20/30/40 depending on your location. Outside of Israel you probably want to use 18/24.", + "havdalah_minutes_after_sunset": "Setting this to 0 means 36 minutes as fixed degrees (8.5°) will be used instead" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 78d96990ee9..e4ab6db9f48 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -269,6 +269,7 @@ FLOWS = { "isy994", "izone", "jellyfin", + "jewish_calendar", "juicenet", "justnimbus", "jvc_projector", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 0955f4157d7..936e2d586fb 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2950,8 +2950,9 @@ "jewish_calendar": { "name": "Jewish Calendar", "integration_type": "hub", - "config_flow": false, - "iot_class": "calculated" + "config_flow": true, + "iot_class": "calculated", + "single_config_entry": true }, "joaoapps_join": { "name": "Joaoapps Join", diff --git a/tests/components/jewish_calendar/__init__.py b/tests/components/jewish_calendar/__init__.py index e1352f789ac..60726fc3a3e 100644 --- a/tests/components/jewish_calendar/__init__.py +++ b/tests/components/jewish_calendar/__init__.py @@ -27,7 +27,7 @@ def make_nyc_test_params(dtime, results, havdalah_offset=0): } return ( dtime, - jewish_calendar.CANDLE_LIGHT_DEFAULT, + jewish_calendar.DEFAULT_CANDLE_LIGHT, havdalah_offset, True, "America/New_York", @@ -49,7 +49,7 @@ def make_jerusalem_test_params(dtime, results, havdalah_offset=0): } return ( dtime, - jewish_calendar.CANDLE_LIGHT_DEFAULT, + jewish_calendar.DEFAULT_CANDLE_LIGHT, havdalah_offset, False, "Asia/Jerusalem", diff --git a/tests/components/jewish_calendar/conftest.py b/tests/components/jewish_calendar/conftest.py new file mode 100644 index 00000000000..5f01ddf8f4a --- /dev/null +++ b/tests/components/jewish_calendar/conftest.py @@ -0,0 +1,28 @@ +"""Common fixtures for the jewish_calendar tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.jewish_calendar import config_flow + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title=config_flow.DEFAULT_NAME, + domain=config_flow.DOMAIN, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.jewish_calendar.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index ce59c7fe189..42d69e42afc 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -1,6 +1,7 @@ """The tests for the Jewish calendar binary sensors.""" from datetime import datetime as dt, timedelta +import logging import pytest @@ -8,18 +9,15 @@ from homeassistant.components import jewish_calendar from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from . import ( - HDATE_DEFAULT_ALTITUDE, - alter_time, - make_jerusalem_test_params, - make_nyc_test_params, -) +from . import alter_time, make_jerusalem_test_params, make_nyc_test_params + +from tests.common import MockConfigEntry, async_fire_time_changed + +_LOGGER = logging.getLogger(__name__) -from tests.common import async_fire_time_changed MELACHA_PARAMS = [ make_nyc_test_params( @@ -170,7 +168,6 @@ MELACHA_TEST_IDS = [ ) async def test_issur_melacha_sensor( hass: HomeAssistant, - entity_registry: er.EntityRegistry, now, candle_lighting, havdalah, @@ -189,49 +186,33 @@ async def test_issur_melacha_sensor( hass.config.longitude = longitude with alter_time(test_time): - assert await async_setup_component( - hass, - jewish_calendar.DOMAIN, - { - "jewish_calendar": { - "name": "test", - "language": "english", - "diaspora": diaspora, - "candle_lighting_minutes_before_sunset": candle_lighting, - "havdalah_minutes_after_sunset": havdalah, - } + entry = MockConfigEntry( + domain=jewish_calendar.DOMAIN, + data={ + "language": "english", + "diaspora": diaspora, + "candle_lighting_minutes_before_sunset": candle_lighting, + "havdalah_minutes_after_sunset": havdalah, }, ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert ( - hass.states.get("binary_sensor.test_issur_melacha_in_effect").state + hass.states.get( + "binary_sensor.jewish_calendar_issur_melacha_in_effect" + ).state == result["state"] ) - entity = entity_registry.async_get("binary_sensor.test_issur_melacha_in_effect") - target_uid = "_".join( - map( - str, - [ - latitude, - longitude, - tzname, - HDATE_DEFAULT_ALTITUDE, - diaspora, - "english", - candle_lighting, - havdalah, - "issur_melacha_in_effect", - ], - ) - ) - assert entity.unique_id == target_uid with alter_time(result["update"]): async_fire_time_changed(hass, result["update"]) await hass.async_block_till_done() assert ( - hass.states.get("binary_sensor.test_issur_melacha_in_effect").state + hass.states.get( + "binary_sensor.jewish_calendar_issur_melacha_in_effect" + ).state == result["new_state"] ) @@ -277,22 +258,22 @@ async def test_issur_melacha_sensor_update( hass.config.longitude = longitude with alter_time(test_time): - assert await async_setup_component( - hass, - jewish_calendar.DOMAIN, - { - "jewish_calendar": { - "name": "test", - "language": "english", - "diaspora": diaspora, - "candle_lighting_minutes_before_sunset": candle_lighting, - "havdalah_minutes_after_sunset": havdalah, - } + entry = MockConfigEntry( + domain=jewish_calendar.DOMAIN, + data={ + "language": "english", + "diaspora": diaspora, + "candle_lighting_minutes_before_sunset": candle_lighting, + "havdalah_minutes_after_sunset": havdalah, }, ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert ( - hass.states.get("binary_sensor.test_issur_melacha_in_effect").state + hass.states.get( + "binary_sensor.jewish_calendar_issur_melacha_in_effect" + ).state == result[0] ) @@ -301,7 +282,9 @@ async def test_issur_melacha_sensor_update( async_fire_time_changed(hass, test_time) await hass.async_block_till_done() assert ( - hass.states.get("binary_sensor.test_issur_melacha_in_effect").state + hass.states.get( + "binary_sensor.jewish_calendar_issur_melacha_in_effect" + ).state == result[1] ) diff --git a/tests/components/jewish_calendar/test_config_flow.py b/tests/components/jewish_calendar/test_config_flow.py new file mode 100644 index 00000000000..9d0dec1b83d --- /dev/null +++ b/tests/components/jewish_calendar/test_config_flow.py @@ -0,0 +1,138 @@ +"""Test the Jewish calendar config flow.""" + +from unittest.mock import AsyncMock + +import pytest + +from homeassistant import config_entries, setup +from homeassistant.components.jewish_calendar import ( + CONF_CANDLE_LIGHT_MINUTES, + CONF_DIASPORA, + CONF_HAVDALAH_OFFSET_MINUTES, + CONF_LANGUAGE, + DEFAULT_CANDLE_LIGHT, + DEFAULT_DIASPORA, + DEFAULT_HAVDALAH_OFFSET_MINUTES, + DEFAULT_LANGUAGE, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import ( + CONF_ELEVATION, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + CONF_TIME_ZONE, +) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_step_user(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test user config.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_DIASPORA: DEFAULT_DIASPORA, CONF_LANGUAGE: DEFAULT_LANGUAGE}, + ) + + assert result2["type"] is FlowResultType.CREATE_ENTRY + + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].data[CONF_DIASPORA] == DEFAULT_DIASPORA + assert entries[0].data[CONF_LANGUAGE] == DEFAULT_LANGUAGE + assert entries[0].data[CONF_LATITUDE] == hass.config.latitude + assert entries[0].data[CONF_LONGITUDE] == hass.config.longitude + assert entries[0].data[CONF_ELEVATION] == hass.config.elevation + assert entries[0].data[CONF_TIME_ZONE] == hass.config.time_zone + + +@pytest.mark.parametrize("diaspora", [True, False]) +@pytest.mark.parametrize("language", ["hebrew", "english"]) +async def test_import_no_options(hass: HomeAssistant, language, diaspora) -> None: + """Test that the import step works.""" + conf = { + DOMAIN: {CONF_NAME: "test", CONF_LANGUAGE: language, CONF_DIASPORA: diaspora} + } + + assert await async_setup_component(hass, DOMAIN, conf.copy()) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].data == conf[DOMAIN] | { + CONF_CANDLE_LIGHT_MINUTES: DEFAULT_CANDLE_LIGHT, + CONF_HAVDALAH_OFFSET_MINUTES: DEFAULT_HAVDALAH_OFFSET_MINUTES, + } + + +async def test_import_with_options(hass: HomeAssistant) -> None: + """Test that the import step works.""" + conf = { + DOMAIN: { + CONF_NAME: "test", + CONF_DIASPORA: DEFAULT_DIASPORA, + CONF_LANGUAGE: DEFAULT_LANGUAGE, + CONF_CANDLE_LIGHT_MINUTES: 20, + CONF_HAVDALAH_OFFSET_MINUTES: 50, + CONF_LATITUDE: 31.76, + CONF_LONGITUDE: 35.235, + } + } + + assert await async_setup_component(hass, DOMAIN, conf.copy()) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].data == conf[DOMAIN] + + +async def test_single_instance_allowed( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> 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_USER} + ) + + assert result.get("type") is FlowResultType.ABORT + assert result.get("reason") == "single_instance_allowed" + + +async def test_options(hass: HomeAssistant, mock_config_entry: MockConfigEntry) -> None: + """Test updating options.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_CANDLE_LIGHT_MINUTES: 25, + CONF_HAVDALAH_OFFSET_MINUTES: 34, + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"][CONF_CANDLE_LIGHT_MINUTES] == 25 + assert result["data"][CONF_HAVDALAH_OFFSET_MINUTES] == 34 diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 91883ce0d19..62d5de368d2 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -7,34 +7,28 @@ import pytest from homeassistant.components import jewish_calendar from homeassistant.components.binary_sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from . import ( - HDATE_DEFAULT_ALTITUDE, - alter_time, - make_jerusalem_test_params, - make_nyc_test_params, -) +from . import alter_time, make_jerusalem_test_params, make_nyc_test_params -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed async def test_jewish_calendar_min_config(hass: HomeAssistant) -> None: """Test minimum jewish calendar configuration.""" - assert await async_setup_component( - hass, jewish_calendar.DOMAIN, {"jewish_calendar": {}} - ) + entry = MockConfigEntry(domain=jewish_calendar.DOMAIN, data={}) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert hass.states.get("sensor.jewish_calendar_date") is not None async def test_jewish_calendar_hebrew(hass: HomeAssistant) -> None: """Test jewish calendar sensor with language set to hebrew.""" - assert await async_setup_component( - hass, jewish_calendar.DOMAIN, {"jewish_calendar": {"language": "hebrew"}} - ) + entry = MockConfigEntry(domain=jewish_calendar.DOMAIN, data={"language": "hebrew"}) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert hass.states.get("sensor.jewish_calendar_date") is not None @@ -172,17 +166,15 @@ async def test_jewish_calendar_sensor( hass.config.longitude = longitude with alter_time(test_time): - assert await async_setup_component( - hass, - jewish_calendar.DOMAIN, - { - "jewish_calendar": { - "name": "test", - "language": language, - "diaspora": diaspora, - } + entry = MockConfigEntry( + domain=jewish_calendar.DOMAIN, + data={ + "language": language, + "diaspora": diaspora, }, ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() future = dt_util.utcnow() + timedelta(seconds=30) @@ -195,7 +187,7 @@ async def test_jewish_calendar_sensor( else result ) - sensor_object = hass.states.get(f"sensor.test_{sensor}") + sensor_object = hass.states.get(f"sensor.jewish_calendar_{sensor}") assert sensor_object.state == result if sensor == "holiday": @@ -497,7 +489,6 @@ SHABBAT_TEST_IDS = [ ) async def test_shabbat_times_sensor( hass: HomeAssistant, - entity_registry: er.EntityRegistry, language, now, candle_lighting, @@ -517,19 +508,17 @@ async def test_shabbat_times_sensor( hass.config.longitude = longitude with alter_time(test_time): - assert await async_setup_component( - hass, - jewish_calendar.DOMAIN, - { - "jewish_calendar": { - "name": "test", - "language": language, - "diaspora": diaspora, - "candle_lighting_minutes_before_sunset": candle_lighting, - "havdalah_minutes_after_sunset": havdalah, - } + entry = MockConfigEntry( + domain=jewish_calendar.DOMAIN, + data={ + "language": language, + "diaspora": diaspora, + "candle_lighting_minutes_before_sunset": candle_lighting, + "havdalah_minutes_after_sunset": havdalah, }, ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() future = dt_util.utcnow() + timedelta(seconds=30) @@ -548,30 +537,10 @@ async def test_shabbat_times_sensor( else result_value ) - assert hass.states.get(f"sensor.test_{sensor_type}").state == str( + assert hass.states.get(f"sensor.jewish_calendar_{sensor_type}").state == str( result_value ), f"Value for {sensor_type}" - entity = entity_registry.async_get(f"sensor.test_{sensor_type}") - target_sensor_type = sensor_type.replace("parshat_hashavua", "weekly_portion") - target_uid = "_".join( - map( - str, - [ - latitude, - longitude, - tzname, - HDATE_DEFAULT_ALTITUDE, - diaspora, - language, - candle_lighting, - havdalah, - target_sensor_type, - ], - ) - ) - assert entity.unique_id == target_uid - OMER_PARAMS = [ (dt(2019, 4, 21, 0), "1"), @@ -597,16 +566,16 @@ async def test_omer_sensor(hass: HomeAssistant, test_time, result) -> None: test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone)) with alter_time(test_time): - assert await async_setup_component( - hass, jewish_calendar.DOMAIN, {"jewish_calendar": {"name": "test"}} - ) + entry = MockConfigEntry(domain=jewish_calendar.DOMAIN) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() future = dt_util.utcnow() + timedelta(seconds=30) async_fire_time_changed(hass, future) await hass.async_block_till_done() - assert hass.states.get("sensor.test_day_of_the_omer").state == result + assert hass.states.get("sensor.jewish_calendar_day_of_the_omer").state == result DAFYOMI_PARAMS = [ @@ -631,16 +600,16 @@ async def test_dafyomi_sensor(hass: HomeAssistant, test_time, result) -> None: test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone)) with alter_time(test_time): - assert await async_setup_component( - hass, jewish_calendar.DOMAIN, {"jewish_calendar": {"name": "test"}} - ) + entry = MockConfigEntry(domain=jewish_calendar.DOMAIN) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() future = dt_util.utcnow() + timedelta(seconds=30) async_fire_time_changed(hass, future) await hass.async_block_till_done() - assert hass.states.get("sensor.test_daf_yomi").state == result + assert hass.states.get("sensor.jewish_calendar_daf_yomi").state == result async def test_no_discovery_info(