This commit is contained in:
Franck Nijhof 2024-12-06 20:21:31 +01:00 committed by GitHub
commit cf53a9743f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 204 additions and 115 deletions

View File

@ -8,6 +8,6 @@
"integration_type": "system", "integration_type": "system",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["hass_nabucasa"], "loggers": ["hass_nabucasa"],
"requirements": ["hass-nabucasa==0.85.0"], "requirements": ["hass-nabucasa==0.86.0"],
"single_config_entry": true "single_config_entry": true
} }

View File

@ -4,8 +4,7 @@ from __future__ import annotations
import logging import logging
from pydeako.deako import Deako, DeviceListTimeout, FindDevicesTimeout from pydeako import Deako, DeakoDiscoverer, FindDevicesError
from pydeako.discover import DeakoDiscoverer
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -30,12 +29,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: DeakoConfigEntry) -> boo
await connection.connect() await connection.connect()
try: try:
await connection.find_devices() await connection.find_devices()
except DeviceListTimeout as exc: # device list never received except FindDevicesError as exc:
_LOGGER.warning("Device not responding to device list") _LOGGER.warning("Error finding devices: %s", exc)
await connection.disconnect()
raise ConfigEntryNotReady(exc) from exc
except FindDevicesTimeout as exc: # total devices expected not received
_LOGGER.warning("Device not responding to device requests")
await connection.disconnect() await connection.disconnect()
raise ConfigEntryNotReady(exc) from exc raise ConfigEntryNotReady(exc) from exc

View File

@ -1,6 +1,6 @@
"""Config flow for deako.""" """Config flow for deako."""
from pydeako.discover import DeakoDiscoverer, DevicesNotFoundException from pydeako import DeakoDiscoverer, DevicesNotFoundException
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant

View File

@ -2,7 +2,7 @@
from typing import Any from typing import Any
from pydeako.deako import Deako from pydeako import Deako
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant

View File

@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/deako", "documentation": "https://www.home-assistant.io/integrations/deako",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pydeako"], "loggers": ["pydeako"],
"requirements": ["pydeako==0.5.4"], "requirements": ["pydeako==0.6.0"],
"single_config_entry": true, "single_config_entry": true,
"zeroconf": ["_deako._tcp.local."] "zeroconf": ["_deako._tcp.local."]
} }

View File

@ -99,8 +99,8 @@ class EcovacsController:
for device_config in devices.not_supported: for device_config in devices.not_supported:
_LOGGER.warning( _LOGGER.warning(
( (
'Device "%s" not supported. Please add support for it to ' 'Device "%s" not supported. More information at '
"https://github.com/DeebotUniverse/client.py: %s" "https://github.com/DeebotUniverse/client.py/issues/612: %s"
), ),
device_config["deviceName"], device_config["deviceName"],
device_config, device_config,

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs", "documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"], "loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.10", "deebot-client==9.1.0"] "requirements": ["py-sucks==0.9.10", "deebot-client==9.2.0"]
} }

View File

@ -35,7 +35,7 @@ def check_local_version_supported(api_version: str | None) -> bool:
class DirectPanel(PanelEntry): class DirectPanel(PanelEntry):
"""Helper class for wrapping a directly accessed Elmax Panel.""" """Helper class for wrapping a directly accessed Elmax Panel."""
def __init__(self, panel_uri): def __init__(self, panel_uri) -> None:
"""Construct the object.""" """Construct the object."""
super().__init__(panel_uri, True, {}) super().__init__(panel_uri, True, {})

View File

