Skip to content

Commit

Permalink
Add Holiday integration (#103795)
Browse files Browse the repository at this point in the history
* Add Holiday integration

* Localize holiday names

* Changes based on review feedback

* Add tests

* Add device info

* Bump holidays to 0.36

* Default to Home Assistant country setting

* Update homeassistant/components/holiday/calendar.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Update homeassistant/components/holiday/calendar.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Update homeassistant/components/holiday/config_flow.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* black

* Move time

* Stop creating duplicate holiday calendars

* Set default language using python-holiday

* Use common translation

* Set _attr_name to None to fix friendly name

* Fix location

* Update homeassistant/components/holiday/__init__.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Update homeassistant/components/holiday/calendar.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Update homeassistant/components/holiday/calendar.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Update tests/components/holiday/test_init.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* cleanup

* Set up the integration and test the state

* Test that configuring more than one instance is rejected

* Set default_language to user's language, fallback to country's default language

* Improve tests

* Update homeassistant/components/holiday/calendar.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Cleanup

* Add next year so we don't run out

* Update tests/components/holiday/test_init.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Cleanup

* Set default language in `__init__`

* Add strict typing

* Change default language: HA's language `en` is `en_US` in holidays, apart from Canada

* CONF_PROVINCE can be None

* Fix test

* Fix default_language

* Refactor tests

* Province can be None

* Add test for translated title

* Address feedback

* Address feedback

* Change test to use service call

* Address feedback

* Apply suggestions from code review

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Changes based on review feedback

* Update homeassistant/components/holiday/calendar.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Update homeassistant/components/holiday/calendar.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Add a test if next event is missing

* Rebase

* Set device to service

* Remove not needed translation key

---------

Co-authored-by: G Johansson <goran.johansson@shiftit.se>
  • Loading branch information
jrieger and gjohansson-ST authored Dec 3, 2023
1 parent 67784de commit 244edb4
Show file tree
Hide file tree
Showing 18 changed files with 716 additions and 0 deletions.
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ homeassistant.components.hardkernel.*
homeassistant.components.hardware.*
homeassistant.components.here_travel_time.*
homeassistant.components.history.*
homeassistant.components.holiday.*
homeassistant.components.homeassistant.exposed_entities
homeassistant.components.homeassistant.triggers.event
homeassistant.components.homeassistant_alerts.*
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,8 @@ build.json @home-assistant/supervisor
/tests/components/hive/ @Rendili @KJonline
/homeassistant/components/hlk_sw16/ @jameshilliard
/tests/components/hlk_sw16/ @jameshilliard
/homeassistant/components/holiday/ @jrieger
/tests/components/holiday/ @jrieger
/homeassistant/components/home_connect/ @DavidMStraub
/tests/components/home_connect/ @DavidMStraub
/homeassistant/components/home_plus_control/ @chemaaa
Expand Down
20 changes: 20 additions & 0 deletions homeassistant/components/holiday/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""The Holiday integration."""
from __future__ import annotations

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant

PLATFORMS: list[Platform] = [Platform.CALENDAR]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Holiday from a config entry."""
await hass.config_entries.async_forward_entry_setups(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)
134 changes: 134 additions & 0 deletions homeassistant/components/holiday/calendar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""Holiday Calendar."""
from __future__ import annotations

from datetime import datetime

from holidays import HolidayBase, country_holidays

from homeassistant.components.calendar import CalendarEntity, CalendarEvent
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_COUNTRY
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util

from .const import CONF_PROVINCE, DOMAIN


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Holiday Calendar config entry."""
country: str = config_entry.data[CONF_COUNTRY]
province: str | None = config_entry.data.get(CONF_PROVINCE)
language = hass.config.language

obj_holidays = country_holidays(
country,
subdiv=province,
years={dt_util.now().year, dt_util.now().year + 1},
language=language,
)
if language == "en":
for lang in obj_holidays.supported_languages:
if lang.startswith("en"):
obj_holidays = country_holidays(
country,
subdiv=province,
years={dt_util.now().year, dt_util.now().year + 1},
language=lang,
)
language = lang
break

async_add_entities(
[
HolidayCalendarEntity(
config_entry.title,
country,
province,
language,
obj_holidays,
config_entry.entry_id,
)
],
True,
)


class HolidayCalendarEntity(CalendarEntity):
"""Representation of a Holiday Calendar element."""

_attr_has_entity_name = True
_attr_name = None

def __init__(
self,
name: str,
country: str,
province: str | None,
language: str,
obj_holidays: HolidayBase,
unique_id: str,
) -> None:
"""Initialize HolidayCalendarEntity."""
self._country = country
self._province = province
self._location = name
self._language = language
self._attr_unique_id = unique_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
entry_type=DeviceEntryType.SERVICE,
name=name,
)
self._obj_holidays = obj_holidays

@property
def event(self) -> CalendarEvent | None:
"""Return the next upcoming event."""
next_holiday = None
for holiday_date, holiday_name in sorted(
self._obj_holidays.items(), key=lambda x: x[0]
):
if holiday_date >= dt_util.now().date():
next_holiday = (holiday_date, holiday_name)
break

if next_holiday is None:
return None

return CalendarEvent(
summary=next_holiday[1],
start=next_holiday[0],
end=next_holiday[0],
location=self._location,
)

