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",
"iot_class": "cloud_push",
"loggers": ["hass_nabucasa"],
"requirements": ["hass-nabucasa==0.85.0"],
"requirements": ["hass-nabucasa==0.86.0"],
"single_config_entry": true
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"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):
"""Helper class for wrapping a directly accessed Elmax Panel."""
def __init__(self, panel_uri):
def __init__(self, panel_uri) -> None:
"""Construct the object."""
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:
"""Handle the direct setup step."""
self._selected_mode = CONF_ELMAX_MODE_CLOUD
self._selected_mode = CONF_ELMAX_MODE_DIRECT
if user_input is None:
return self.async_show_form(
step_id=CONF_ELMAX_MODE_DIRECT,

View File

@ -121,13 +121,13 @@ class ElmaxCover(ElmaxEntity, CoverEntity):
else:
_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."""
await self.coordinator.http_client.execute_command(
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."""
await self.coordinator.http_client.execute_command(
endpoint_id=self._device.endpoint_id, command=CoverCommand.DOWN

View File

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

View File

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

View File

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"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 datetime import date, datetime, timedelta
from datetime import UTC, date, datetime, timedelta
from typing import Any, cast
from homeassistant.components.todo import (
@ -39,8 +39,10 @@ def _convert_todo_item(item: TodoItem) -> dict[str, str | None]:
else:
result["status"] = TodoItemStatus.NEEDS_ACTION
if (due := item.due) is not None:
# due API field is a timestamp string, but with only date resolution
result["due"] = dt_util.start_of_local_day(due).isoformat()
# due API field is a timestamp string, but with only date resolution.
# 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:
result["due"] = None
result["notes"] = item.description
@ -51,6 +53,8 @@ def _convert_api_item(item: dict[str, str]) -> TodoItem:
"""Convert tasks API items into a TodoItem."""
due: date | None = 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()
return TodoItem(
summary=item["title"],

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Mapping
from pathlib import Path
import sys
from typing import Final
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}
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):
"""Static Resource handler that will add cache headers."""
@ -37,9 +47,7 @@ class CachingStaticResource(StaticResource):
# Must be directory index; ignore caching
return response
file_path = response._path # noqa: SLF001
response.content_type = (
CONTENT_TYPES.guess_type(file_path)[0] or FALLBACK_CONTENT_TYPE
)
response.content_type = _GUESSER(file_path)[0] or FALLBACK_CONTENT_TYPE
# Cache actual header after setter construction.
content_type = response.headers[CONTENT_TYPE]
RESPONSE_CACHE[key] = (file_path, content_type)

View File

@ -27,7 +27,9 @@ from .entity import NordpoolBaseEntity
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.
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)
next_time = current_time + timedelta(hours=1)
price_data = data.entries
LOGGER.debug("Price data: %s", price_data)
for entry in price_data:
if entry.start <= current_time <= entry.end:
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
if entry.start <= next_time <= entry.end:
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 = {}
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)
return result
@ -90,7 +103,7 @@ class NordpoolDefaultSensorEntityDescription(SensorEntityDescription):
class NordpoolPricesSensorEntityDescription(SensorEntityDescription):
"""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)
@ -136,13 +149,13 @@ PRICES_SENSOR_TYPES: tuple[NordpoolPricesSensorEntityDescription, ...] = (
NordpoolPricesSensorEntityDescription(
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,
),
NordpoolPricesSensorEntityDescription(
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,
),
)

View File

@ -480,7 +480,13 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
NumberDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
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_INTENSITY: set(UnitOfVolumetricFlux),
NumberDeviceClass.PRESSURE: set(UnitOfPressure),

View File

@ -37,7 +37,7 @@
"requirements": [
"getmac==0.9.4",
"samsungctl[websocket]==0.7.1",
"samsungtvws[async,encrypted]==2.7.1",
"samsungtvws[async,encrypted]==2.7.2",
"wakeonlan==2.1.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.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
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_INTENSITY: set(UnitOfVolumetricFlux),
SensorDeviceClass.PRESSURE: set(UnitOfPressure),

View File

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

View File

@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/tesla_fleet",
"iot_class": "cloud_polling",
"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",
"iot_class": "cloud_polling",
"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",
"iot_class": "cloud_polling",
"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",
"iot_class": "local_polling",
"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",
"iot_class": "local_push",
"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."""
from datetime import timedelta
from datetime import datetime, timedelta
import logging
from croniter import croniter
from cronsim import CronSim, CronSimError
import voluptuous as vol
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
@ -47,9 +47,12 @@ DEFAULT_OFFSET = timedelta(hours=0)
def validate_cron_pattern(pattern):
"""Check that the pattern is well-formed."""
if croniter.is_valid(pattern):
try:
CronSim(pattern, datetime(2020, 1, 1)) # any date will do
except CronSimError as err:
_LOGGER.error("Invalid cron pattern %s: %s", pattern, err)
raise vol.Invalid("Invalid pattern") from err
return pattern
raise vol.Invalid("Invalid pattern")
def period_or_cron(config):