@ -203,7 +203,7 @@ class ElmaxConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_direct(self, user_input: dict[str, Any]) -> ConfigFlowResult: async def async_step_direct(self, user_input: dict[str, Any]) -> ConfigFlowResult:
"""Handle the direct setup step.""" """Handle the direct setup step."""
self._selected_mode = CONF_ELMAX_MODE_CLOUD self._selected_mode = CONF_ELMAX_MODE_DIRECT
if user_input is None: if user_input is None:
return self.async_show_form( return self.async_show_form(
step_id=CONF_ELMAX_MODE_DIRECT, step_id=CONF_ELMAX_MODE_DIRECT,

View File

@ -121,13 +121,13 @@ class ElmaxCover(ElmaxEntity, CoverEntity):
else: else:
_LOGGER.debug("Ignoring stop request as the cover is IDLE") _LOGGER.debug("Ignoring stop request as the cover is IDLE")
async def async_open_cover(self, **kwargs): async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover.""" """Open the cover."""
await self.coordinator.http_client.execute_command( await self.coordinator.http_client.execute_command(
endpoint_id=self._device.endpoint_id, command=CoverCommand.UP endpoint_id=self._device.endpoint_id, command=CoverCommand.UP
) )
async def async_close_cover(self, **kwargs): async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover.""" """Close the cover."""
await self.coordinator.http_client.execute_command( await self.coordinator.http_client.execute_command(
endpoint_id=self._device.endpoint_id, command=CoverCommand.DOWN endpoint_id=self._device.endpoint_id, command=CoverCommand.DOWN

View File

@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/elmax", "documentation": "https://www.home-assistant.io/integrations/elmax",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["elmax_api"], "loggers": ["elmax_api"],
"requirements": ["elmax-api==0.0.6.1"], "requirements": ["elmax-api==0.0.6.3"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_elmax-ssl._tcp.local." "type": "_elmax-ssl._tcp.local."

View File

@ -16,7 +16,7 @@
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
"mqtt": ["esphome/discover/#"], "mqtt": ["esphome/discover/#"],
"requirements": [ "requirements": [
"aioesphomeapi==27.0.3", "aioesphomeapi==28.0.0",
"esphome-dashboard-api==1.2.3", "esphome-dashboard-api==1.2.3",
"bleak-esphome==1.1.0" "bleak-esphome==1.1.0"
], ],

View File

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend", "documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["home-assistant-frontend==20241127.4"] "requirements": ["home-assistant-frontend==20241127.6"]
} }

View File

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import date, datetime, timedelta from datetime import UTC, date, datetime, timedelta
from typing import Any, cast from typing import Any, cast
from homeassistant.components.todo import ( from homeassistant.components.todo import (
@ -39,8 +39,10 @@ def _convert_todo_item(item: TodoItem) -> dict[str, str | None]:
else: else:
result["status"] = TodoItemStatus.NEEDS_ACTION result["status"] = TodoItemStatus.NEEDS_ACTION
if (due := item.due) is not None: if (due := item.due) is not None:
# due API field is a timestamp string, but with only date resolution # due API field is a timestamp string, but with only date resolution.
result["due"] = dt_util.start_of_local_day(due).isoformat() # The time portion of the date is always discarded by the API, so we
# always set to UTC.
result["due"] = dt_util.start_of_local_day(due).replace(tzinfo=UTC).isoformat()
else: else:
result["due"] = None result["due"] = None
result["notes"] = item.description result["notes"] = item.description
@ -51,6 +53,8 @@ def _convert_api_item(item: dict[str, str]) -> TodoItem:
"""Convert tasks API items into a TodoItem.""" """Convert tasks API items into a TodoItem."""
due: date | None = None due: date | None = None
if (due_str := item.get("due")) is not None: if (due_str := item.get("due")) is not None:
# Due dates are returned always in UTC so we only need to
# parse the date portion which will be interpreted as a a local date.
due = datetime.fromisoformat(due_str).date() due = datetime.fromisoformat(due_str).date()
return TodoItem( return TodoItem(
summary=item["title"], summary=item["title"],

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
from pathlib import Path from pathlib import Path
import sys
from typing import Final from typing import Final
from aiohttp.hdrs import CACHE_CONTROL, CONTENT_TYPE from aiohttp.hdrs import CACHE_CONTROL, CONTENT_TYPE
@ -17,6 +18,15 @@ CACHE_HEADER = f"public, max-age={CACHE_TIME}"
CACHE_HEADERS: Mapping[str, str] = {CACHE_CONTROL: CACHE_HEADER} CACHE_HEADERS: Mapping[str, str] = {CACHE_CONTROL: CACHE_HEADER}
RESPONSE_CACHE: LRU[tuple[str, Path], tuple[Path, str]] = LRU(512) RESPONSE_CACHE: LRU[tuple[str, Path], tuple[Path, str]] = LRU(512)
if sys.version_info >= (3, 13):
# guess_type is soft-deprecated in 3.13
# for paths and should only be used for
# URLs. guess_file_type should be used
# for paths instead.
_GUESSER = CONTENT_TYPES.guess_file_type
else:
_GUESSER = CONTENT_TYPES.guess_type
class CachingStaticResource(StaticResource): class CachingStaticResource(StaticResource):
"""Static Resource handler that will add cache headers.""" """Static Resource handler that will add cache headers."""
@ -37,9 +47,7 @@ class CachingStaticResource(StaticResource):
# Must be directory index; ignore caching # Must be directory index; ignore caching
return response return response
file_path = response._path # noqa: SLF001 file_path = response._path # noqa: SLF001
response.content_type = ( response.content_type = _GUESSER(file_path)[0] or FALLBACK_CONTENT_TYPE
CONTENT_TYPES.guess_type(file_path)[0] or FALLBACK_CONTENT_TYPE
)
# Cache actual header after setter construction. # Cache actual header after setter construction.
content_type = response.headers[CONTENT_TYPE] content_type = response.headers[CONTENT_TYPE]
RESPONSE_CACHE[key] = (file_path, content_type) RESPONSE_CACHE[key] = (file_path, content_type)

View File

@ -27,7 +27,9 @@ from .entity import NordpoolBaseEntity
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
def get_prices(data: DeliveryPeriodData) -> dict[str, tuple[float, float, float]]: def get_prices(
data: DeliveryPeriodData,
) -> dict[str, tuple[float | None, float, float | None]]:
"""Return previous, current and next prices. """Return previous, current and next prices.
Output: {"SE3": (10.0, 10.5, 12.1)} Output: {"SE3": (10.0, 10.5, 12.1)}
@ -39,6 +41,7 @@ def get_prices(data: DeliveryPeriodData) -> dict[str, tuple[float, float, float]
previous_time = current_time - timedelta(hours=1) previous_time = current_time - timedelta(hours=1)
next_time = current_time + timedelta(hours=1) next_time = current_time + timedelta(hours=1)
price_data = data.entries price_data = data.entries
LOGGER.debug("Price data: %s", price_data)
for entry in price_data: for entry in price_data:
if entry.start <= current_time <= entry.end: if entry.start <= current_time <= entry.end:
current_price_entries = entry.entry current_price_entries = entry.entry
@ -46,10 +49,20 @@ def get_prices(data: DeliveryPeriodData) -> dict[str, tuple[float, float, float]
last_price_entries = entry.entry last_price_entries = entry.entry
if entry.start <= next_time <= entry.end: if entry.start <= next_time <= entry.end:
next_price_entries = entry.entry next_price_entries = entry.entry
LOGGER.debug(
"Last price %s, current price %s, next price %s",
last_price_entries,
current_price_entries,
next_price_entries,
)
result = {} result = {}
for area, price in current_price_entries.items(): for area, price in current_price_entries.items():
result[area] = (last_price_entries[area], price, next_price_entries[area]) result[area] = (
last_price_entries.get(area),
price,
next_price_entries.get(area),
)
LOGGER.debug("Prices: %s", result) LOGGER.debug("Prices: %s", result)
return result return result
@ -90,7 +103,7 @@ class NordpoolDefaultSensorEntityDescription(SensorEntityDescription):
class NordpoolPricesSensorEntityDescription(SensorEntityDescription): class NordpoolPricesSensorEntityDescription(SensorEntityDescription):
"""Describes Nord Pool prices sensor entity.""" """Describes Nord Pool prices sensor entity."""
value_fn: Callable[[tuple[float, float, float]], float | None] value_fn: Callable[[tuple[float | None, float, float | None]], float | None]
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
@ -136,13 +149,13 @@ PRICES_SENSOR_TYPES: tuple[NordpoolPricesSensorEntityDescription, ...] = (
NordpoolPricesSensorEntityDescription( NordpoolPricesSensorEntityDescription(
key="last_price", key="last_price",
translation_key="last_price", translation_key="last_price",
value_fn=lambda data: data[0] / 1000, value_fn=lambda data: data[0] / 1000 if data[0] else None,
suggested_display_precision=2, suggested_display_precision=2,
), ),
NordpoolPricesSensorEntityDescription( NordpoolPricesSensorEntityDescription(
key="next_price", key="next_price",
translation_key="next_price", translation_key="next_price",
value_fn=lambda data: data[2] / 1000, value_fn=lambda data: data[2] / 1000 if data[2] else None,
suggested_display_precision=2, suggested_display_precision=2,
), ),
) )

View File

@ -480,7 +480,13 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
NumberDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, NumberDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, NumberDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.POWER_FACTOR: {PERCENTAGE, None}, NumberDeviceClass.POWER_FACTOR: {PERCENTAGE, None},
NumberDeviceClass.POWER: {UnitOfPower.WATT, UnitOfPower.KILO_WATT}, NumberDeviceClass.POWER: {
UnitOfPower.WATT,
UnitOfPower.KILO_WATT,
UnitOfPower.MEGA_WATT,
UnitOfPower.GIGA_WATT,
UnitOfPower.TERA_WATT,
},
NumberDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth), NumberDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth),
NumberDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux), NumberDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux),
NumberDeviceClass.PRESSURE: set(UnitOfPressure), NumberDeviceClass.PRESSURE: set(UnitOfPressure),

View File

@ -37,7 +37,7 @@
"requirements": [ "requirements": [
"getmac==0.9.4", "getmac==0.9.4",
"samsungctl[websocket]==0.7.1", "samsungctl[websocket]==0.7.1",
"samsungtvws[async,encrypted]==2.7.1", "samsungtvws[async,encrypted]==2.7.2",
"wakeonlan==2.1.0", "wakeonlan==2.1.0",
"async-upnp-client==0.41.0" "async-upnp-client==0.41.0"
], ],

View File

@ -579,7 +579,13 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
SensorDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, SensorDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, SensorDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.POWER_FACTOR: {PERCENTAGE, None}, SensorDeviceClass.POWER_FACTOR: {PERCENTAGE, None},
SensorDeviceClass.POWER: {UnitOfPower.WATT, UnitOfPower.KILO_WATT}, SensorDeviceClass.POWER: {
UnitOfPower.WATT,
UnitOfPower.KILO_WATT,
UnitOfPower.MEGA_WATT,
UnitOfPower.GIGA_WATT,
UnitOfPower.TERA_WATT,
},
SensorDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth), SensorDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth),
SensorDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux), SensorDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux),
SensorDeviceClass.PRESSURE: set(UnitOfPressure), SensorDeviceClass.PRESSURE: set(UnitOfPressure),

