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 <erik@montnemery.com>

* 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 <ttmost@gmail.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Yuval Aboulafia 2024-05-24 15:04:17 +03:00 committed by GitHub
parent 23597a8cdf
commit 2c09f72c33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 544 additions and 182 deletions

View file

@ -5,41 +5,54 @@ from __future__ import annotations
from hdate import Location from hdate import Location
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, Platform from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.core import HomeAssistant 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 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 from homeassistant.helpers.typing import ConfigType
DOMAIN = "jewish_calendar" DOMAIN = "jewish_calendar"
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
CONF_DIASPORA = "diaspora" CONF_DIASPORA = "diaspora"
CONF_LANGUAGE = "language"
CONF_CANDLE_LIGHT_MINUTES = "candle_lighting_minutes_before_sunset" CONF_CANDLE_LIGHT_MINUTES = "candle_lighting_minutes_before_sunset"
CONF_HAVDALAH_OFFSET_MINUTES = "havdalah_minutes_after_sunset" CONF_HAVDALAH_OFFSET_MINUTES = "havdalah_minutes_after_sunset"
CANDLE_LIGHT_DEFAULT = 18
DEFAULT_NAME = "Jewish Calendar" 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( 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_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_LATITUDE, "coordinates"): cv.latitude,
vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, 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"] ["hebrew", "english"]
), ),
vol.Optional( vol.Optional(
CONF_CANDLE_LIGHT_MINUTES, default=CANDLE_LIGHT_DEFAULT CONF_CANDLE_LIGHT_MINUTES, default=DEFAULT_CANDLE_LIGHT
): int, ): int,
# Default of 0 means use 8.5 degrees / 'three_stars' time. # 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, extra=vol.ALLOW_EXTRA,
@ -72,37 +85,72 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if DOMAIN not in config: if DOMAIN not in config:
return True return True
name = config[DOMAIN][CONF_NAME] async_create_issue(
language = config[DOMAIN][CONF_LANGUAGE] 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) hass.async_create_task(
longitude = config[DOMAIN].get(CONF_LONGITUDE, hass.config.longitude) hass.config_entries.flow.async_init(
diaspora = config[DOMAIN][CONF_DIASPORA] DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN]
)
)
candle_lighting_offset = config[DOMAIN][CONF_CANDLE_LIGHT_MINUTES] return True
havdalah_offset = config[DOMAIN][CONF_HAVDALAH_OFFSET_MINUTES]
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( location = Location(
latitude=latitude, name=hass.config.location_name,
longitude=longitude,
timezone=hass.config.time_zone,
diaspora=diaspora, 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( prefix = get_unique_prefix(
location, language, candle_lighting_offset, havdalah_offset location, language, candle_lighting_offset, havdalah_offset
) )
hass.data[DOMAIN] = { hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = {
"location": location,
"name": name,
"language": language, "language": language,
"diaspora": diaspora,
"location": location,
"candle_lighting_offset": candle_lighting_offset, "candle_lighting_offset": candle_lighting_offset,
"havdalah_offset": havdalah_offset, "havdalah_offset": havdalah_offset,
"diaspora": diaspora,
"prefix": prefix, "prefix": prefix,
} }
for platform in PLATFORMS: await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
hass.async_create_task(async_load_platform(hass, platform, DOMAIN, {}, config))
return True 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

View file

@ -6,6 +6,7 @@ from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
import datetime as dt import datetime as dt
from datetime import datetime from datetime import datetime
from typing import Any
import hdate import hdate
from hdate.zmanim import Zmanim from hdate.zmanim import Zmanim
@ -14,13 +15,14 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity, BinarySensorEntity,
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers import event from homeassistant.helpers import event
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from . import DOMAIN from . import DEFAULT_NAME, DOMAIN
@dataclass(frozen=True) @dataclass(frozen=True)
@ -63,15 +65,25 @@ async def async_setup_platform(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Jewish Calendar binary sensor devices.""" """Set up the Jewish calendar binary sensors from YAML.
if discovery_info is None:
return
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( async_add_entities(
[ JewishCalendarBinarySensor(
JewishCalendarBinarySensor(hass.data[DOMAIN], description) hass.data[DOMAIN][config_entry.entry_id], description
for description in BINARY_SENSORS )
] for description in BINARY_SENSORS
) )
@ -83,13 +95,13 @@ class JewishCalendarBinarySensor(BinarySensorEntity):
def __init__( def __init__(
self, self,
data: dict[str, str | bool | int | float], data: dict[str, Any],
description: JewishCalendarBinarySensorEntityDescription, description: JewishCalendarBinarySensorEntityDescription,
) -> None: ) -> None:
"""Initialize the binary sensor.""" """Initialize the binary sensor."""
self.entity_description = description self.entity_description = description
self._attr_name = f"{data['name']} {description.name}" self._attr_name = f"{DEFAULT_NAME} {description.name}"
self._attr_unique_id = f"{data['prefix']}_{description.key}" self._attr_unique_id = f'{data["prefix"]}_{description.key}'
self._location = data["location"] self._location = data["location"]
self._hebrew = data["language"] == "hebrew" self._hebrew = data["language"] == "hebrew"
self._candle_lighting_offset = data["candle_lighting_offset"] self._candle_lighting_offset = data["candle_lighting_offset"]

View file

@ -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
),
)

View file

@ -2,8 +2,10 @@
"domain": "jewish_calendar", "domain": "jewish_calendar",
"name": "Jewish Calendar", "name": "Jewish Calendar",
"codeowners": ["@tsvi"], "codeowners": ["@tsvi"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/jewish_calendar", "documentation": "https://www.home-assistant.io/integrations/jewish_calendar",
"iot_class": "calculated", "iot_class": "calculated",
"loggers": ["hdate"], "loggers": ["hdate"],
"requirements": ["hdate==0.10.8"] "requirements": ["hdate==0.10.8"],
"single_config_entry": true
} }

