mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
2024.5.1 (#116696)
This commit is contained in:
commit
ab8a811c9f
@ -939,6 +939,7 @@ omit =
|
||||
homeassistant/components/omnilogic/switch.py
|
||||
homeassistant/components/ondilo_ico/__init__.py
|
||||
homeassistant/components/ondilo_ico/api.py
|
||||
homeassistant/components/ondilo_ico/coordinator.py
|
||||
homeassistant/components/ondilo_ico/sensor.py
|
||||
homeassistant/components/onkyo/media_player.py
|
||||
homeassistant/components/onvif/__init__.py
|
||||
|
@ -24,5 +24,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["airthings-ble==0.7.1"]
|
||||
"requirements": ["airthings-ble==0.8.0"]
|
||||
}
|
||||
|
@ -225,7 +225,7 @@ class AirthingsSensor(
|
||||
manufacturer=airthings_device.manufacturer,
|
||||
hw_version=airthings_device.hw_version,
|
||||
sw_version=airthings_device.sw_version,
|
||||
model=airthings_device.model.name,
|
||||
model=airthings_device.model.product_name,
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -20,6 +20,6 @@
|
||||
"bluetooth-auto-recovery==1.4.2",
|
||||
"bluetooth-data-tools==1.19.0",
|
||||
"dbus-fast==2.21.1",
|
||||
"habluetooth==2.8.0"
|
||||
"habluetooth==2.8.1"
|
||||
]
|
||||
}
|
||||
|
@ -373,8 +373,11 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity):
|
||||
start_time = dt_util.utcnow()
|
||||
while (dt_util.utcnow() - start_time) < LEARNING_TIMEOUT:
|
||||
await asyncio.sleep(1)
|
||||
found = await device.async_request(device.api.check_frequency)[0]
|
||||
if found:
|
||||
is_found, frequency = await device.async_request(
|
||||
device.api.check_frequency
|
||||
)
|
||||
if is_found:
|
||||
_LOGGER.info("Radiofrequency detected: %s MHz", frequency)
|
||||
break
|
||||
else:
|
||||
await device.async_request(device.api.cancel_sweep_frequency)
|
||||
|
@ -15,5 +15,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/elkm1",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["elkm1_lib"],
|
||||
"requirements": ["elkm1-lib==2.2.6"]
|
||||
"requirements": ["elkm1-lib==2.2.7"]
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["env_canada"],
|
||||
"requirements": ["env-canada==0.6.0"]
|
||||
"requirements": ["env-canada==0.6.2"]
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ from homeassistant.components.light import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@ -94,7 +94,7 @@ class GoveeLight(CoordinatorEntity[GoveeLocalApiCoordinator], LightEntity):
|
||||
name=device.sku,
|
||||
manufacturer=MANUFACTURER,
|
||||
model=device.sku,
|
||||
connections={(CONNECTION_NETWORK_MAC, device.fingerprint)},
|
||||
serial_number=device.fingerprint,
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -6,5 +6,5 @@
|
||||
"dependencies": ["network"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["govee-local-api==1.4.4"]
|
||||
"requirements": ["govee-local-api==1.4.5"]
|
||||
}
|
||||
|
@ -153,6 +153,7 @@ class HKDevice:
|
||||
self._subscriptions: dict[tuple[int, int], set[CALLBACK_TYPE]] = {}
|
||||
self._pending_subscribes: set[tuple[int, int]] = set()
|
||||
self._subscribe_timer: CALLBACK_TYPE | None = None
|
||||
self._load_platforms_lock = asyncio.Lock()
|
||||
|
||||
@property
|
||||
def entity_map(self) -> Accessories:
|
||||
@ -327,7 +328,8 @@ class HKDevice:
|
||||
)
|
||||
# BLE devices always get an RSSI sensor as well
|
||||
if "sensor" not in self.platforms:
|
||||
await self._async_load_platforms({"sensor"})
|
||||
async with self._load_platforms_lock:
|
||||
await self._async_load_platforms({"sensor"})
|
||||
|
||||
@callback
|
||||
def _async_start_polling(self) -> None:
|
||||
@ -804,6 +806,7 @@ class HKDevice:
|
||||
|
||||
async def _async_load_platforms(self, platforms: set[str]) -> None:
|
||||
"""Load a group of platforms."""
|
||||
assert self._load_platforms_lock.locked(), "Must be called with lock held"
|
||||
if not (to_load := platforms - self.platforms):
|
||||
return
|
||||
self.platforms.update(to_load)
|
||||
@ -813,22 +816,23 @@ class HKDevice:
|
||||
|
||||
async def async_load_platforms(self) -> None:
|
||||
"""Load any platforms needed by this HomeKit device."""
|
||||
to_load: set[str] = set()
|
||||
for accessory in self.entity_map.accessories:
|
||||
for service in accessory.services:
|
||||
if service.type in HOMEKIT_ACCESSORY_DISPATCH:
|
||||
platform = HOMEKIT_ACCESSORY_DISPATCH[service.type]
|
||||
if platform not in self.platforms:
|
||||
to_load.add(platform)
|
||||
|
||||
for char in service.characteristics:
|
||||
if char.type in CHARACTERISTIC_PLATFORMS:
|
||||
platform = CHARACTERISTIC_PLATFORMS[char.type]
|
||||
async with self._load_platforms_lock:
|
||||
to_load: set[str] = set()
|
||||
for accessory in self.entity_map.accessories:
|
||||
for service in accessory.services:
|
||||
if service.type in HOMEKIT_ACCESSORY_DISPATCH:
|
||||
platform = HOMEKIT_ACCESSORY_DISPATCH[service.type]
|
||||
if platform not in self.platforms:
|
||||
to_load.add(platform)
|
||||
|
||||
if to_load:
|
||||
await self._async_load_platforms(to_load)
|
||||
for char in service.characteristics:
|
||||
if char.type in CHARACTERISTIC_PLATFORMS:
|
||||
platform = CHARACTERISTIC_PLATFORMS[char.type]
|
||||
if platform not in self.platforms:
|
||||
to_load.add(platform)
|
||||
|
||||
if to_load:
|
||||
await self._async_load_platforms(to_load)
|
||||
|
||||
@callback
|
||||
def async_update_available_state(self, *_: Any) -> None:
|
||||
|
@ -97,7 +97,7 @@ def _create_notification_instance(xknx: XKNX, config: ConfigType) -> XknxNotific
|
||||
)
|
||||
|
||||
|
||||
class KNXNotify(NotifyEntity, KnxEntity):
|
||||
class KNXNotify(KnxEntity, NotifyEntity):
|
||||
"""Representation of a KNX notification entity."""
|
||||
|
||||
_device: XknxNotification
|
||||
|
@ -398,6 +398,8 @@ class MatterLight(MatterEntity, LightEntity):
|
||||
def _check_transition_blocklist(self) -> None:
|
||||
"""Check if this device is reported to have non working transitions."""
|
||||
device_info = self._endpoint.device_info
|
||||
if isinstance(device_info, clusters.BridgedDeviceBasicInformation):
|
||||
return
|
||||
if (
|
||||
device_info.vendorID,
|
||||
device_info.productID,
|
||||
|
@ -83,7 +83,7 @@ if TYPE_CHECKING:
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DISCOVERY_COOLDOWN = 2
|
||||
DISCOVERY_COOLDOWN = 5
|
||||
INITIAL_SUBSCRIBE_COOLDOWN = 1.0
|
||||
SUBSCRIBE_COOLDOWN = 0.1
|
||||
UNSUBSCRIBE_COOLDOWN = 0.1
|
||||
@ -349,6 +349,12 @@ class EnsureJobAfterCooldown:
|
||||
self._task = create_eager_task(self._async_job())
|
||||
self._task.add_done_callback(self._async_task_done)
|
||||
|
||||
async def async_fire(self) -> None:
|
||||
"""Execute the job immediately."""
|
||||
if self._task:
|
||||
await self._task
|
||||
self._async_execute()
|
||||
|
||||
@callback
|
||||
def _async_cancel_timer(self) -> None:
|
||||
"""Cancel any pending task."""
|
||||
@ -846,7 +852,7 @@ class MQTT:
|
||||
|
||||
for topic, qos in subscriptions.items():
|
||||
_LOGGER.debug("Subscribing to %s, mid: %s, qos: %s", topic, mid, qos)
|
||||
self._last_subscribe = time.time()
|
||||
self._last_subscribe = time.monotonic()
|
||||
|
||||
if result == 0:
|
||||
await self._wait_for_mid(mid)
|
||||
@ -876,6 +882,8 @@ class MQTT:
|
||||
await self._ha_started.wait() # Wait for Home Assistant to start
|
||||
await self._discovery_cooldown() # Wait for MQTT discovery to cool down
|
||||
# Update subscribe cooldown period to a shorter time
|
||||
# and make sure we flush the debouncer
|
||||
await self._subscribe_debouncer.async_fire()
|
||||
self._subscribe_debouncer.set_timeout(SUBSCRIBE_COOLDOWN)
|
||||
await self.async_publish(
|
||||
topic=birth_message.topic,
|
||||
@ -1121,7 +1129,7 @@ class MQTT:
|
||||
|
||||
async def _discovery_cooldown(self) -> None:
|
||||
"""Wait until all discovery and subscriptions are processed."""
|
||||
now = time.time()
|
||||
now = time.monotonic()
|
||||
# Reset discovery and subscribe cooldowns
|
||||
self._mqtt_data.last_discovery = now
|
||||
self._last_subscribe = now
|
||||
@ -1133,7 +1141,7 @@ class MQTT:
|
||||
)
|
||||
while now < wait_until:
|
||||
await asyncio.sleep(wait_until - now)
|
||||
now = time.time()
|
||||
now = time.monotonic()
|
||||
last_discovery = self._mqtt_data.last_discovery
|
||||
last_subscribe = (
|
||||
now if self._pending_subscriptions else self._last_subscribe
|
||||
|
@ -177,7 +177,7 @@ async def async_start( # noqa: C901
|
||||
@callback
|
||||
def async_discovery_message_received(msg: ReceiveMessage) -> None: # noqa: C901
|
||||
"""Process the received message."""
|
||||
mqtt_data.last_discovery = time.time()
|
||||
mqtt_data.last_discovery = time.monotonic()
|
||||
payload = msg.payload
|
||||
topic = msg.topic
|
||||
topic_trimmed = topic.replace(f"{discovery_topic}/", "", 1)
|
||||
@ -370,7 +370,7 @@ async def async_start( # noqa: C901
|
||||
)
|
||||
)
|
||||
|
||||
mqtt_data.last_discovery = time.time()
|
||||
mqtt_data.last_discovery = time.monotonic()
|
||||
mqtt_integrations = await async_get_mqtt(hass)
|
||||
|
||||
for integration, topics in mqtt_integrations.items():
|
||||
|
@ -2,21 +2,18 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
import datetime
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pynws import SimpleNWS
|
||||
from pynws import SimpleNWS, call_with_retry
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import debounce
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
@ -27,8 +24,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||
|
||||
DEFAULT_SCAN_INTERVAL = datetime.timedelta(minutes=10)
|
||||
FAILED_SCAN_INTERVAL = datetime.timedelta(minutes=1)
|
||||
DEBOUNCE_TIME = 60 # in seconds
|
||||
RETRY_INTERVAL = datetime.timedelta(minutes=1)
|
||||
RETRY_STOP = datetime.timedelta(minutes=10)
|
||||
|
||||
DEBOUNCE_TIME = 10 * 60 # in seconds
|
||||
|
||||
|
||||
def base_unique_id(latitude: float, longitude: float) -> str:
|
||||
@ -41,62 +40,9 @@ class NWSData:
|
||||
"""Data for the National Weather Service integration."""
|
||||
|
||||
api: SimpleNWS
|
||||
coordinator_observation: NwsDataUpdateCoordinator
|
||||
coordinator_forecast: NwsDataUpdateCoordinator
|
||||
coordinator_forecast_hourly: NwsDataUpdateCoordinator
|
||||
|
||||
|
||||
class NwsDataUpdateCoordinator(TimestampDataUpdateCoordinator[None]): # pylint: disable=hass-enforce-coordinator-module
|
||||
"""NWS data update coordinator.
|
||||
|
||||
Implements faster data update intervals for failed updates and exposes a last successful update time.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
logger: logging.Logger,
|
||||
*,
|
||||
name: str,
|
||||
update_interval: datetime.timedelta,
|
||||
failed_update_interval: datetime.timedelta,
|
||||
update_method: Callable[[], Awaitable[None]] | None = None,
|
||||
request_refresh_debouncer: debounce.Debouncer | None = None,
|
||||
) -> None:
|
||||
"""Initialize NWS coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
logger,
|
||||
name=name,
|
||||
update_interval=update_interval,
|
||||
update_method=update_method,
|
||||
request_refresh_debouncer=request_refresh_debouncer,
|
||||
)
|
||||
self.failed_update_interval = failed_update_interval
|
||||
|
||||
@callback
|
||||
def _schedule_refresh(self) -> None:
|
||||
"""Schedule a refresh."""
|
||||
if self._unsub_refresh:
|
||||
self._unsub_refresh()
|
||||
self._unsub_refresh = None
|
||||
|
||||
# We _floor_ utcnow to create a schedule on a rounded second,
|
||||
# minimizing the time between the point and the real activation.
|
||||
# That way we obtain a constant update frequency,
|
||||
# as long as the update process takes less than a second
|
||||
if self.last_update_success:
|
||||
if TYPE_CHECKING:
|
||||
# the base class allows None, but this one doesn't
|
||||
assert self.update_interval is not None
|
||||
update_interval = self.update_interval
|
||||
else:
|
||||
update_interval = self.failed_update_interval
|
||||
self._unsub_refresh = async_track_point_in_utc_time(
|
||||
self.hass,
|
||||
self._handle_refresh_interval,
|
||||
utcnow().replace(microsecond=0) + update_interval,
|
||||
)
|
||||
coordinator_observation: TimestampDataUpdateCoordinator[None]
|
||||
coordinator_forecast: TimestampDataUpdateCoordinator[None]
|
||||
coordinator_forecast_hourly: TimestampDataUpdateCoordinator[None]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@ -114,39 +60,57 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def update_observation() -> None:
|
||||
"""Retrieve recent observations."""
|
||||
await nws_data.update_observation(start_time=utcnow() - UPDATE_TIME_PERIOD)
|
||||
await call_with_retry(
|
||||
nws_data.update_observation,
|
||||
RETRY_INTERVAL,
|
||||
RETRY_STOP,
|
||||
start_time=utcnow() - UPDATE_TIME_PERIOD,
|
||||
)
|
||||
|
||||
coordinator_observation = NwsDataUpdateCoordinator(
|
||||
async def update_forecast() -> None:
|
||||
"""Retrieve twice-daily forecsat."""
|
||||
await call_with_retry(
|
||||
nws_data.update_forecast,
|
||||
RETRY_INTERVAL,
|
||||
RETRY_STOP,
|
||||
)
|
||||
|
||||
async def update_forecast_hourly() -> None:
|
||||
"""Retrieve hourly forecast."""
|
||||
await call_with_retry(
|
||||
nws_data.update_forecast_hourly,
|
||||
RETRY_INTERVAL,
|
||||
RETRY_STOP,
|
||||
)
|
||||
|
||||
coordinator_observation = TimestampDataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=f"NWS observation station {station}",
|
||||
update_method=update_observation,
|
||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||
failed_update_interval=FAILED_SCAN_INTERVAL,
|
||||
request_refresh_debouncer=debounce.Debouncer(
|
||||
hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True
|
||||
),
|
||||
)
|
||||
|
||||
coordinator_forecast = NwsDataUpdateCoordinator(
|
||||
coordinator_forecast = TimestampDataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=f"NWS forecast station {station}",
|
||||
update_method=nws_data.update_forecast,
|
||||
update_method=update_forecast,
|
||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||
failed_update_interval=FAILED_SCAN_INTERVAL,
|
||||
request_refresh_debouncer=debounce.Debouncer(
|
||||
hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True
|
||||
),
|
||||
)
|
||||
|
||||
coordinator_forecast_hourly = NwsDataUpdateCoordinator(
|
||||
coordinator_forecast_hourly = TimestampDataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=f"NWS forecast hourly station {station}",
|
||||
update_method=nws_data.update_forecast_hourly,
|
||||
update_method=update_forecast_hourly,
|
||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||
failed_update_interval=FAILED_SCAN_INTERVAL,
|
||||
request_refresh_debouncer=debounce.Debouncer(
|
||||
hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True
|
||||
),
|
||||
|
@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["metar", "pynws"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pynws==1.6.0"]
|
||||
"requirements": ["pynws[retry]==1.7.0"]
|
||||
}
|
||||
|
@ -25,7 +25,10 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
TimestampDataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.util.unit_conversion import (
|
||||
DistanceConverter,
|
||||
@ -34,7 +37,7 @@ from homeassistant.util.unit_conversion import (
|
||||
)
|
||||
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
|
||||
|
||||
from . import NWSData, NwsDataUpdateCoordinator, base_unique_id, device_info
|
||||
from . import NWSData, base_unique_id, device_info
|
||||
from .const import ATTRIBUTION, CONF_STATION, DOMAIN, OBSERVATION_VALID_TIME
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
@ -158,7 +161,7 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class NWSSensor(CoordinatorEntity[NwsDataUpdateCoordinator], SensorEntity):
|
||||
class NWSSensor(CoordinatorEntity[TimestampDataUpdateCoordinator[None]], SensorEntity):
|
||||
"""An NWS Sensor Entity."""
|
||||
|
||||
entity_description: NWSSensorEntityDescription
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
@ -34,7 +35,6 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.util.unit_conversion import SpeedConverter, TemperatureConverter
|
||||
|
||||
from . import NWSData, base_unique_id, device_info
|
||||
@ -46,7 +46,6 @@ from .const import (
|
||||
DOMAIN,
|
||||
FORECAST_VALID_TIME,
|
||||
HOURLY,
|
||||
OBSERVATION_VALID_TIME,
|
||||
)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
@ -140,96 +139,69 @@ class NWSWeather(CoordinatorWeatherEntity):
|
||||
self.nws = nws_data.api
|
||||
latitude = entry_data[CONF_LATITUDE]
|
||||
longitude = entry_data[CONF_LONGITUDE]
|
||||
self.coordinator_forecast_legacy = nws_data.coordinator_forecast
|
||||
self.station = self.nws.station
|
||||
|
||||
self.observation: dict[str, Any] | None = None
|
||||
self._forecast_hourly: list[dict[str, Any]] | None = None
|
||||
self._forecast_legacy: list[dict[str, Any]] | None = None
|
||||
self._forecast_twice_daily: list[dict[str, Any]] | None = None
|
||||
self.station = self.nws.station
|
||||
|
||||
self._attr_unique_id = _calculate_unique_id(entry_data, DAYNIGHT)
|
||||
self._attr_device_info = device_info(latitude, longitude)
|
||||
self._attr_name = self.station
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Set up a listener and load data."""
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
self.coordinator_forecast_legacy.async_add_listener(
|
||||
self._handle_legacy_forecast_coordinator_update
|
||||
self.async_on_remove(partial(self._remove_forecast_listener, "daily"))
|
||||
self.async_on_remove(partial(self._remove_forecast_listener, "hourly"))
|
||||
self.async_on_remove(partial(self._remove_forecast_listener, "twice_daily"))
|
||||
|
||||
for forecast_type in ("twice_daily", "hourly"):
|
||||
if (coordinator := self.forecast_coordinators[forecast_type]) is None:
|
||||
continue
|
||||
self.unsub_forecast[forecast_type] = coordinator.async_add_listener(
|
||||
partial(self._handle_forecast_update, forecast_type)
|
||||
)
|
||||
)
|
||||
# Load initial data from coordinators
|
||||
self._handle_coordinator_update()
|
||||
self._handle_hourly_forecast_coordinator_update()
|
||||
self._handle_twice_daily_forecast_coordinator_update()
|
||||
self._handle_legacy_forecast_coordinator_update()
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Load data from integration."""
|
||||
self.observation = self.nws.observation
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _handle_hourly_forecast_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the hourly forecast coordinator."""
|
||||
self._forecast_hourly = self.nws.forecast_hourly
|
||||
|
||||
@callback
|
||||
def _handle_twice_daily_forecast_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the twice daily forecast coordinator."""
|
||||
self._forecast_twice_daily = self.nws.forecast
|
||||
|
||||
@callback
|
||||
def _handle_legacy_forecast_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the legacy forecast coordinator."""
|
||||
self._forecast_legacy = self.nws.forecast
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def native_temperature(self) -> float | None:
|
||||
"""Return the current temperature."""
|
||||
if self.observation:
|
||||
return self.observation.get("temperature")
|
||||
if observation := self.nws.observation:
|
||||
return observation.get("temperature")
|
||||
return None
|
||||
|
||||
@property
|
||||
def native_pressure(self) -> int | None:
|
||||
"""Return the current pressure."""
|
||||
if self.observation:
|
||||
return self.observation.get("seaLevelPressure")
|
||||
if observation := self.nws.observation:
|
||||
return observation.get("seaLevelPressure")
|
||||
return None
|
||||
|
||||
@property
|
||||
def humidity(self) -> float | None:
|
||||
"""Return the name of the sensor."""
|
||||
if self.observation:
|
||||
return self.observation.get("relativeHumidity")
|
||||
if observation := self.nws.observation:
|
||||
return observation.get("relativeHumidity")
|
||||
return None
|
||||
|
||||
@property
|
||||
def native_wind_speed(self) -> float | None:
|
||||
"""Return the current windspeed."""
|
||||
if self.observation:
|
||||
return self.observation.get("windSpeed")
|
||||
if observation := self.nws.observation:
|
||||
return observation.get("windSpeed")
|
||||
return None
|
||||
|
||||
@property
|
||||
def wind_bearing(self) -> int | None:
|
||||
"""Return the current wind bearing (degrees)."""
|
||||
if self.observation:
|
||||
return self.observation.get("windDirection")
|
||||
if observation := self.nws.observation:
|
||||
return observation.get("windDirection")
|
||||
return None
|
||||
|
||||
@property
|
||||
def condition(self) -> str | None:
|
||||
"""Return current condition."""
|
||||
weather = None
|
||||
if self.observation:
|
||||
weather = self.observation.get("iconWeather")
|
||||
time = cast(str, self.observation.get("iconTime"))
|
||||
if observation := self.nws.observation:
|
||||
weather = observation.get("iconWeather")
|
||||
time = cast(str, observation.get("iconTime"))
|
||||
|
||||
if weather:
|
||||
return convert_condition(time, weather)
|
||||
@ -238,8 +210,8 @@ class NWSWeather(CoordinatorWeatherEntity):
|
||||
@property
|
||||
def native_visibility(self) -> int | None:
|
||||
"""Return visibility."""
|
||||
if self.observation:
|
||||
return self.observation.get("visibility")
|
||||
if observation := self.nws.observation:
|
||||
return observation.get("visibility")
|
||||
return None
|
||||
|
||||
def _forecast(
|
||||
@ -302,33 +274,12 @@ class NWSWeather(CoordinatorWeatherEntity):
|
||||
@callback
|
||||
def _async_forecast_hourly(self) -> list[Forecast] | None:
|
||||
"""Return the hourly forecast in native units."""
|
||||
return self._forecast(self._forecast_hourly, HOURLY)
|
||||
return self._forecast(self.nws.forecast_hourly, HOURLY)
|
||||
|
||||
@callback
|
||||
def _async_forecast_twice_daily(self) -> list[Forecast] | None:
|
||||
"""Return the twice daily forecast in native units."""
|
||||
return self._forecast(self._forecast_twice_daily, DAYNIGHT)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if state is available."""
|
||||
last_success = (
|
||||
self.coordinator.last_update_success
|
||||
and self.coordinator_forecast_legacy.last_update_success
|
||||
)
|
||||
if (
|
||||
self.coordinator.last_update_success_time
|
||||
and self.coordinator_forecast_legacy.last_update_success_time
|
||||
):
|
||||
last_success_time = (
|
||||
utcnow() - self.coordinator.last_update_success_time
|
||||
< OBSERVATION_VALID_TIME
|
||||
and utcnow() - self.coordinator_forecast_legacy.last_update_success_time
|
||||
< FORECAST_VALID_TIME
|
||||
)
|
||||
else:
|
||||
last_success_time = False
|
||||
return last_success or last_success_time
|
||||
return self._forecast(self.nws.forecast, DAYNIGHT)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the entity.
|
||||
@ -336,4 +287,7 @@ class NWSWeather(CoordinatorWeatherEntity):
|
||||
Only used by the generic entity update service.
|
||||
"""
|
||||
await self.coordinator.async_request_refresh()
|
||||
await self.coordinator_forecast_legacy.async_request_refresh()
|
||||
|
||||
for forecast_type in ("twice_daily", "hourly"):
|
||||
if (coordinator := self.forecast_coordinators[forecast_type]) is not None:
|
||||
await coordinator.async_request_refresh()
|
||||
|
@ -7,6 +7,7 @@ from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
from . import api, config_flow
|
||||
from .const import DOMAIN
|
||||
from .coordinator import OndiloIcoCoordinator
|
||||
from .oauth_impl import OndiloOauth2Implementation
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
@ -26,8 +27,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = api.OndiloClient(hass, entry, implementation)
|
||||
coordinator = OndiloIcoCoordinator(
|
||||
hass, api.OndiloClient(hass, entry, implementation)
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
|
37
homeassistant/components/ondilo_ico/coordinator.py
Normal file
37
homeassistant/components/ondilo_ico/coordinator.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""Define an object to coordinate fetching Ondilo ICO data."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from ondilo import OndiloError
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from . import DOMAIN
|
||||
from .api import OndiloClient
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OndiloIcoCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
|
||||
"""Class to manage fetching Ondilo ICO data from API."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, api: OndiloClient) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
hass,
|
||||
logger=_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(minutes=5),
|
||||
)
|
||||
self.api = api
|
||||
|
||||
async def _async_update_data(self) -> list[dict[str, Any]]:
|
||||
"""Fetch data from API endpoint."""
|
||||
try:
|
||||
return await self.hass.async_add_executor_job(self.api.get_all_pools_data)
|
||||
|
||||
except OndiloError as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
@ -2,12 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from ondilo import OndiloError
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
@ -24,14 +18,10 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
UpdateFailed,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .api import OndiloClient
|
||||
from .const import DOMAIN
|
||||
from .coordinator import OndiloIcoCoordinator
|
||||
|
||||
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
@ -78,66 +68,30 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
)
|
||||
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the Ondilo ICO sensors."""
|
||||
|
||||
api: OndiloClient = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator: OndiloIcoCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async def async_update_data() -> list[dict[str, Any]]:
|
||||
"""Fetch data from API endpoint.
|
||||
|
||||
This is the place to pre-process the data to lookup tables
|
||||
so entities can quickly look up their data.
|
||||
"""
|
||||
try:
|
||||
return await hass.async_add_executor_job(api.get_all_pools_data)
|
||||
|
||||
except OndiloError as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
# Name of the data. For logging purposes.
|
||||
name="sensor",
|
||||
update_method=async_update_data,
|
||||
# Polling interval. Will only be polled if there are subscribers.
|
||||
update_interval=SCAN_INTERVAL,
|
||||
async_add_entities(
|
||||
OndiloICO(coordinator, poolidx, description)
|
||||
for poolidx, pool in enumerate(coordinator.data)
|
||||
for sensor in pool["sensors"]
|
||||
for description in SENSOR_TYPES
|
||||
if description.key == sensor["data_type"]
|
||||
)
|
||||
|
||||
# Fetch initial data so we have data when entities subscribe
|
||||
await coordinator.async_refresh()
|
||||
|
||||
entities = []
|
||||
for poolidx, pool in enumerate(coordinator.data):
|
||||
entities.extend(
|
||||
[
|
||||
OndiloICO(coordinator, poolidx, description)
|
||||
for sensor in pool["sensors"]
|
||||
for description in SENSOR_TYPES
|
||||
if description.key == sensor["data_type"]
|
||||
]
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class OndiloICO(
|
||||
CoordinatorEntity[DataUpdateCoordinator[list[dict[str, Any]]]], SensorEntity
|
||||
):
|
||||
class OndiloICO(CoordinatorEntity[OndiloIcoCoordinator], SensorEntity):
|
||||
"""Representation of a Sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator[list[dict[str, Any]]],
|
||||
coordinator: OndiloIcoCoordinator,
|
||||
poolidx: int,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opentherm_gw",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyotgw"],
|
||||
"requirements": ["pyotgw==2.1.3"]
|
||||
"requirements": ["pyotgw==2.2.0"]
|
||||
}
|
||||
|
@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/sanix",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["sanix==1.0.5"]
|
||||
"requirements": ["sanix==1.0.6"]
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiounifi"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiounifi==76"],
|
||||
"requirements": ["aiounifi==77"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/upb",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["upb_lib"],
|
||||
"requirements": ["upb-lib==0.5.4"]
|
||||
"requirements": ["upb-lib==0.5.6"]
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/waze_travel_time",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pywaze", "homeassistant.helpers.location"],
|
||||
"requirements": ["pywaze==1.0.0"]
|
||||
"requirements": ["pywaze==1.0.1"]
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
"universal_silabs_flasher"
|
||||
],
|
||||
"requirements": [
|
||||
"bellows==0.38.3",
|
||||
"bellows==0.38.4",
|
||||
"pyserial==3.5",
|
||||
"pyserial-asyncio==0.6",
|
||||
"zha-quirks==0.0.115",
|
||||
|
@ -23,7 +23,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2024
|
||||
MINOR_VERSION: Final = 5
|
||||
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)
|
||||
|
@ -90,7 +90,12 @@ class BlockedIntegration:
|
||||
|
||||
BLOCKED_CUSTOM_INTEGRATIONS: dict[str, BlockedIntegration] = {
|
||||
# Added in 2024.3.0 because of https://github.com/home-assistant/core/issues/112464
|
||||
"start_time": BlockedIntegration(AwesomeVersion("1.1.7"), "breaks Home Assistant")
|
||||
"start_time": BlockedIntegration(AwesomeVersion("1.1.7"), "breaks Home Assistant"),
|
||||
# Added in 2024.5.1 because of
|
||||
# https://community.home-assistant.io/t/psa-2024-5-upgrade-failure-and-dreame-vacuum-custom-integration/724612
|
||||
"dreame_vacuum": BlockedIntegration(
|
||||
AwesomeVersion("1.0.4"), "crashes Home Assistant"
|
||||
),
|
||||
}
|
||||
|
||||
DATA_COMPONENTS = "components"
|
||||
|
@ -28,7 +28,7 @@ dbus-fast==2.21.1
|
||||
fnv-hash-fast==0.5.0
|
||||
ha-av==10.1.1
|
||||
ha-ffmpeg==3.2.0
|
||||
habluetooth==2.8.0
|
||||
habluetooth==2.8.1
|
||||
hass-nabucasa==0.78.0
|
||||
hassil==1.6.1
|
||||
home-assistant-bluetooth==1.12.0
|
||||
@ -192,3 +192,8 @@ pycountry>=23.12.11
|
||||
|
||||
# scapy<2.5.0 will not work with python3.12
|
||||
scapy>=2.5.0
|
||||
|
||||
# tuf isn't updated to deal with breaking changes in securesystemslib==1.0.
|
||||
# Only tuf>=4 includes a constraint to <1.0.
|
||||
# https://github.com/theupdateframework/python-tuf/releases/tag/v4.0.0
|
||||
tuf>=4.0.0
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2024.5.0"
|
||||
version = "2024.5.1"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
@ -386,7 +386,7 @@ aiotankerkoenig==0.4.1
|
||||
aiotractive==0.5.6
|
||||
|
||||
# homeassistant.components.unifi
|
||||
aiounifi==76
|
||||
aiounifi==77
|
||||
|
||||
# homeassistant.components.vlc_telnet
|
||||
aiovlc==0.1.0
|
||||
@ -413,7 +413,7 @@ aioymaps==1.2.2
|
||||
airly==1.1.0
|
||||
|
||||
# homeassistant.components.airthings_ble
|
||||
airthings-ble==0.7.1
|
||||
airthings-ble==0.8.0
|
||||
|
||||
# homeassistant.components.airthings
|
||||
airthings-cloud==0.2.0
|
||||
@ -541,7 +541,7 @@ beautifulsoup4==4.12.3
|
||||
# beewi-smartclim==0.0.10
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows==0.38.3
|
||||
bellows==0.38.4
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer-connected[china]==0.15.2
|
||||
@ -777,7 +777,7 @@ elgato==5.1.2
|
||||
eliqonline==1.2.2
|
||||
|
||||
# homeassistant.components.elkm1
|
||||
elkm1-lib==2.2.6
|
||||
elkm1-lib==2.2.7
|
||||
|
||||
# homeassistant.components.elmax
|
||||
elmax-api==0.0.4
|
||||
@ -804,7 +804,7 @@ enocean==0.50
|
||||
enturclient==0.2.4
|
||||
|
||||
# homeassistant.components.environment_canada
|
||||
env-canada==0.6.0
|
||||
env-canada==0.6.2
|
||||
|
||||
# homeassistant.components.season
|
||||
ephem==4.1.5
|
||||
@ -983,7 +983,7 @@ gotailwind==0.2.2
|
||||
govee-ble==0.31.2
|
||||
|
||||
# homeassistant.components.govee_light_local
|
||||
govee-local-api==1.4.4
|
||||
govee-local-api==1.4.5
|
||||
|
||||
# homeassistant.components.remote_rpi_gpio
|
||||
gpiozero==1.6.2
|
||||
@ -1035,7 +1035,7 @@ ha-philipsjs==3.1.1
|
||||
habitipy==0.2.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
habluetooth==2.8.0
|
||||
habluetooth==2.8.1
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==0.78.0
|
||||
@ -2001,7 +2001,7 @@ pynobo==1.8.1
|
||||
pynuki==1.6.3
|
||||
|
||||
# homeassistant.components.nws
|
||||
pynws==1.6.0
|
||||
pynws[retry]==1.7.0
|
||||
|
||||
# homeassistant.components.nx584
|
||||
pynx584==0.5
|
||||
@ -2031,7 +2031,7 @@ pyoppleio-legacy==1.0.8
|
||||
pyosoenergyapi==1.1.3
|
||||
|
||||
# homeassistant.components.opentherm_gw
|
||||
pyotgw==2.1.3
|
||||
pyotgw==2.2.0
|
||||
|
||||
# homeassistant.auth.mfa_modules.notify
|
||||
# homeassistant.auth.mfa_modules.totp
|
||||
@ -2370,7 +2370,7 @@ pyvlx==0.2.21
|
||||
pyvolumio==0.1.5
|
||||
|
||||
# homeassistant.components.waze_travel_time
|
||||
pywaze==1.0.0
|
||||
pywaze==1.0.1
|
||||
|
||||
# homeassistant.components.weatherflow
|
||||
pyweatherflowudp==1.4.5
|
||||
@ -2499,7 +2499,7 @@ samsungctl[websocket]==0.7.1
|
||||
samsungtvws[async,encrypted]==2.6.0
|
||||
|
||||
# homeassistant.components.sanix
|
||||
sanix==1.0.5
|
||||
sanix==1.0.6
|
||||
|
||||
# homeassistant.components.satel_integra
|
||||
satel-integra==0.3.7
|
||||
@ -2779,7 +2779,7 @@ unifiled==0.11
|
||||
universal-silabs-flasher==0.0.18
|
||||
|
||||
# homeassistant.components.upb
|
||||
upb-lib==0.5.4
|
||||
upb-lib==0.5.6
|
||||
|
||||
# homeassistant.components.upcloud
|
||||
upcloud-api==2.0.0
|
||||
|
@ -359,7 +359,7 @@ aiotankerkoenig==0.4.1
|
||||
aiotractive==0.5.6
|
||||
|
||||
# homeassistant.components.unifi
|
||||
aiounifi==76
|
||||
aiounifi==77
|
||||
|
||||
# homeassistant.components.vlc_telnet
|
||||
aiovlc==0.1.0
|
||||
@ -386,7 +386,7 @@ aioymaps==1.2.2
|
||||
airly==1.1.0
|
||||
|
||||
# homeassistant.components.airthings_ble
|
||||
airthings-ble==0.7.1
|
||||
airthings-ble==0.8.0
|
||||
|
||||
# homeassistant.components.airthings
|
||||
airthings-cloud==0.2.0
|
||||
@ -466,7 +466,7 @@ base36==0.1.1
|
||||
beautifulsoup4==4.12.3
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows==0.38.3
|
||||
bellows==0.38.4
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer-connected[china]==0.15.2
|
||||
@ -637,7 +637,7 @@ electrickiwi-api==0.8.5
|
||||
elgato==5.1.2
|
||||
|
||||
# homeassistant.components.elkm1
|
||||
elkm1-lib==2.2.6
|
||||
elkm1-lib==2.2.7
|
||||
|
||||
# homeassistant.components.elmax
|
||||
elmax-api==0.0.4
|
||||
@ -658,7 +658,7 @@ energyzero==2.1.0
|
||||
enocean==0.50
|
||||
|
||||
# homeassistant.components.environment_canada
|
||||
env-canada==0.6.0
|
||||
env-canada==0.6.2
|
||||
|
||||
# homeassistant.components.season
|
||||
ephem==4.1.5
|
||||
@ -806,7 +806,7 @@ gotailwind==0.2.2
|
||||
govee-ble==0.31.2
|
||||
|
||||
# homeassistant.components.govee_light_local
|
||||
govee-local-api==1.4.4
|
||||
govee-local-api==1.4.5
|
||||
|
||||
# homeassistant.components.gpsd
|
||||
gps3==0.33.3
|
||||
@ -849,7 +849,7 @@ ha-philipsjs==3.1.1
|
||||
habitipy==0.2.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
habluetooth==2.8.0
|
||||
habluetooth==2.8.1
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==0.78.0
|
||||
@ -1564,7 +1564,7 @@ pynobo==1.8.1
|
||||
pynuki==1.6.3
|
||||
|
||||
# homeassistant.components.nws
|
||||
pynws==1.6.0
|
||||
pynws[retry]==1.7.0
|
||||
|
||||
# homeassistant.components.nx584
|
||||
pynx584==0.5
|
||||
@ -1588,7 +1588,7 @@ pyopnsense==0.4.0
|
||||
pyosoenergyapi==1.1.3
|
||||
|
||||
# homeassistant.components.opentherm_gw
|
||||
pyotgw==2.1.3
|
||||
pyotgw==2.2.0
|
||||
|
||||
# homeassistant.auth.mfa_modules.notify
|
||||
# homeassistant.auth.mfa_modules.totp
|
||||
@ -1843,7 +1843,7 @@ pyvlx==0.2.21
|
||||
pyvolumio==0.1.5
|
||||
|
||||
# homeassistant.components.waze_travel_time
|
||||
pywaze==1.0.0
|
||||
pywaze==1.0.1
|
||||
|
||||
# homeassistant.components.weatherflow
|
||||
pyweatherflowudp==1.4.5
|
||||
@ -1939,7 +1939,7 @@ samsungctl[websocket]==0.7.1
|
||||
samsungtvws[async,encrypted]==2.6.0
|
||||
|
||||
# homeassistant.components.sanix
|
||||
sanix==1.0.5
|
||||
sanix==1.0.6
|
||||
|
||||
# homeassistant.components.screenlogic
|
||||
screenlogicpy==0.10.0
|
||||
@ -2147,7 +2147,7 @@ unifi-discovery==1.1.8
|
||||
universal-silabs-flasher==0.0.18
|
||||
|
||||
# homeassistant.components.upb
|
||||
upb-lib==0.5.4
|
||||
upb-lib==0.5.6
|
||||
|
||||
# homeassistant.components.upcloud
|
||||
upcloud-api==2.0.0
|
||||
|
@ -214,6 +214,11 @@ pycountry>=23.12.11
|
||||
|
||||
# scapy<2.5.0 will not work with python3.12
|
||||
scapy>=2.5.0
|
||||
|
||||
# tuf isn't updated to deal with breaking changes in securesystemslib==1.0.
|
||||
# Only tuf>=4 includes a constraint to <1.0.
|
||||
# https://github.com/theupdateframework/python-tuf/releases/tag/v4.0.0
|
||||
tuf>=4.0.0
|
||||
"""
|
||||
|
||||
GENERATED_MESSAGE = (
|
||||
|
@ -21,7 +21,7 @@ from tests.common import MockConfigEntry
|
||||
USERNAME = "fyta_user"
|
||||
PASSWORD = "fyta_pass"
|
||||
ACCESS_TOKEN = "123xyz"
|
||||
EXPIRATION = datetime.fromisoformat("2024-12-31T10:00:00").astimezone(UTC)
|
||||
EXPIRATION = datetime.fromisoformat("2024-12-31T10:00:00").replace(tzinfo=UTC)
|
||||
|
||||
|
||||
async def test_user_flow(
|
||||
|
@ -11,6 +11,7 @@ from .const import DEFAULT_FORECAST, DEFAULT_OBSERVATION
|
||||
@pytest.fixture
|
||||
def mock_simple_nws():
|
||||
"""Mock pynws SimpleNWS with default values."""
|
||||
|
||||
with patch("homeassistant.components.nws.SimpleNWS") as mock_nws:
|
||||
instance = mock_nws.return_value
|
||||
instance.set_station = AsyncMock(return_value=None)
|
||||
|
@ -13,7 +13,6 @@ from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
ATTR_CONDITION_SUNNY,
|
||||
DOMAIN as WEATHER_DOMAIN,
|
||||
LEGACY_SERVICE_GET_FORECAST,
|
||||
SERVICE_GET_FORECASTS,
|
||||
)
|
||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
@ -181,7 +180,7 @@ async def test_entity_refresh(hass: HomeAssistant, mock_simple_nws, no_sensor) -
|
||||
await hass.async_block_till_done()
|
||||
assert instance.update_observation.call_count == 2
|
||||
assert instance.update_forecast.call_count == 2
|
||||
instance.update_forecast_hourly.assert_called_once()
|
||||
assert instance.update_forecast_hourly.call_count == 2
|
||||
|
||||
|
||||
async def test_error_observation(
|
||||
@ -189,18 +188,8 @@ async def test_error_observation(
|
||||
) -> None:
|
||||
"""Test error during update observation."""
|
||||
utc_time = dt_util.utcnow()
|
||||
with (
|
||||
patch("homeassistant.components.nws.utcnow") as mock_utc,
|
||||
patch("homeassistant.components.nws.weather.utcnow") as mock_utc_weather,
|
||||
):
|
||||
|
||||
def increment_time(time):
|
||||
mock_utc.return_value += time
|
||||
mock_utc_weather.return_value += time
|
||||
async_fire_time_changed(hass, mock_utc.return_value)
|
||||
|
||||
with patch("homeassistant.components.nws.utcnow") as mock_utc:
|
||||
mock_utc.return_value = utc_time
|
||||
mock_utc_weather.return_value = utc_time
|
||||
instance = mock_simple_nws.return_value
|
||||
# first update fails
|
||||
instance.update_observation.side_effect = aiohttp.ClientError
|
||||
@ -219,68 +208,6 @@ async def test_error_observation(
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
# second update happens faster and succeeds
|
||||
instance.update_observation.side_effect = None
|
||||
increment_time(timedelta(minutes=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert instance.update_observation.call_count == 2
|
||||
|
||||
state = hass.states.get("weather.abc")
|
||||
assert state
|
||||
assert state.state == ATTR_CONDITION_SUNNY
|
||||
|
||||
# third udate fails, but data is cached
|
||||
instance.update_observation.side_effect = aiohttp.ClientError
|
||||
|
||||
increment_time(timedelta(minutes=10))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert instance.update_observation.call_count == 3
|
||||
|
||||
state = hass.states.get("weather.abc")
|
||||
assert state
|
||||
assert state.state == ATTR_CONDITION_SUNNY
|
||||
|
||||
# after 20 minutes data caching expires, data is no longer shown
|
||||
increment_time(timedelta(minutes=10))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("weather.abc")
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_error_forecast(hass: HomeAssistant, mock_simple_nws, no_sensor) -> None:
|
||||
"""Test error during update forecast."""
|
||||
instance = mock_simple_nws.return_value
|
||||
instance.update_forecast.side_effect = aiohttp.ClientError
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=nws.DOMAIN,
|
||||
data=NWS_CONFIG,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
instance.update_forecast.assert_called_once()
|
||||
|
||||
state = hass.states.get("weather.abc")
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
instance.update_forecast.side_effect = None
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert instance.update_forecast.call_count == 2
|
||||
|
||||
state = hass.states.get("weather.abc")
|
||||
assert state
|
||||
assert state.state == ATTR_CONDITION_SUNNY
|
||||
|
||||
|
||||
async def test_new_config_entry(hass: HomeAssistant, no_sensor) -> None:
|
||||
"""Test the expected entities are created."""
|
||||
@ -304,7 +231,6 @@ async def test_new_config_entry(hass: HomeAssistant, no_sensor) -> None:
|
||||
("service"),
|
||||
[
|
||||
SERVICE_GET_FORECASTS,
|
||||
LEGACY_SERVICE_GET_FORECAST,
|
||||
],
|
||||
)
|
||||
async def test_forecast_service(
|
||||
@ -355,7 +281,7 @@ async def test_forecast_service(
|
||||
|
||||
assert instance.update_observation.call_count == 2
|
||||
assert instance.update_forecast.call_count == 2
|
||||
assert instance.update_forecast_hourly.call_count == 1
|
||||
assert instance.update_forecast_hourly.call_count == 2
|
||||
|
||||
for forecast_type in ("twice_daily", "hourly"):
|
||||
response = await hass.services.async_call(
|
||||
|
Loading…
x
Reference in New Issue
Block a user