View File

@ -21,6 +21,7 @@ SCOPES = [
Scope.OPENID, Scope.OPENID,
Scope.OFFLINE_ACCESS, Scope.OFFLINE_ACCESS,
Scope.VEHICLE_DEVICE_DATA, Scope.VEHICLE_DEVICE_DATA,
Scope.VEHICLE_LOCATION,
Scope.VEHICLE_CMDS, Scope.VEHICLE_CMDS,
Scope.VEHICLE_CHARGING_CMDS, Scope.VEHICLE_CHARGING_CMDS,
Scope.ENERGY_DEVICE_DATA, Scope.ENERGY_DEVICE_DATA,

View File

@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/tesla_fleet", "documentation": "https://www.home-assistant.io/integrations/tesla_fleet",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"], "loggers": ["tesla-fleet-api"],
"requirements": ["tesla-fleet-api==0.8.4"] "requirements": ["tesla-fleet-api==0.8.5"]
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/teslemetry", "documentation": "https://www.home-assistant.io/integrations/teslemetry",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"], "loggers": ["tesla-fleet-api"],
"requirements": ["tesla-fleet-api==0.8.4", "teslemetry-stream==0.4.2"] "requirements": ["tesla-fleet-api==0.8.5", "teslemetry-stream==0.4.2"]
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/tessie", "documentation": "https://www.home-assistant.io/integrations/tessie",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["tessie", "tesla-fleet-api"], "loggers": ["tessie", "tesla-fleet-api"],
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==0.8.4"] "requirements": ["tessie-api==0.1.1", "tesla-fleet-api==0.8.5"]
} }