View file

@ -1,4 +1,4 @@
"""Platform to retrieve Jewish calendar information for Home Assistant.""" """Support for Jewish calendar sensors."""
from __future__ import annotations from __future__ import annotations
@ -14,6 +14,7 @@ from homeassistant.components.sensor import (
SensorEntity, SensorEntity,
SensorEntityDescription, SensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import SUN_EVENT_SUNSET from homeassistant.const import SUN_EVENT_SUNSET
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback 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 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from . import DOMAIN from . import DEFAULT_NAME, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
INFO_SENSORS = ( INFO_SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription( SensorEntityDescription(
key="date", key="date",
name="Date", name="Date",
@ -53,7 +54,7 @@ INFO_SENSORS = (
), ),
) )
TIME_SENSORS = ( TIME_SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription( SensorEntityDescription(
key="first_light", key="first_light",
name="Alot Hashachar", # codespell:ignore alot name="Alot Hashachar", # codespell:ignore alot
@ -148,17 +149,24 @@ async def async_setup_platform(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Jewish calendar sensor platform.""" """Set up the Jewish calendar sensors from YAML.
if discovery_info is None:
return
sensors = [ The YAML platform config is automatically
JewishCalendarSensor(hass.data[DOMAIN], description) imported to a config entry, this method can be removed
for description in INFO_SENSORS 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( sensors.extend(
JewishCalendarTimeSensor(hass.data[DOMAIN], description) JewishCalendarTimeSensor(entry, description) for description in TIME_SENSORS
for description in TIME_SENSORS
) )
async_add_entities(sensors) async_add_entities(sensors)
@ -169,13 +177,13 @@ class JewishCalendarSensor(SensorEntity):
def __init__( def __init__(
self, self,
data: dict[str, str | bool | int | float], data: dict[str, Any],
description: SensorEntityDescription, description: SensorEntityDescription,
) -> None: ) -> None:
"""Initialize the Jewish calendar sensor.""" """Initialize the Jewish calendar sensor."""
self.entity_description = description self.entity_description = description
self._attr_name = f"{data['name']} {description.name}" self._attr_name = f"{DEFAULT_NAME} {description.name}"
self._attr_unique_id = f"{data['prefix']}_{description.key}" self._attr_unique_id = f'{data["prefix"]}_{description.key}'
self._location = data["location"] self._location = data["location"]
self._hebrew = data["language"] == "hebrew" self._hebrew = data["language"] == "hebrew"
self._candle_lighting_offset = data["candle_lighting_offset"] self._candle_lighting_offset = data["candle_lighting_offset"]

View file

@ -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"
}
}
}
}
}

View file

@ -269,6 +269,7 @@ FLOWS = {
"isy994", "isy994",
"izone", "izone",
"jellyfin", "jellyfin",
"jewish_calendar",
"juicenet", "juicenet",
"justnimbus", "justnimbus",
"jvc_projector", "jvc_projector",

View file

@ -2950,8 +2950,9 @@
"jewish_calendar": { "jewish_calendar": {
"name": "Jewish Calendar", "name": "Jewish Calendar",
"integration_type": "hub", "integration_type": "hub",
"config_flow": false, "config_flow": true,
"iot_class": "calculated" "iot_class": "calculated",
"single_config_entry": true
}, },
"joaoapps_join": { "joaoapps_join": {
"name": "Joaoapps Join", "name": "Joaoapps Join",

View file

@ -27,7 +27,7 @@ def make_nyc_test_params(dtime, results, havdalah_offset=0):
} }
return ( return (
dtime, dtime,
jewish_calendar.CANDLE_LIGHT_DEFAULT, jewish_calendar.DEFAULT_CANDLE_LIGHT,
havdalah_offset, havdalah_offset,
True, True,
"America/New_York", "America/New_York",
@ -49,7 +49,7 @@ def make_jerusalem_test_params(dtime, results, havdalah_offset=0):
} }
return ( return (
dtime, dtime,
jewish_calendar.CANDLE_LIGHT_DEFAULT, jewish_calendar.DEFAULT_CANDLE_LIGHT,
havdalah_offset, havdalah_offset,
False, False,
"Asia/Jerusalem", "Asia/Jerusalem",

View file

@ -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

View file

@ -1,6 +1,7 @@
"""The tests for the Jewish calendar binary sensors.""" """The tests for the Jewish calendar binary sensors."""
from datetime import datetime as dt, timedelta from datetime import datetime as dt, timedelta
import logging
import pytest 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.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from . import ( from . import alter_time, make_jerusalem_test_params, make_nyc_test_params
HDATE_DEFAULT_ALTITUDE,
alter_time, from tests.common import MockConfigEntry, async_fire_time_changed
make_jerusalem_test_params,
make_nyc_test_params, _LOGGER = logging.getLogger(__name__)
)
from tests.common import async_fire_time_changed
MELACHA_PARAMS = [ MELACHA_PARAMS = [
make_nyc_test_params( make_nyc_test_params(
@ -170,7 +168,6 @@ MELACHA_TEST_IDS = [
) )
async def test_issur_melacha_sensor( async def test_issur_melacha_sensor(
hass: HomeAssistant, hass: HomeAssistant,
entity_registry: er.EntityRegistry,
now, now,
candle_lighting, candle_lighting,
havdalah, havdalah,
@ -189,49 +186,33 @@ async def test_issur_melacha_sensor(
hass.config.longitude = longitude hass.config.longitude = longitude
with alter_time(test_time): with alter_time(test_time):
assert await async_setup_component( entry = MockConfigEntry(
hass, domain=jewish_calendar.DOMAIN,
jewish_calendar.DOMAIN, data={
{ "language": "english",
"jewish_calendar": { "diaspora": diaspora,
"name": "test", "candle_lighting_minutes_before_sunset": candle_lighting,
"language": "english", "havdalah_minutes_after_sunset": havdalah,
"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() await hass.async_block_till_done()
assert ( 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"] == 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"]): with alter_time(result["update"]):
async_fire_time_changed(hass, result["update"]) async_fire_time_changed(hass, result["update"])
await hass.async_block_till_done() await hass.async_block_till_done()
assert ( 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"] == result["new_state"]
) )
@ -277,22 +258,22 @@ async def test_issur_melacha_sensor_update(
hass.config.longitude = longitude hass.config.longitude = longitude
with alter_time(test_time): with alter_time(test_time):
assert await async_setup_component( entry = MockConfigEntry(
hass, domain=jewish_calendar.DOMAIN,
jewish_calendar.DOMAIN, data={
{ "language": "english",
"jewish_calendar": { "diaspora": diaspora,
"name": "test", "candle_lighting_minutes_before_sunset": candle_lighting,
"language": "english", "havdalah_minutes_after_sunset": havdalah,
"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() await hass.async_block_till_done()
assert ( 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] == result[0]
) )
@ -301,7 +282,9 @@ async def test_issur_melacha_sensor_update(
async_fire_time_changed(hass, test_time) async_fire_time_changed(hass, test_time)
await hass.async_block_till_done() await hass.async_block_till_done()
assert ( 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] == result[1]
) )

View file

@ -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

View file

@ -7,34 +7,28 @@ import pytest
from homeassistant.components import jewish_calendar from homeassistant.components import jewish_calendar
from homeassistant.components.binary_sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.binary_sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from . import ( from . import alter_time, make_jerusalem_test_params, make_nyc_test_params
HDATE_DEFAULT_ALTITUDE,
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: async def test_jewish_calendar_min_config(hass: HomeAssistant) -> None:
"""Test minimum jewish calendar configuration.""" """Test minimum jewish calendar configuration."""
assert await async_setup_component( entry = MockConfigEntry(domain=jewish_calendar.DOMAIN, data={})
hass, jewish_calendar.DOMAIN, {"jewish_calendar": {}} entry.add_to_hass(hass)
) await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("sensor.jewish_calendar_date") is not None assert hass.states.get("sensor.jewish_calendar_date") is not None
async def test_jewish_calendar_hebrew(hass: HomeAssistant) -> None: async def test_jewish_calendar_hebrew(hass: HomeAssistant) -> None:
"""Test jewish calendar sensor with language set to hebrew.""" """Test jewish calendar sensor with language set to hebrew."""
assert await async_setup_component( entry = MockConfigEntry(domain=jewish_calendar.DOMAIN, data={"language": "hebrew"})
hass, jewish_calendar.DOMAIN, {"jewish_calendar": {"language": "hebrew"}} entry.add_to_hass(hass)
) await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("sensor.jewish_calendar_date") is not None 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 hass.config.longitude = longitude
with alter_time(test_time): with alter_time(test_time):
assert await async_setup_component( entry = MockConfigEntry(
hass, domain=jewish_calendar.DOMAIN,
jewish_calendar.DOMAIN, data={
{ "language": language,
"jewish_calendar": { "diaspora": diaspora,
"name": "test",
"language": language,
"diaspora": diaspora,
}
}, },
) )
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
future = dt_util.utcnow() + timedelta(seconds=30) future = dt_util.utcnow() + timedelta(seconds=30)
@ -195,7 +187,7 @@ async def test_jewish_calendar_sensor(
else result 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 assert sensor_object.state == result
if sensor == "holiday": if sensor == "holiday":
@ -497,7 +489,6 @@ SHABBAT_TEST_IDS = [
) )
async def test_shabbat_times_sensor( async def test_shabbat_times_sensor(
hass: HomeAssistant, hass: HomeAssistant,
entity_registry: er.EntityRegistry,
language, language,
now, now,
candle_lighting, candle_lighting,
@ -517,19 +508,17 @@ async def test_shabbat_times_sensor(
hass.config.longitude = longitude hass.config.longitude = longitude
with alter_time(test_time): with alter_time(test_time):
assert await async_setup_component( entry = MockConfigEntry(
hass, domain=jewish_calendar.DOMAIN,
jewish_calendar.DOMAIN, data={
{ "language": language,
"jewish_calendar": { "diaspora": diaspora,
"name": "test", "candle_lighting_minutes_before_sunset": candle_lighting,
"language": language, "havdalah_minutes_after_sunset": havdalah,
"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() await hass.async_block_till_done()
future = dt_util.utcnow() + timedelta(seconds=30) future = dt_util.utcnow() + timedelta(seconds=30)
@ -548,30 +537,10 @@ async def test_shabbat_times_sensor(
else result_value 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 result_value
), f"Value for {sensor_type}" ), 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 = [ OMER_PARAMS = [
(dt(2019, 4, 21, 0), "1"), (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)) test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone))
with alter_time(test_time): with alter_time(test_time):
assert await async_setup_component( entry = MockConfigEntry(domain=jewish_calendar.DOMAIN)
hass, jewish_calendar.DOMAIN, {"jewish_calendar": {"name": "test"}} entry.add_to_hass(hass)
) await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
future = dt_util.utcnow() + timedelta(seconds=30) future = dt_util.utcnow() + timedelta(seconds=30)
async_fire_time_changed(hass, future) async_fire_time_changed(hass, future)
await hass.async_block_till_done() 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 = [ 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)) test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone))
with alter_time(test_time): with alter_time(test_time):
assert await async_setup_component( entry = MockConfigEntry(domain=jewish_calendar.DOMAIN)
hass, jewish_calendar.DOMAIN, {"jewish_calendar": {"name": "test"}} entry.add_to_hass(hass)
) await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
future = dt_util.utcnow() + timedelta(seconds=30) future = dt_util.utcnow() + timedelta(seconds=30)
async_fire_time_changed(hass, future) async_fire_time_changed(hass, future)
await hass.async_block_till_done() 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( async def test_no_discovery_info(