View File

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

View File

@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 12
PATCH_VERSION: Final = "0"
PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
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:
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
if platform.system() == "Darwin":

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
from unittest.mock import MagicMock
from pydeako.deako import DeviceListTimeout, FindDevicesTimeout
from pydeako import FindDevicesError
from homeassistant.config_entries import ConfigEntryState
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
async def test_deako_async_setup_entry_device_list_timeout(
async def test_deako_async_setup_entry_devices_error(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
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)
pydeako_deako_mock.return_value.find_devices.side_effect = DeviceListTimeout()
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()
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()

View File

@ -1,6 +1,7 @@
"""Configuration for Elmax tests."""
from collections.abc import Generator
from datetime import datetime, timedelta
import json
from unittest.mock import AsyncMock, patch
@ -11,6 +12,7 @@ from elmax_api.constants import (
ENDPOINT_LOGIN,
)
from httpx import Response
import jwt
import pytest
import respx
@ -64,9 +66,20 @@ def httpx_mock_direct_fixture() -> Generator[respx.MockRouter]:
) as respx_mock:
# Mock Login POST.
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.
list_devices_route = respx_mock.get(

View File

@ -15,7 +15,7 @@
)
# ---
# 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]
tuple(
@ -137,7 +137,7 @@
)
# ---
# 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]
tuple(
@ -166,6 +166,33 @@
# name: test_partial_update_status[api_responses0].1
'{"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]
tuple(
'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
@pytest.mark.parametrize("timezone", ["America/Regina", "UTC", "Asia/Tokyo"])
@pytest.mark.parametrize(
"api_responses",
[
@ -251,7 +252,7 @@ def mock_http_response(response_handler: list | Callable) -> Mock:
"title": "Task 1",
"status": "needsAction",
"position": "0000000000000001",
"due": "2023-11-18T00:00:00+00:00",
"due": "2023-11-18T00:00:00Z",
},
{
"id": "task-2",
@ -271,8 +272,10 @@ async def test_get_items(
integration_setup: Callable[[], Awaitable[bool]],
hass_ws_client: WebSocketGenerator,
ws_get_items: Callable[[], Awaitable[dict[str, str]]],
timezone: str,
) -> None:
"""Test getting todo list items."""
await hass.config.async_set_time_zone(timezone)
assert await integration_setup()
@ -484,6 +487,39 @@ async def test_update_todo_list_item(
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(
"api_responses",
[

View File

@ -165,6 +165,7 @@
'openid',
'offline_access',
'vehicle_device_data',
'vehicle_location',
'vehicle_cmds',
'vehicle_charging_cmds',
'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"
async def test_getuser_keyerror(hass: HomeAssistant) -> None:
"""Test getuser keyerror."""
with patch(
"homeassistant.helpers.system_info.cached_get_user", side_effect=KeyError
):
@pytest.mark.parametrize("error", [KeyError, OSError])
async def test_getuser_oserror(hass: HomeAssistant, error: Exception) -> None:
"""Test getuser oserror."""
with patch("homeassistant.helpers.system_info.cached_get_user", side_effect=error):
info = await async_get_system_info(hass)
assert info["user"] is None