View File

@ -300,5 +300,5 @@
"documentation": "https://www.home-assistant.io/integrations/tplink", "documentation": "https://www.home-assistant.io/integrations/tplink",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["kasa"], "loggers": ["kasa"],
"requirements": ["python-kasa[speedups]==0.8.0"] "requirements": ["python-kasa[speedups]==0.8.1"]
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/upb", "documentation": "https://www.home-assistant.io/integrations/upb",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["upb_lib"], "loggers": ["upb_lib"],
"requirements": ["upb-lib==0.5.8"] "requirements": ["upb-lib==0.5.9"]
} }

View File

@ -1,9 +1,9 @@
"""Support for tracking consumption over given periods of time.""" """Support for tracking consumption over given periods of time."""
from datetime import timedelta from datetime import datetime, timedelta
import logging import logging
from croniter import croniter from cronsim import CronSim, CronSimError
import voluptuous as vol import voluptuous as vol
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
@ -47,9 +47,12 @@ DEFAULT_OFFSET = timedelta(hours=0)
def validate_cron_pattern(pattern): def validate_cron_pattern(pattern):
"""Check that the pattern is well-formed.""" """Check that the pattern is well-formed."""
if croniter.is_valid(pattern): try:
return pattern CronSim(pattern, datetime(2020, 1, 1)) # any date will do
raise vol.Invalid("Invalid pattern") except CronSimError as err:
_LOGGER.error("Invalid cron pattern %s: %s", pattern, err)
raise vol.Invalid("Invalid pattern") from err
return pattern
def period_or_cron(config): def period_or_cron(config):

View File

@ -6,7 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/utility_meter", "documentation": "https://www.home-assistant.io/integrations/utility_meter",
"integration_type": "helper", "integration_type": "helper",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["croniter"],
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["cronsim==2.6"] "requirements": ["cronsim==2.6"]
} }

View File

@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024 MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 12 MINOR_VERSION: Final = 12
PATCH_VERSION: Final = "0" PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)

View File

@ -71,7 +71,10 @@ async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]:
try: try:
info_object["user"] = cached_get_user() info_object["user"] = cached_get_user()
except KeyError: except (KeyError, OSError):
# OSError on python >= 3.13, KeyError on python < 3.13
# KeyError can be removed when 3.12 support is dropped
# see https://docs.python.org/3/whatsnew/3.13.html
info_object["user"] = None info_object["user"] = None
if platform.system() == "Darwin": if platform.system() == "Darwin":

View File

