Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.4
rev: v0.16.5
hooks:
- id: ruff-check
args:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/environment_canada/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env-canada==0.19.0"]
"requirements": ["env-canada==0.19.2"]
}
12 changes: 11 additions & 1 deletion homeassistant/components/hotspring/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
from collections.abc import Mapping
from typing import Any, override

from hotspring import HotSpring, HotSpringConnectionError, HotSpringError, Spa
from hotspring import (
HotSpring,
HotSpringConnectionError,
HotSpringError,
HotSpringSNADetectedError,
Spa,
)
import voluptuous as vol

from homeassistant.config_entries import (
Expand Down Expand Up @@ -52,6 +58,8 @@ async def async_step_user(
if user_input is not None:
try:
spa = await validate_input(self.hass, user_input)
except HotSpringSNADetectedError:
errors["base"] = "sna_device"
except HotSpringConnectionError, HotSpringError:
errors["base"] = "cannot_connect"
else:
Expand Down Expand Up @@ -101,6 +109,8 @@ async def async_step_zeroconf(
self.discovered_spa = await validate_input(
self.hass, {CONF_HOST: discovery_info.host}
)
except HotSpringSNADetectedError:
return self.async_abort(reason="sna_device")
except HotSpringConnectionError, HotSpringError:
return self.async_abort(reason="cannot_connect")

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/hotspring/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["hotspring"],
"quality_scale": "platinum",
"requirements": ["python-hotspring==2.0.1"],
"requirements": ["python-hotspring==2.1.0"],
"zeroconf": [
{
"name": "watkins_spa*",
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/hotspring/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"sna_device": "The discovered device is a Spa Network Adapter (SNA). Only the Home Network Adapter (HNA) can be configured.",
"unique_id_mismatch": "The MAC address does not match the configured device. Please ensure you reconfigure against the same device."
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"sna_device": "The entered address belongs to the Spa Network Adapter (SNA). Please enter the IP address or hostname of your Home Network Adapter (HNA) instead."
},
"step": {
"user": {
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/imou/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
}
},
"select": {
"collection_point": {
"default": "mdi:crosshairs-gps"
},
"device_volume": {
"default": "mdi:volume-high",
"state": {
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/imou/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import override

from pyimouapi.const import (
PARAM_COLLECTION_POINT,
PARAM_CURRENT_OPTION,
PARAM_DEVICE_VOLUME,
PARAM_NIGHT_VISION_MODE,
Expand Down Expand Up @@ -33,6 +34,10 @@
entity_category=EntityCategory.CONFIG,
translation_key=PARAM_NIGHT_VISION_MODE,
),
SelectEntityDescription(
key=PARAM_COLLECTION_POINT,
translation_key=PARAM_COLLECTION_POINT,
),
)


Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/imou/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@
}
},
"select": {
"collection_point": {
"name": "Go to collection point",
"state": {
"select_collection_point": "Select a collection point…"
}
},
"device_volume": {
"name": "Volume",
"state": {
Expand Down
112 changes: 46 additions & 66 deletions homeassistant/components/jewish_calendar/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

from hdate import HDateInfo, Zmanim
from hdate.parasha import Parasha
from hdate.translator import TranslatorMixin, set_language
from hdate.zmanim import Zman

from homeassistant.components.calendar import (
CalendarEntity,
Expand Down Expand Up @@ -51,35 +53,40 @@ class JewishCalendarCalendarEntityDescription(CalendarEntityDescription):
]


def _all_day_event(target_date: date, item: TranslatorMixin) -> CalendarEvent:
"""Create an all-day event from a translatable hdate object."""
return CalendarEvent(
start=target_date,
end=target_date,
summary=str(item),
description=item.description,
)


def _timed_event(zman: Zman) -> CalendarEvent:
"""Create an instantaneous event from a zman."""
return CalendarEvent(
start=zman.utc,
end=zman.utc,
summary=str(zman),
description=zman.description,
)


def _create_daily_event(
event_type: JewishCalendarEventType,
target_date: date,
info: HDateInfo,
zmanim: Zmanim,
) -> CalendarEvent | None:
"""Create a daily calendar event."""
# Hebrew date
if event_type == DailyCalendarEventType.DATE:
return CalendarEvent(
start=target_date,
end=target_date,
summary=str(info.hdate),
description=f"Hebrew date: {info.hdate}",
)
return _all_day_event(target_date, info.hdate)

# Time-based daily events using enum properties
daily_event = DailyCalendarEventType(event_type)
time_value = zmanim.zmanim.get(daily_event.value)

if time_value is not None:
return CalendarEvent(
start=time_value.utc,
end=time_value.utc,
summary=daily_event.summary,
description=f"{daily_event.description_prefix}: {time_value.local.strftime('%H:%M')}",
)

return None # Should never happen
if (zman := zmanim.zmanim.get(daily_event.value)) is None:
return None # Should never happen
return _timed_event(zman)


def _create_yearly_event(
Expand All @@ -89,56 +96,30 @@ def _create_yearly_event(
zmanim: Zmanim,
) -> list[CalendarEvent] | CalendarEvent | None:
"""Create a yearly calendar event."""
if event_type == YearlyCalendarEventType.HOLIDAY and info.holidays:
return [
CalendarEvent(
start=target_date,
end=target_date,
summary=str(holiday),
description=(
f"Jewish Holiday: {holiday}\nHoliday Type: {holiday.type}"
),
)
for holiday in info.holidays
]
if event_type == YearlyCalendarEventType.HOLIDAY:
return [_all_day_event(target_date, holiday) for holiday in info.holidays]

if event_type == YearlyCalendarEventType.WEEKLY_PORTION:
is_shabbat = target_date.weekday() == _SATURDAY
is_simchat_torah = any(
holiday.name == _SIMCHAT_TORAH for holiday in info.holidays
)
if (is_shabbat or is_simchat_torah) and info.parasha != str(Parasha.NONE):
return CalendarEvent(
start=target_date,
end=target_date,
summary=str(info.parasha),
description=f"Parshat Hashavua: {info.parasha}",
)
parasha = info.parasha_obj
if (is_shabbat or is_simchat_torah) and parasha is not Parasha.NONE:
return _all_day_event(target_date, parasha)
return None

if event_type == YearlyCalendarEventType.OMER_COUNT and info.omer.total_days > 0:
return CalendarEvent(
start=target_date,
end=target_date,
summary=str(info.omer),
description=f"Sefirat HaOmer: {info.omer.count_str()}",
)
if event_type == YearlyCalendarEventType.OMER_COUNT:
omer = info.omer
return _all_day_event(target_date, omer) if omer.total_days > 0 else None

if event_type == YearlyCalendarEventType.CANDLE_LIGHTING and zmanim.candle_lighting:
return CalendarEvent(
start=zmanim.candle_lighting.astimezone(UTC),
end=zmanim.candle_lighting.astimezone(UTC),
summary="Candle Lighting",
description=f"Candle lighting time: {zmanim.candle_lighting.strftime('%H:%M')}",
)
if event_type == YearlyCalendarEventType.CANDLE_LIGHTING:
zman = zmanim.candle_lighting_obj
return _timed_event(zman) if zman is not None else None

if event_type == YearlyCalendarEventType.HAVDALAH and zmanim.havdalah:
return CalendarEvent(
start=zmanim.havdalah.astimezone(UTC),
end=zmanim.havdalah.astimezone(UTC),
summary="Havdalah",
description=f"Havdalah time: {zmanim.havdalah.strftime('%H:%M')}",
)
if event_type == YearlyCalendarEventType.HAVDALAH:
zman = zmanim.havdalah_obj
return _timed_event(zman) if zman is not None else None

return None

Expand All @@ -150,13 +131,8 @@ def _create_learning_event(
zmanim: Zmanim,
) -> CalendarEvent | None:
"""Create a learning schedule event."""
if event_type == LearningScheduleEventType.DAF_YOMI and info.daf_yomi:
return CalendarEvent(
start=target_date,
end=target_date,
summary=str(info.daf_yomi),
description=f"Daf Yomi: {info.daf_yomi}",
)
if event_type == LearningScheduleEventType.DAF_YOMI:
return _all_day_event(target_date, info.daf_yomi_obj)

return None

Expand Down Expand Up @@ -277,6 +253,10 @@ def _date_to_dt(self, val: date | datetime, _time: time) -> datetime:

def _get_events_for_date(self, target_date: date) -> list[CalendarEvent]:
"""Get all configured events for a specific date."""
# hdate holds its display language in a ContextVar that the coordinator sets
# in its own task, so it has to be re-applied in the task serving this request.
set_language(self.coordinator.data.language)

events = []

info = HDateInfo(target_date, self.coordinator.data.diaspora)
Expand Down
71 changes: 13 additions & 58 deletions homeassistant/components/jewish_calendar/const.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Jewish Calendar constants."""

from enum import StrEnum
from typing import TYPE_CHECKING, Self

DOMAIN = "jewish_calendar"

Expand All @@ -24,65 +23,21 @@


class DailyCalendarEventType(StrEnum):
"""Daily Calendar event types with metadata."""
"""Daily Calendar event types."""

DATE = "date"
ALOT_HASHACHAR = (
"alot_hashachar",
"Alot Hashachar", # codespell:ignore alot
"Halachic dawn",
)
NETZ_HACHAMA = ("netz_hachama", "Netz Hachama", "Halachic sunrise")
SOF_ZMAN_SHEMA_GRA = (
"sof_zman_shema_gra",
'Sof Zman Shema (Gr"A)', # codespell:ignore shema
"Latest time for Shema", # codespell:ignore shema
)
SOF_ZMAN_SHEMA_MGA = (
"sof_zman_shema_mga",
'Sof Zman Shema (Mg"A)', # codespell:ignore shema
"Latest time for Shema", # codespell:ignore shema
)
SOF_ZMAN_TFILLA_GRA = (
"sof_zman_tfilla_gra",
'Sof Zman Tefilla (Gr"A)',
"Latest time for Tefilla",
)
SOF_ZMAN_TFILLA_MGA = (
"sof_zman_tfilla_mga",
'Sof Zman Tefilla (Mg"A)',
"Latest time for Tefilla",
)
CHATZOT_HAYOM = ("chatzot_hayom", "Chatzot Hayom", "Halachic midday")
MINCHA_GEDOLA = ("mincha_gedola", "Mincha Gedola", "Earliest time for Mincha")
MINCHA_KETANA = ("mincha_ketana", "Mincha Ketana", "Preferable time for Mincha")
PLAG_HAMINCHA = ("plag_hamincha", "Plag Hamincha", "Plag Hamincha")
SHKIA = ("shkia", "Shkia", "Sunset")
TSET_HAKOHAVIM = ("tset_hakohavim_tsom", "T'set Hakochavim", "Nightfall")

if TYPE_CHECKING:
_summary: str
_description_prefix: str

def __new__(
cls, value: str, summary: str = "", description_prefix: str = ""
) -> Self:
"""Create new enum member with additional attributes."""
obj = str.__new__(cls, value)
obj._value_ = value
obj._summary = summary # noqa: SLF001
obj._description_prefix = description_prefix # noqa: SLF001
return obj

@property
def summary(self) -> str:
"""Return the summary for the event."""
return self._summary

@property
def description_prefix(self) -> str:
"""Return the description prefix for the event."""
return self._description_prefix
ALOT_HASHACHAR = "alot_hashachar"
NETZ_HACHAMA = "netz_hachama"
SOF_ZMAN_SHEMA_GRA = "sof_zman_shema_gra"
SOF_ZMAN_SHEMA_MGA = "sof_zman_shema_mga"
SOF_ZMAN_TFILLA_GRA = "sof_zman_tfilla_gra"
SOF_ZMAN_TFILLA_MGA = "sof_zman_tfilla_mga"
CHATZOT_HAYOM = "chatzot_hayom"
MINCHA_GEDOLA = "mincha_gedola"
MINCHA_KETANA = "mincha_ketana"
PLAG_HAMINCHA = "plag_hamincha"
SHKIA = "shkia"
TSET_HAKOHAVIM = "tset_hakohavim_tsom"


class YearlyCalendarEventType(StrEnum):
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/jewish_calendar/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"common": {
"descr_diaspora": "Is the location outside of Israel?",
"descr_elevation": "Elevation in meters above sea level. This is used to calculate the times correctly.",
"descr_language": "Language to use when displaying values in the UI. This does not affect the Hebrew date.",
"descr_language": "Language to use when displaying values in the UI.",
"descr_location": "Location to use for the Jewish calendar calculations. By default, the location is set to the Home Assistant location.",
"descr_time_zone": "If you specify a location, make sure to specify the time zone for correct calendar times calculations",
"diaspora": "Outside of Israel?",
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/knx/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"requirements": [
"xknx==3.20.0",
"xknxproject==3.10.0",
"knx-frontend==2026.8.28.62336",
"knx-frontend==2026.9.4.63549",
"knx-telegram-store[sqlite,postgres]==0.11.2"
],
"single_config_entry": true
Expand Down
Loading
Loading