async def async_get_events(
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
) -> list[CalendarEvent]:
"""Get all events in a specific time frame."""
obj_holidays = country_holidays(
self._country,
subdiv=self._province,
years=list({start_date.year, end_date.year}),
language=self._language,
)

event_list: list[CalendarEvent] = []

for holiday_date, holiday_name in obj_holidays.items():
if start_date.date() <= holiday_date <= end_date.date():
event = CalendarEvent(
summary=holiday_name,
start=holiday_date,
end=holiday_date,
location=self._location,
)
event_list.append(event)

return event_list
99 changes: 99 additions & 0 deletions homeassistant/components/holiday/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Config flow for Holiday integration."""
from __future__ import annotations

from typing import Any

from babel import Locale
from holidays import list_supported_countries
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_COUNTRY
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.selector import (
CountrySelector,
CountrySelectorConfig,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)

from .const import CONF_PROVINCE, DOMAIN

SUPPORTED_COUNTRIES = list_supported_countries(include_aliases=False)


class HolidayConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Holiday."""

VERSION = 1

data: dict[str, Any] = {}

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is not None:
self.data = user_input

selected_country = self.data[CONF_COUNTRY]

if SUPPORTED_COUNTRIES[selected_country]:
return await self.async_step_province()

self._async_abort_entries_match({CONF_COUNTRY: user_input[CONF_COUNTRY]})

locale = Locale(self.hass.config.language)
title = locale.territories[selected_country]
return self.async_create_entry(title=title, data=self.data)

user_schema = vol.Schema(
{
vol.Optional(
CONF_COUNTRY, default=self.hass.config.country
): CountrySelector(
CountrySelectorConfig(
countries=list(SUPPORTED_COUNTRIES),
)
),
}
)

return self.async_show_form(step_id="user", data_schema=user_schema)

async def async_step_province(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the province step."""
if user_input is not None:
combined_input: dict[str, Any] = {**self.data, **user_input}

country = combined_input[CONF_COUNTRY]
province = combined_input.get(CONF_PROVINCE)

self._async_abort_entries_match(
{
CONF_COUNTRY: country,
CONF_PROVINCE: province,
}
)

locale = Locale(self.hass.config.language)
province_str = f", {province}" if province else ""
name = f"{locale.territories[country]}{province_str}"

return self.async_create_entry(title=name, data=combined_input)

province_schema = vol.Schema(
{
vol.Optional(CONF_PROVINCE): SelectSelector(
SelectSelectorConfig(
options=SUPPORTED_COUNTRIES[self.data[CONF_COUNTRY]],
mode=SelectSelectorMode.DROPDOWN,
)
),
}
)

return self.async_show_form(step_id="province", data_schema=province_schema)
6 changes: 6 additions & 0 deletions homeassistant/components/holiday/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Constants for the Holiday integration."""
from typing import Final

DOMAIN: Final = "holiday"

CONF_PROVINCE: Final = "province"
9 changes: 9 additions & 0 deletions homeassistant/components/holiday/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"domain": "holiday",
"name": "Holiday",
"codeowners": ["@jrieger"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.37", "babel==2.13.1"]
}
19 changes: 19 additions & 0 deletions homeassistant/components/holiday/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "Already configured. Only a single configuration for country/province combination possible."
},
"step": {
"user": {
"data": {
"country": "Country"
}
},
"province": {
"data": {
"province": "Province"
}
}
}
}
}
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@
"hisense_aehw4a1",
"hive",
"hlk_sw16",
"holiday",
"home_connect",
"home_plus_control",
"homeassistant_sky_connect",
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/generated/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -2408,6 +2408,12 @@
"config_flow": true,
"iot_class": "local_push"
},
"holiday": {
"name": "Holiday",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
"home_connect": {
"name": "Home Connect",
"integration_type": "hub",
Expand Down
10 changes: 10 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,16 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true

[mypy-homeassistant.components.holiday.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true

[mypy-homeassistant.components.homeassistant.exposed_entities]
check_untyped_defs = true
disallow_incomplete_defs = true
Expand Down
4 changes: 4 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,9 @@ azure-eventhub==5.11.1
# homeassistant.components.azure_service_bus
azure-servicebus==7.10.0

# homeassistant.components.holiday
babel==2.13.1

# homeassistant.components.baidu
baidu-aip==1.6.6

Expand Down Expand Up @@ -1013,6 +1016,7 @@ hlk-sw16==0.0.9
# homeassistant.components.pi_hole
hole==0.8.0

# homeassistant.components.holiday
# homeassistant.components.workday
holidays==0.37

Expand Down
4 changes: 4 additions & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,9 @@ axis==48
# homeassistant.components.azure_event_hub
azure-eventhub==5.11.1

# homeassistant.components.holiday
babel==2.13.1

# homeassistant.components.homekit
base36==0.1.1

Expand Down Expand Up @@ -800,6 +803,7 @@ hlk-sw16==0.0.9
# homeassistant.components.pi_hole
hole==0.8.0

# homeassistant.components.holiday
# homeassistant.components.workday
holidays==0.37

Expand Down
1 change: 1 addition & 0 deletions tests/components/holiday/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for the Holiday integration."""
Loading

0 comments on commit 244edb4

Please sign in to comment.