@ -5,7 +5,7 @@ aiodiscover==2.1.0
aiodns==3.2.0 aiodns==3.2.0
aiohasupervisor==0.2.1 aiohasupervisor==0.2.1
aiohttp-fast-zlib==0.2.0 aiohttp-fast-zlib==0.2.0
aiohttp==3.11.9 aiohttp==3.11.10
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
aiozoneinfo==0.2.1 aiozoneinfo==0.2.1
astral==2.2 astral==2.2
@ -31,10 +31,10 @@ fnv-hash-fast==1.0.2
go2rtc-client==0.1.1 go2rtc-client==0.1.1
ha-ffmpeg==3.2.2 ha-ffmpeg==3.2.2
habluetooth==3.6.0 habluetooth==3.6.0
hass-nabucasa==0.85.0 hass-nabucasa==0.86.0
hassil==2.0.5 hassil==2.0.5
home-assistant-bluetooth==1.13.0 home-assistant-bluetooth==1.13.0
home-assistant-frontend==20241127.4 home-assistant-frontend==20241127.6
home-assistant-intents==2024.12.4 home-assistant-intents==2024.12.4
httpx==0.27.2 httpx==0.27.2
ifaddr==0.2.0 ifaddr==0.2.0

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2024.12.0" version = "2024.12.1"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"
@ -29,7 +29,7 @@ dependencies = [
# change behavior based on presence of supervisor. Deprecated with #127228 # change behavior based on presence of supervisor. Deprecated with #127228
# Lib can be removed with 2025.11 # Lib can be removed with 2025.11
"aiohasupervisor==0.2.1", "aiohasupervisor==0.2.1",
"aiohttp==3.11.9", "aiohttp==3.11.10",
"aiohttp_cors==0.7.0", "aiohttp_cors==0.7.0",
"aiohttp-fast-zlib==0.2.0", "aiohttp-fast-zlib==0.2.0",
"aiozoneinfo==0.2.1", "aiozoneinfo==0.2.1",
@ -45,7 +45,7 @@ dependencies = [
"fnv-hash-fast==1.0.2", "fnv-hash-fast==1.0.2",
# hass-nabucasa is imported by helpers which don't depend on the cloud # hass-nabucasa is imported by helpers which don't depend on the cloud
# integration # integration
"hass-nabucasa==0.85.0", "hass-nabucasa==0.86.0",
# When bumping httpx, please check the version pins of # When bumping httpx, please check the version pins of
# httpcore, anyio, and h11 in gen_requirements_all # httpcore, anyio, and h11 in gen_requirements_all
"httpx==0.27.2", "httpx==0.27.2",

View File

@ -5,7 +5,7 @@
# Home Assistant Core # Home Assistant Core
aiodns==3.2.0 aiodns==3.2.0
aiohasupervisor==0.2.1 aiohasupervisor==0.2.1
aiohttp==3.11.9 aiohttp==3.11.10
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
aiohttp-fast-zlib==0.2.0 aiohttp-fast-zlib==0.2.0
aiozoneinfo==0.2.1 aiozoneinfo==0.2.1
@ -19,7 +19,7 @@ bcrypt==4.2.0
certifi>=2021.5.30 certifi>=2021.5.30
ciso8601==2.3.1 ciso8601==2.3.1
fnv-hash-fast==1.0.2 fnv-hash-fast==1.0.2
hass-nabucasa==0.85.0 hass-nabucasa==0.86.0
httpx==0.27.2 httpx==0.27.2
home-assistant-bluetooth==1.13.0 home-assistant-bluetooth==1.13.0
ifaddr==0.2.0 ifaddr==0.2.0

View File

@ -243,7 +243,7 @@ aioelectricitymaps==0.4.0
aioemonitor==1.0.5 aioemonitor==1.0.5
# homeassistant.components.esphome # homeassistant.components.esphome
aioesphomeapi==27.0.3 aioesphomeapi==28.0.0
# homeassistant.components.flo # homeassistant.components.flo
aioflo==2021.11.0 aioflo==2021.11.0
@ -738,7 +738,7 @@ debugpy==1.8.6
# decora==0.6 # decora==0.6
# homeassistant.components.ecovacs # homeassistant.components.ecovacs
deebot-client==9.1.0 deebot-client==9.2.0
# homeassistant.components.ihc # homeassistant.components.ihc
# homeassistant.components.namecheapdns # homeassistant.components.namecheapdns
@ -824,7 +824,7 @@ eliqonline==1.2.2
elkm1-lib==2.2.10 elkm1-lib==2.2.10
# homeassistant.components.elmax # homeassistant.components.elmax
elmax-api==0.0.6.1 elmax-api==0.0.6.3
# homeassistant.components.elvia # homeassistant.components.elvia
elvia==0.1.0 elvia==0.1.0
@ -1090,7 +1090,7 @@ habitipy==0.3.3
habluetooth==3.6.0 habluetooth==3.6.0
# homeassistant.components.cloud # homeassistant.components.cloud
hass-nabucasa==0.85.0 hass-nabucasa==0.86.0
# homeassistant.components.splunk # homeassistant.components.splunk
hass-splunk==0.1.1 hass-splunk==0.1.1
@ -1130,7 +1130,7 @@ hole==0.8.0
holidays==0.62 holidays==0.62
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20241127.4 home-assistant-frontend==20241127.6
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2024.12.4 home-assistant-intents==2024.12.4
@ -1841,7 +1841,7 @@ pydaikin==2.13.7
pydanfossair==0.1.0 pydanfossair==0.1.0
# homeassistant.components.deako # homeassistant.components.deako
pydeako==0.5.4 pydeako==0.6.0
# homeassistant.components.deconz # homeassistant.components.deconz
pydeconz==118 pydeconz==118
@ -2362,7 +2362,7 @@ python-join-api==0.0.9
python-juicenet==1.1.0 python-juicenet==1.1.0
# homeassistant.components.tplink # homeassistant.components.tplink
python-kasa[speedups]==0.8.0 python-kasa[speedups]==0.8.1
# homeassistant.components.linkplay # homeassistant.components.linkplay
python-linkplay==0.0.20 python-linkplay==0.0.20
@ -2610,7 +2610,7 @@ rxv==0.7.0
samsungctl[websocket]==0.7.1 samsungctl[websocket]==0.7.1
# homeassistant.components.samsungtv # homeassistant.components.samsungtv
samsungtvws[async,encrypted]==2.7.1 samsungtvws[async,encrypted]==2.7.2
# homeassistant.components.sanix # homeassistant.components.sanix
sanix==1.0.6 sanix==1.0.6
@ -2810,7 +2810,7 @@ temperusb==1.6.1
# homeassistant.components.tesla_fleet # homeassistant.components.tesla_fleet
# homeassistant.components.teslemetry # homeassistant.components.teslemetry
# homeassistant.components.tessie # homeassistant.components.tessie
tesla-fleet-api==0.8.4 tesla-fleet-api==0.8.5
# homeassistant.components.powerwall # homeassistant.components.powerwall
tesla-powerwall==0.5.2 tesla-powerwall==0.5.2
@ -2915,7 +2915,7 @@ unifiled==0.11
universal-silabs-flasher==0.0.25 universal-silabs-flasher==0.0.25
# homeassistant.components.upb # homeassistant.components.upb
upb-lib==0.5.8 upb-lib==0.5.9
# homeassistant.components.upcloud # homeassistant.components.upcloud
upcloud-api==2.6.0 upcloud-api==2.6.0

View File

@ -231,7 +231,7 @@ aioelectricitymaps==0.4.0
aioemonitor==1.0.5 aioemonitor==1.0.5
# homeassistant.components.esphome # homeassistant.components.esphome
aioesphomeapi==27.0.3 aioesphomeapi==28.0.0
# homeassistant.components.flo # homeassistant.components.flo
aioflo==2021.11.0 aioflo==2021.11.0
@ -628,7 +628,7 @@ dbus-fast==2.24.3
debugpy==1.8.6 debugpy==1.8.6
# homeassistant.components.ecovacs # homeassistant.components.ecovacs
deebot-client==9.1.0 deebot-client==9.2.0
# homeassistant.components.ihc # homeassistant.components.ihc
# homeassistant.components.namecheapdns # homeassistant.components.namecheapdns
@ -699,7 +699,7 @@ elgato==5.1.2
elkm1-lib==2.2.10 elkm1-lib==2.2.10
# homeassistant.components.elmax # homeassistant.components.elmax
elmax-api==0.0.6.1 elmax-api==0.0.6.3
# homeassistant.components.elvia # homeassistant.components.elvia
elvia==0.1.0 elvia==0.1.0
@ -928,7 +928,7 @@ habitipy==0.3.3
habluetooth==3.6.0 habluetooth==3.6.0
# homeassistant.components.cloud # homeassistant.components.cloud
hass-nabucasa==0.85.0 hass-nabucasa==0.86.0
# homeassistant.components.conversation # homeassistant.components.conversation
hassil==2.0.5 hassil==2.0.5
@ -956,7 +956,7 @@ hole==0.8.0
holidays==0.62 holidays==0.62
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20241127.4 home-assistant-frontend==20241127.6
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2024.12.4 home-assistant-intents==2024.12.4
@ -1488,7 +1488,7 @@ pycsspeechtts==1.0.8
pydaikin==2.13.7 pydaikin==2.13.7
# homeassistant.components.deako # homeassistant.components.deako
pydeako==0.5.4 pydeako==0.6.0
# homeassistant.components.deconz # homeassistant.components.deconz
pydeconz==118 pydeconz==118
@ -1889,7 +1889,7 @@ python-izone==1.2.9
python-juicenet==1.1.0 python-juicenet==1.1.0
# homeassistant.components.tplink # homeassistant.components.tplink
python-kasa[speedups]==0.8.0 python-kasa[speedups]==0.8.1
# homeassistant.components.linkplay # homeassistant.components.linkplay
python-linkplay==0.0.20 python-linkplay==0.0.20
@ -2086,7 +2086,7 @@ rxv==0.7.0
samsungctl[websocket]==0.7.1 samsungctl[websocket]==0.7.1
# homeassistant.components.samsungtv # homeassistant.components.samsungtv
samsungtvws[async,encrypted]==2.7.1 samsungtvws[async,encrypted]==2.7.2
# homeassistant.components.sanix # homeassistant.components.sanix
sanix==1.0.6 sanix==1.0.6
@ -2238,7 +2238,7 @@ temperusb==1.6.1
# homeassistant.components.tesla_fleet # homeassistant.components.tesla_fleet
# homeassistant.components.teslemetry # homeassistant.components.teslemetry
# homeassistant.components.tessie # homeassistant.components.tessie
tesla-fleet-api==0.8.4 tesla-fleet-api==0.8.5
# homeassistant.components.powerwall # homeassistant.components.powerwall
tesla-powerwall==0.5.2 tesla-powerwall==0.5.2
@ -2322,7 +2322,7 @@ unifi-discovery==1.2.0
universal-silabs-flasher==0.0.25 universal-silabs-flasher==0.0.25
# homeassistant.components.upb # homeassistant.components.upb
upb-lib==0.5.8 upb-lib==0.5.9
# homeassistant.components.upcloud # homeassistant.components.upcloud
upcloud-api==2.6.0 upcloud-api==2.6.0

View File

@ -2,7 +2,7 @@
from unittest.mock import MagicMock from unittest.mock import MagicMock
from pydeako.deako import DeviceListTimeout, FindDevicesTimeout from pydeako import FindDevicesError
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -37,7 +37,7 @@ async def test_deako_async_setup_entry(
assert mock_config_entry.runtime_data == pydeako_deako_mock.return_value assert mock_config_entry.runtime_data == pydeako_deako_mock.return_value
async def test_deako_async_setup_entry_device_list_timeout( async def test_deako_async_setup_entry_devices_error(
hass: HomeAssistant, hass: HomeAssistant,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
pydeako_deako_mock: MagicMock, pydeako_deako_mock: MagicMock,
@ -47,32 +47,7 @@ async def test_deako_async_setup_entry_device_list_timeout(
mock_config_entry.add_to_hass(hass) mock_config_entry.add_to_hass(hass)
pydeako_deako_mock.return_value.find_devices.side_effect = DeviceListTimeout() pydeako_deako_mock.return_value.find_devices.side_effect = FindDevicesError()
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
pydeako_deako_mock.assert_called_once_with(
pydeako_discoverer_mock.return_value.get_address
)
pydeako_deako_mock.return_value.connect.assert_called_once()
pydeako_deako_mock.return_value.find_devices.assert_called_once()
pydeako_deako_mock.return_value.disconnect.assert_called_once()
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_deako_async_setup_entry_find_devices_timeout(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
pydeako_deako_mock: MagicMock,
pydeako_discoverer_mock: MagicMock,
) -> None:
"""Test async_setup_entry raises ConfigEntryNotReady when pydeako raises FindDevicesTimeout."""
mock_config_entry.add_to_hass(hass)
pydeako_deako_mock.return_value.find_devices.side_effect = FindDevicesTimeout()
await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -1,6 +1,7 @@
"""Configuration for Elmax tests.""" """Configuration for Elmax tests."""
from collections.abc import Generator from collections.abc import Generator
from datetime import datetime, timedelta
import json import json
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
@ -11,6 +12,7 @@ from elmax_api.constants import (
ENDPOINT_LOGIN, ENDPOINT_LOGIN,
) )
from httpx import Response from httpx import Response
import jwt
import pytest import pytest
import respx import respx
@ -64,9 +66,20 @@ def httpx_mock_direct_fixture() -> Generator[respx.MockRouter]:
) as respx_mock: ) as respx_mock:
# Mock Login POST. # Mock Login POST.
login_route = respx_mock.post(f"/api/v2/{ENDPOINT_LOGIN}", name="login") login_route = respx_mock.post(f"/api/v2/{ENDPOINT_LOGIN}", name="login")
login_route.return_value = Response(
200, json=json.loads(load_fixture("direct/login.json", "elmax")) login_json = json.loads(load_fixture("direct/login.json", "elmax"))
decoded_jwt = jwt.decode_complete(
login_json["token"].split(" ")[1],
algorithms="HS256",
options={"verify_signature": False},
) )
expiration = datetime.now() + timedelta(hours=1)
decoded_jwt["payload"]["exp"] = int(expiration.timestamp())
jws_string = jwt.encode(
payload=decoded_jwt["payload"], algorithm="HS256", key=""
)
login_json["token"] = f"JWT {jws_string}"
login_route.return_value = Response(200, json=login_json)
# Mock Device list GET. # Mock Device list GET.
list_devices_route = respx_mock.get( list_devices_route = respx_mock.get(

View File

@ -15,7 +15,7 @@
) )
# --- # ---
# name: test_create_todo_list_item[due].1 # name: test_create_todo_list_item[due].1
'{"title": "Soda", "status": "needsAction", "due": "2023-11-18T00:00:00-08:00", "notes": null}' '{"title": "Soda", "status": "needsAction", "due": "2023-11-18T00:00:00+00:00", "notes": null}'
# --- # ---
# name: test_create_todo_list_item[summary] # name: test_create_todo_list_item[summary]
tuple( tuple(
@ -137,7 +137,7 @@
) )
# --- # ---
# name: test_partial_update[due_date].1 # name: test_partial_update[due_date].1
'{"title": "Water", "status": "needsAction", "due": "2023-11-18T00:00:00-08:00", "notes": null}' '{"title": "Water", "status": "needsAction", "due": "2023-11-18T00:00:00+00:00", "notes": null}'
# --- # ---
# name: test_partial_update[empty_description] # name: test_partial_update[empty_description]
tuple( tuple(
@ -166,6 +166,33 @@
# name: test_partial_update_status[api_responses0].1 # name: test_partial_update_status[api_responses0].1
'{"title": "Water", "status": "needsAction", "due": null, "notes": null}' '{"title": "Water", "status": "needsAction", "due": null, "notes": null}'
# --- # ---
# name: test_update_due_date[api_responses0-America/Regina]
tuple(
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json',
'PATCH',
)
# ---
# name: test_update_due_date[api_responses0-America/Regina].1
'{"title": "Water", "status": "needsAction", "due": "2024-12-05T00:00:00+00:00", "notes": null}'
# ---
# name: test_update_due_date[api_responses0-Asia/Tokyo]
tuple(
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json',
'PATCH',
)
# ---
# name: test_update_due_date[api_responses0-Asia/Tokyo].1
'{"title": "Water", "status": "needsAction", "due": "2024-12-05T00:00:00+00:00", "notes": null}'
# ---
# name: test_update_due_date[api_responses0-UTC]
tuple(
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json',
'PATCH',
)
# ---
# name: test_update_due_date[api_responses0-UTC].1
'{"title": "Water", "status": "needsAction", "due": "2024-12-05T00:00:00+00:00", "notes": null}'
# ---
# name: test_update_todo_list_item[api_responses0] # name: test_update_todo_list_item[api_responses0]
tuple( tuple(
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json', 'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json',

View File

@ -239,6 +239,7 @@ def mock_http_response(response_handler: list | Callable) -> Mock:
yield mock_response yield mock_response
@pytest.mark.parametrize("timezone", ["America/Regina", "UTC", "Asia/Tokyo"])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"api_responses", "api_responses",
[ [
@ -251,7 +252,7 @@ def mock_http_response(response_handler: list | Callable) -> Mock:
"title": "Task 1", "title": "Task 1",
"status": "needsAction", "status": "needsAction",
"position": "0000000000000001", "position": "0000000000000001",
"due": "2023-11-18T00:00:00+00:00", "due": "2023-11-18T00:00:00Z",
}, },
{ {
"id": "task-2", "id": "task-2",
@ -271,8 +272,10 @@ async def test_get_items(
integration_setup: Callable[[], Awaitable[bool]], integration_setup: Callable[[], Awaitable[bool]],
hass_ws_client: WebSocketGenerator, hass_ws_client: WebSocketGenerator,
ws_get_items: Callable[[], Awaitable[dict[str, str]]], ws_get_items: Callable[[], Awaitable[dict[str, str]]],
timezone: str,
) -> None: ) -> None:
"""Test getting todo list items.""" """Test getting todo list items."""
await hass.config.async_set_time_zone(timezone)
assert await integration_setup() assert await integration_setup()
@ -484,6 +487,39 @@ async def test_update_todo_list_item(
assert call.kwargs.get("body") == snapshot assert call.kwargs.get("body") == snapshot
@pytest.mark.parametrize("timezone", ["America/Regina", "UTC", "Asia/Tokyo"])
@pytest.mark.parametrize("api_responses", [UPDATE_API_RESPONSES])
async def test_update_due_date(
hass: HomeAssistant,
setup_credentials: None,
integration_setup: Callable[[], Awaitable[bool]],
mock_http_response: Any,
snapshot: SnapshotAssertion,
timezone: str,
) -> None:
"""Test for updating the due date of a To-do item and timezone."""
await hass.config.async_set_time_zone(timezone)
assert await integration_setup()
state = hass.states.get("todo.my_tasks")
assert state
assert state.state == "1"
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.UPDATE_ITEM,
{ATTR_ITEM: "some-task-id", ATTR_DUE_DATE: "2024-12-5"},
target={ATTR_ENTITY_ID: "todo.my_tasks"},
blocking=True,
)
assert len(mock_http_response.call_args_list) == 4
call = mock_http_response.call_args_list[2]
assert call
assert call.args == snapshot
assert call.kwargs.get("body") == snapshot
@pytest.mark.parametrize( @pytest.mark.parametrize(
"api_responses", "api_responses",
[ [

View File

@ -165,6 +165,7 @@
'openid', 'openid',
'offline_access', 'offline_access',
'vehicle_device_data', 'vehicle_device_data',
'vehicle_location',
'vehicle_cmds', 'vehicle_cmds',
'vehicle_charging_cmds', 'vehicle_charging_cmds',
'energy_device_data', 'energy_device_data',

View File

@ -93,10 +93,9 @@ async def test_container_installationtype(hass: HomeAssistant) -> None:
assert info["installation_type"] == "Unsupported Third Party Container" assert info["installation_type"] == "Unsupported Third Party Container"
async def test_getuser_keyerror(hass: HomeAssistant) -> None: @pytest.mark.parametrize("error", [KeyError, OSError])
"""Test getuser keyerror.""" async def test_getuser_oserror(hass: HomeAssistant, error: Exception) -> None:
with patch( """Test getuser oserror."""
"homeassistant.helpers.system_info.cached_get_user", side_effect=KeyError with patch("homeassistant.helpers.system_info.cached_get_user", side_effect=error):
):
info = await async_get_system_info(hass) info = await async_get_system_info(hass)
assert info["user"] is None assert info["user"] is None