mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 09:47:13 +00:00
2023.2.4 (#88007)
This commit is contained in:
commit
2fa35e174a
@ -168,28 +168,28 @@ BINARY_SENSOR_DESCRIPTIONS = (
|
||||
name="Leak detector battery 1",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_LEAK2,
|
||||
name="Leak detector battery 2",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_LEAK3,
|
||||
name="Leak detector battery 3",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_LEAK4,
|
||||
name="Leak detector battery 4",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM1,
|
||||
@ -273,7 +273,7 @@ BINARY_SENSOR_DESCRIPTIONS = (
|
||||
name="Lightning detector battery",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_LEAK1,
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "august",
|
||||
"name": "August",
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"requirements": ["yalexs==1.2.6", "yalexs_ble==1.12.8"],
|
||||
"requirements": ["yalexs==1.2.6", "yalexs_ble==1.12.12"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"dhcp": [
|
||||
{
|
||||
|
@ -216,13 +216,7 @@ class BluetoothManager:
|
||||
if address in seen:
|
||||
continue
|
||||
seen.add(address)
|
||||
for domain in self._integration_matcher.match_domains(service_info):
|
||||
discovery_flow.async_create_flow(
|
||||
self.hass,
|
||||
domain,
|
||||
{"source": config_entries.SOURCE_BLUETOOTH},
|
||||
service_info,
|
||||
)
|
||||
self._async_trigger_matching_discovery(service_info)
|
||||
|
||||
@hass_callback
|
||||
def async_stop(self, event: Event) -> None:
|
||||
@ -649,10 +643,27 @@ class BluetoothManager:
|
||||
"""Return the last service info for an address."""
|
||||
return self._get_history_by_type(connectable).get(address)
|
||||
|
||||
def _async_trigger_matching_discovery(
|
||||
self, service_info: BluetoothServiceInfoBleak
|
||||
) -> None:
|
||||
"""Trigger discovery for matching domains."""
|
||||
for domain in self._integration_matcher.match_domains(service_info):
|
||||
discovery_flow.async_create_flow(
|
||||
self.hass,
|
||||
domain,
|
||||
{"source": config_entries.SOURCE_BLUETOOTH},
|
||||
service_info,
|
||||
)
|
||||
|
||||
@hass_callback
|
||||
def async_rediscover_address(self, address: str) -> None:
|
||||
"""Trigger discovery of devices which have already been seen."""
|
||||
self._integration_matcher.async_clear_address(address)
|
||||
if service_info := self._connectable_history.get(address):
|
||||
self._async_trigger_matching_discovery(service_info)
|
||||
return
|
||||
if service_info := self._all_history.get(address):
|
||||
self._async_trigger_matching_discovery(service_info)
|
||||
|
||||
def _get_scanners_by_type(self, connectable: bool) -> list[BaseHaScanner]:
|
||||
"""Return the scanners by type."""
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "ESPHome",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/esphome",
|
||||
"requirements": ["aioesphomeapi==13.1.0", "esphome-dashboard-api==1.2.3"],
|
||||
"requirements": ["aioesphomeapi==13.3.1", "esphome-dashboard-api==1.2.3"],
|
||||
"zeroconf": ["_esphomelib._tcp.local."],
|
||||
"dhcp": [{ "registered_devices": true }],
|
||||
"codeowners": ["@OttoWinter", "@jesserockz"],
|
||||
|
@ -137,7 +137,7 @@ async def async_setup_entry(
|
||||
|
||||
def calc_min(
|
||||
sensor_values: list[tuple[str, float, State]]
|
||||
) -> tuple[dict[str, str | None], float]:
|
||||
) -> tuple[dict[str, str | None], float | None]:
|
||||
"""Calculate min value."""
|
||||
val: float | None = None
|
||||
entity_id: str | None = None
|
||||
@ -153,7 +153,7 @@ def calc_min(
|
||||
|
||||
def calc_max(
|
||||
sensor_values: list[tuple[str, float, State]]
|
||||
) -> tuple[dict[str, str | None], float]:
|
||||
) -> tuple[dict[str, str | None], float | None]:
|
||||
"""Calculate max value."""
|
||||
val: float | None = None
|
||||
entity_id: str | None = None
|
||||
@ -169,7 +169,7 @@ def calc_max(
|
||||
|
||||
def calc_mean(
|
||||
sensor_values: list[tuple[str, float, State]]
|
||||
) -> tuple[dict[str, str | None], float]:
|
||||
) -> tuple[dict[str, str | None], float | None]:
|
||||
"""Calculate mean value."""
|
||||
result = (sensor_value for _, sensor_value, _ in sensor_values)
|
||||
|
||||
@ -179,7 +179,7 @@ def calc_mean(
|
||||
|
||||
def calc_median(
|
||||
sensor_values: list[tuple[str, float, State]]
|
||||
) -> tuple[dict[str, str | None], float]:
|
||||
) -> tuple[dict[str, str | None], float | None]:
|
||||
"""Calculate median value."""
|
||||
result = (sensor_value for _, sensor_value, _ in sensor_values)
|
||||
|
||||
@ -189,10 +189,11 @@ def calc_median(
|
||||
|
||||
def calc_last(
|
||||
sensor_values: list[tuple[str, float, State]]
|
||||
) -> tuple[dict[str, str | None], float]:
|
||||
) -> tuple[dict[str, str | None], float | None]:
|
||||
"""Calculate last value."""
|
||||
last_updated: datetime | None = None
|
||||
last_entity_id: str | None = None
|
||||
last: float | None = None
|
||||
for entity_id, state_f, state in sensor_values:
|
||||
if last_updated is None or state.last_updated > last_updated:
|
||||
last_updated = state.last_updated
|
||||
@ -227,7 +228,9 @@ def calc_sum(
|
||||
|
||||
CALC_TYPES: dict[
|
||||
str,
|
||||
Callable[[list[tuple[str, float, State]]], tuple[dict[str, str | None], float]],
|
||||
Callable[
|
||||
[list[tuple[str, float, State]]], tuple[dict[str, str | None], float | None]
|
||||
],
|
||||
] = {
|
||||
"min": calc_min,
|
||||
"max": calc_max,
|
||||
|
@ -7,7 +7,7 @@ from functools import wraps
|
||||
import logging
|
||||
from typing import Any, Concatenate, ParamSpec, TypeVar
|
||||
|
||||
import aiohttp.client_exceptions
|
||||
import httpx
|
||||
from iaqualink.client import AqualinkClient
|
||||
from iaqualink.device import (
|
||||
AqualinkBinarySensor,
|
||||
@ -77,10 +77,7 @@ async def async_setup_entry( # noqa: C901
|
||||
_LOGGER.error("Failed to login: %s", login_exception)
|
||||
await aqualink.close()
|
||||
return False
|
||||
except (
|
||||
asyncio.TimeoutError,
|
||||
aiohttp.client_exceptions.ClientConnectorError,
|
||||
) as aio_exception:
|
||||
except (asyncio.TimeoutError, httpx.HTTPError) as aio_exception:
|
||||
await aqualink.close()
|
||||
raise ConfigEntryNotReady(
|
||||
f"Error while attempting login: {aio_exception}"
|
||||
@ -149,7 +146,7 @@ async def async_setup_entry( # noqa: C901
|
||||
|
||||
try:
|
||||
await system.update()
|
||||
except AqualinkServiceException as svc_exception:
|
||||
except (AqualinkServiceException, httpx.HTTPError) as svc_exception:
|
||||
if prev is not None:
|
||||
_LOGGER.warning(
|
||||
"Failed to refresh system %s state: %s",
|
||||
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from iaqualink.client import AqualinkClient
|
||||
from iaqualink.exception import (
|
||||
AqualinkServiceException,
|
||||
@ -42,7 +43,7 @@ class AqualinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
pass
|
||||
except AqualinkServiceUnauthorizedException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except AqualinkServiceException:
|
||||
except (AqualinkServiceException, httpx.HTTPError):
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return self.async_create_entry(title=username, data=user_input)
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Instituto Portugu\u00eas do Mar e Atmosfera (IPMA)",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/ipma",
|
||||
"requirements": ["pyipma==3.0.5"],
|
||||
"requirements": ["pyipma==3.0.6"],
|
||||
"codeowners": ["@dgomes", "@abmantis"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["geopy", "pyipma"]
|
||||
|
@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/lifx",
|
||||
"requirements": [
|
||||
"aiolifx==0.8.7",
|
||||
"aiolifx==0.8.9",
|
||||
"aiolifx_effects==0.3.1",
|
||||
"aiolifx_themes==0.4.0"
|
||||
],
|
||||
|
@ -5,7 +5,11 @@ from datetime import datetime
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import CONF_STATE_CLASS, SensorEntity
|
||||
from homeassistant.components.sensor import (
|
||||
CONF_STATE_CLASS,
|
||||
RestoreSensor,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_SENSORS,
|
||||
@ -14,7 +18,6 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
@ -53,7 +56,7 @@ async def async_setup_platform(
|
||||
async_add_entities(sensors)
|
||||
|
||||
|
||||
class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity):
|
||||
class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity):
|
||||
"""Modbus register sensor."""
|
||||
|
||||
def __init__(
|
||||
@ -90,8 +93,9 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
await self.async_base_added_to_hass()
|
||||
if state := await self.async_get_last_state():
|
||||
self._attr_native_value = state.state
|
||||
state = await self.async_get_last_sensor_data()
|
||||
if state:
|
||||
self._attr_native_value = state.native_value
|
||||
|
||||
async def async_update(self, now: datetime | None = None) -> None:
|
||||
"""Update the state of the sensor."""
|
||||
@ -135,7 +139,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity):
|
||||
|
||||
class SlaveSensor(
|
||||
CoordinatorEntity[DataUpdateCoordinator[list[int] | None]],
|
||||
RestoreEntity,
|
||||
RestoreSensor,
|
||||
SensorEntity,
|
||||
):
|
||||
"""Modbus slave register sensor."""
|
||||
|
@ -135,6 +135,9 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
_LOGGER.debug("Netgear ssdp discovery info: %s", discovery_info)
|
||||
|
||||
if ssdp.ATTR_UPNP_SERIAL not in discovery_info.upnp:
|
||||
return self.async_abort(reason="no_serial")
|
||||
|
||||
await self.async_set_unique_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL])
|
||||
self._abort_if_unique_id_configured(updates=updated_data)
|
||||
|
||||
|
@ -14,7 +14,9 @@
|
||||
"config": "Connection or login error: please check your configuration"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"not_ipv4_address": "No IPv4 address in ssdp discovery information",
|
||||
"no_serial": "No serial number in ssdp discovery information"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
@ -8,7 +8,7 @@
|
||||
"manufacturer_id": 220
|
||||
}
|
||||
],
|
||||
"requirements": ["oralb-ble==0.17.4"],
|
||||
"requirements": ["oralb-ble==0.17.5"],
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"codeowners": ["@bdraco", "@Lash-L"],
|
||||
"iot_class": "local_push"
|
||||
|
@ -988,7 +988,9 @@ class Recorder(threading.Thread):
|
||||
|
||||
def _handle_sqlite_corruption(self) -> None:
|
||||
"""Handle the sqlite3 database being corrupt."""
|
||||
try:
|
||||
self._close_event_session()
|
||||
finally:
|
||||
self._close_connection()
|
||||
move_away_broken_database(dburl_to_path(self.db_url))
|
||||
self.run_history.reset()
|
||||
@ -1212,18 +1214,21 @@ class Recorder(threading.Thread):
|
||||
"""End the recorder session."""
|
||||
if self.event_session is None:
|
||||
return
|
||||
try:
|
||||
if self.run_history.active:
|
||||
self.run_history.end(self.event_session)
|
||||
try:
|
||||
self._commit_event_session_or_retry()
|
||||
self.event_session.close()
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Error saving the event session during shutdown: %s", err)
|
||||
|
||||
self.event_session.close()
|
||||
self.run_history.clear()
|
||||
|
||||
def _shutdown(self) -> None:
|
||||
"""Save end time for current run."""
|
||||
self.hass.add_job(self._async_stop_listeners)
|
||||
self._stop_executor()
|
||||
try:
|
||||
self._end_session()
|
||||
finally:
|
||||
self._close_connection()
|
||||
|
@ -72,6 +72,11 @@ class RunHistory:
|
||||
start=self.recording_start, created=dt_util.utcnow()
|
||||
)
|
||||
|
||||
@property
|
||||
def active(self) -> bool:
|
||||
"""Return if a run is active."""
|
||||
return self._current_run_info is not None
|
||||
|
||||
def get(self, start: datetime) -> RecorderRuns | None:
|
||||
"""Return the recorder run that started before or at start.
|
||||
|
||||
@ -141,6 +146,5 @@ class RunHistory:
|
||||
|
||||
Must run in the recorder thread.
|
||||
"""
|
||||
assert self._current_run_info is not None
|
||||
assert self._current_run_info.end is not None
|
||||
if self._current_run_info:
|
||||
self._current_run_info = None
|
||||
|
@ -54,6 +54,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
raise ConfigEntryNotReady(
|
||||
f"Error while trying to setup {host.api.host}:{host.api.port}: {str(err)}"
|
||||
) from err
|
||||
except Exception: # pylint: disable=broad-except
|
||||
await host.stop()
|
||||
raise
|
||||
|
||||
config_entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, host.stop)
|
||||
|
@ -68,8 +68,6 @@ class ReolinkHost:
|
||||
|
||||
async def async_init(self) -> None:
|
||||
"""Connect to Reolink host."""
|
||||
self._api.expire_session()
|
||||
|
||||
await self._api.get_host_data()
|
||||
|
||||
if self._api.mac_address is None:
|
||||
@ -138,24 +136,27 @@ class ReolinkHost:
|
||||
|
||||
async def disconnect(self):
|
||||
"""Disconnect from the API, so the connection will be released."""
|
||||
await self._api.unsubscribe()
|
||||
|
||||
try:
|
||||
await self._api.logout()
|
||||
except aiohttp.ClientConnectorError as err:
|
||||
await self._api.unsubscribe()
|
||||
except (
|
||||
aiohttp.ClientConnectorError,
|
||||
asyncio.TimeoutError,
|
||||
ReolinkError,
|
||||
) as err:
|
||||
_LOGGER.error(
|
||||
"Reolink connection error while logging out for host %s:%s: %s",
|
||||
"Reolink error while unsubscribing from host %s:%s: %s",
|
||||
self._api.host,
|
||||
self._api.port,
|
||||
str(err),
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error(
|
||||
"Reolink connection timeout while logging out for host %s:%s",
|
||||
self._api.host,
|
||||
self._api.port,
|
||||
)
|
||||
except ReolinkError as err:
|
||||
|
||||
try:
|
||||
await self._api.logout()
|
||||
except (
|
||||
aiohttp.ClientConnectorError,
|
||||
asyncio.TimeoutError,
|
||||
ReolinkError,
|
||||
) as err:
|
||||
_LOGGER.error(
|
||||
"Reolink error while logging out for host %s:%s: %s",
|
||||
self._api.host,
|
||||
@ -165,13 +166,13 @@ class ReolinkHost:
|
||||
|
||||
async def stop(self, event=None):
|
||||
"""Disconnect the API."""
|
||||
await self.unregister_webhook()
|
||||
self.unregister_webhook()
|
||||
await self.disconnect()
|
||||
|
||||
async def subscribe(self) -> None:
|
||||
"""Subscribe to motion events and register the webhook as a callback."""
|
||||
if self.webhook_id is None:
|
||||
await self.register_webhook()
|
||||
self.register_webhook()
|
||||
|
||||
if self._api.subscribed:
|
||||
_LOGGER.debug(
|
||||
@ -248,7 +249,7 @@ class ReolinkHost:
|
||||
self._api.host,
|
||||
)
|
||||
|
||||
async def register_webhook(self) -> None:
|
||||
def register_webhook(self) -> None:
|
||||
"""Register the webhook for motion events."""
|
||||
self.webhook_id = f"{DOMAIN}_{self.unique_id.replace(':', '')}_ONVIF"
|
||||
event_id = self.webhook_id
|
||||
@ -263,8 +264,7 @@ class ReolinkHost:
|
||||
try:
|
||||
base_url = get_url(self._hass, prefer_external=True)
|
||||
except NoURLAvailableError as err:
|
||||
webhook.async_unregister(self._hass, event_id)
|
||||
self.webhook_id = None
|
||||
self.unregister_webhook()
|
||||
raise ReolinkWebhookException(
|
||||
f"Error registering URL for webhook {event_id}: "
|
||||
"HomeAssistant URL is not available"
|
||||
@ -275,9 +275,8 @@ class ReolinkHost:
|
||||
|
||||
_LOGGER.debug("Registered webhook: %s", event_id)
|
||||
|
||||
async def unregister_webhook(self):
|
||||
def unregister_webhook(self):
|
||||
"""Unregister the webhook for motion events."""
|
||||
if self.webhook_id:
|
||||
_LOGGER.debug("Unregistering webhook %s", self.webhook_id)
|
||||
webhook.async_unregister(self._hass, self.webhook_id)
|
||||
self.webhook_id = None
|
||||
@ -300,9 +299,10 @@ class ReolinkHost:
|
||||
)
|
||||
return
|
||||
|
||||
channel = await self._api.ONVIF_event_callback(data)
|
||||
channels = await self._api.ONVIF_event_callback(data)
|
||||
|
||||
if channel is None:
|
||||
if channels is None:
|
||||
async_dispatcher_send(hass, f"{webhook_id}_all", {})
|
||||
else:
|
||||
for channel in channels:
|
||||
async_dispatcher_send(hass, f"{webhook_id}_{channel}", {})
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Reolink IP NVR/camera",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
||||
"requirements": ["reolink-aio==0.3.4"],
|
||||
"requirements": ["reolink-aio==0.4.0"],
|
||||
"dependencies": ["webhook"],
|
||||
"codeowners": ["@starkillerOG"],
|
||||
"iot_class": "local_polling",
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "volvooncall",
|
||||
"name": "Volvo On Call",
|
||||
"documentation": "https://www.home-assistant.io/integrations/volvooncall",
|
||||
"requirements": ["volvooncall==0.10.1"],
|
||||
"requirements": ["volvooncall==0.10.2"],
|
||||
"codeowners": ["@molobrakos"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["geopy", "hbmqtt", "volvooncall"],
|
||||
|
@ -3,8 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from xiaomi_ble import SensorUpdate, XiaomiBluetoothDeviceData
|
||||
from xiaomi_ble.parser import EncryptionScheme
|
||||
from xiaomi_ble import EncryptionScheme, SensorUpdate, XiaomiBluetoothDeviceData
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bluetooth import (
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Support for Xiaomi binary sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from xiaomi_ble import SLEEPY_DEVICE_MODELS
|
||||
from xiaomi_ble.parser import (
|
||||
BinarySensorDeviceClass as XiaomiBinarySensorDeviceClass,
|
||||
ExtendedBinarySensorDeviceClass,
|
||||
@ -19,6 +20,7 @@ from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothProcessorCoordinator,
|
||||
PassiveBluetoothProcessorEntity,
|
||||
)
|
||||
from homeassistant.const import ATTR_MODEL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info
|
||||
@ -128,3 +130,12 @@ class XiaomiBluetoothSensorEntity(
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the native value."""
|
||||
return self.processor.entity_data.get(self.entity_key)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
if self.device_info and self.device_info[ATTR_MODEL] in SLEEPY_DEVICE_MODELS:
|
||||
# These devices sleep for an indeterminate amount of time
|
||||
# so there is no way to track their availability.
|
||||
return True
|
||||
return super().available
|
||||
|
@ -14,7 +14,7 @@
|
||||
}
|
||||
],
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"requirements": ["xiaomi-ble==0.16.1"],
|
||||
"requirements": ["xiaomi-ble==0.16.3"],
|
||||
"codeowners": ["@Jc2k", "@Ernst79"],
|
||||
"iot_class": "local_push"
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Yale Access Bluetooth",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
|
||||
"requirements": ["yalexs-ble==1.12.8"],
|
||||
"requirements": ["yalexs-ble==1.12.12"],
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"bluetooth": [
|
||||
|
@ -8,7 +8,7 @@ from .backports.enum import StrEnum
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2023
|
||||
MINOR_VERSION: Final = 2
|
||||
PATCH_VERSION: Final = "3"
|
||||
PATCH_VERSION: Final = "4"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0)
|
||||
|
@ -17,7 +17,7 @@ bluetooth-auto-recovery==1.0.3
|
||||
bluetooth-data-tools==0.3.1
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.3.0
|
||||
cryptography==39.0.0
|
||||
cryptography==39.0.1
|
||||
dbus-fast==1.84.0
|
||||
fnvhash==0.1.0
|
||||
hass-nabucasa==0.61.0
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2023.2.3"
|
||||
version = "2023.2.4"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
@ -41,7 +41,7 @@ dependencies = [
|
||||
"lru-dict==1.1.8",
|
||||
"PyJWT==2.5.0",
|
||||
# PyJWT has loose dependency. We want the latest one.
|
||||
"cryptography==39.0.0",
|
||||
"cryptography==39.0.1",
|
||||
# pyOpenSSL 23.0.0 is required to work with cryptography 39+
|
||||
"pyOpenSSL==23.0.0",
|
||||
"orjson==3.8.5",
|
||||
|
@ -16,7 +16,7 @@ ifaddr==0.1.7
|
||||
jinja2==3.1.2
|
||||
lru-dict==1.1.8
|
||||
PyJWT==2.5.0
|
||||
cryptography==39.0.0
|
||||
cryptography==39.0.1
|
||||
pyOpenSSL==23.0.0
|
||||
orjson==3.8.5
|
||||
pip>=21.0,<22.4
|
||||
|
@ -156,7 +156,7 @@ aioecowitt==2023.01.0
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==13.1.0
|
||||
aioesphomeapi==13.3.1
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
@ -193,7 +193,7 @@ aiokafka==0.7.2
|
||||
aiokef==0.2.16
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx==0.8.7
|
||||
aiolifx==0.8.9
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx_effects==0.3.1
|
||||
@ -1299,7 +1299,7 @@ openwrt-luci-rpc==1.1.11
|
||||
openwrt-ubus-rpc==0.0.2
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.17.4
|
||||
oralb-ble==0.17.5
|
||||
|
||||
# homeassistant.components.oru
|
||||
oru==0.1.11
|
||||
@ -1687,7 +1687,7 @@ pyinsteon==1.2.0
|
||||
pyintesishome==1.8.0
|
||||
|
||||
# homeassistant.components.ipma
|
||||
pyipma==3.0.5
|
||||
pyipma==3.0.6
|
||||
|
||||
# homeassistant.components.ipp
|
||||
pyipp==0.12.1
|
||||
@ -2227,7 +2227,7 @@ regenmaschine==2022.11.0
|
||||
renault-api==0.1.11
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.3.4
|
||||
reolink-aio==0.4.0
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==6.0
|
||||
@ -2576,7 +2576,7 @@ vilfo-api-client==0.3.2
|
||||
volkszaehler==0.4.0
|
||||
|
||||
# homeassistant.components.volvooncall
|
||||
volvooncall==0.10.1
|
||||
volvooncall==0.10.2
|
||||
|
||||
# homeassistant.components.verisure
|
||||
vsure==1.8.1
|
||||
@ -2637,7 +2637,7 @@ xbox-webapi==2.0.11
|
||||
xboxapi==2.0.1
|
||||
|
||||
# homeassistant.components.xiaomi_ble
|
||||
xiaomi-ble==0.16.1
|
||||
xiaomi-ble==0.16.3
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==2.3.0
|
||||
@ -2657,13 +2657,13 @@ xs1-api-client==3.0.0
|
||||
yalesmartalarmclient==0.3.9
|
||||
|
||||
# homeassistant.components.yalexs_ble
|
||||
yalexs-ble==1.12.8
|
||||
yalexs-ble==1.12.12
|
||||
|
||||
# homeassistant.components.august
|
||||
yalexs==1.2.6
|
||||
|
||||
# homeassistant.components.august
|
||||
yalexs_ble==1.12.8
|
||||
yalexs_ble==1.12.12
|
||||
|
||||
# homeassistant.components.yeelight
|
||||
yeelight==0.7.10
|
||||
|
@ -143,7 +143,7 @@ aioecowitt==2023.01.0
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==13.1.0
|
||||
aioesphomeapi==13.3.1
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
@ -174,7 +174,7 @@ aioimaplib==1.0.1
|
||||
aiokafka==0.7.2
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx==0.8.7
|
||||
aiolifx==0.8.9
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx_effects==0.3.1
|
||||
@ -947,7 +947,7 @@ openai==0.26.2
|
||||
openerz-api==0.2.0
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.17.4
|
||||
oralb-ble==0.17.5
|
||||
|
||||
# homeassistant.components.ovo_energy
|
||||
ovoenergy==1.2.0
|
||||
@ -1209,7 +1209,7 @@ pyicloud==1.0.0
|
||||
pyinsteon==1.2.0
|
||||
|
||||
# homeassistant.components.ipma
|
||||
pyipma==3.0.5
|
||||
pyipma==3.0.6
|
||||
|
||||
# homeassistant.components.ipp
|
||||
pyipp==0.12.1
|
||||
@ -1572,7 +1572,7 @@ regenmaschine==2022.11.0
|
||||
renault-api==0.1.11
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.3.4
|
||||
reolink-aio==0.4.0
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==6.0
|
||||
@ -1819,7 +1819,7 @@ venstarcolortouch==0.19
|
||||
vilfo-api-client==0.3.2
|
||||
|
||||
# homeassistant.components.volvooncall
|
||||
volvooncall==0.10.1
|
||||
volvooncall==0.10.2
|
||||
|
||||
# homeassistant.components.verisure
|
||||
vsure==1.8.1
|
||||
@ -1862,7 +1862,7 @@ wolf_smartset==0.1.11
|
||||
xbox-webapi==2.0.11
|
||||
|
||||
# homeassistant.components.xiaomi_ble
|
||||
xiaomi-ble==0.16.1
|
||||
xiaomi-ble==0.16.3
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==2.3.0
|
||||
@ -1879,13 +1879,13 @@ xmltodict==0.13.0
|
||||
yalesmartalarmclient==0.3.9
|
||||
|
||||
# homeassistant.components.yalexs_ble
|
||||
yalexs-ble==1.12.8
|
||||
yalexs-ble==1.12.12
|
||||
|
||||
# homeassistant.components.august
|
||||
yalexs==1.2.6
|
||||
|
||||
# homeassistant.components.august
|
||||
yalexs_ble==1.12.8
|
||||
yalexs_ble==1.12.12
|
||||
|
||||
# homeassistant.components.yeelight
|
||||
yeelight==0.7.10
|
||||
|
@ -980,7 +980,7 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth):
|
||||
inject_advertisement(hass, switchbot_device, switchbot_adv_2)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_config_flow.mock_calls) == 2
|
||||
assert len(mock_config_flow.mock_calls) == 3
|
||||
assert mock_config_flow.mock_calls[1][1][0] == "switchbot"
|
||||
|
||||
|
||||
|
@ -369,3 +369,28 @@ async def test_sensor_calculated_properties(hass: HomeAssistant) -> None:
|
||||
assert state.attributes.get("device_class") is None
|
||||
assert state.attributes.get("state_class") is None
|
||||
assert state.attributes.get("unit_of_measurement") is None
|
||||
|
||||
|
||||
async def test_last_sensor(hass: HomeAssistant) -> None:
|
||||
"""Test the last sensor."""
|
||||
config = {
|
||||
SENSOR_DOMAIN: {
|
||||
"platform": GROUP_DOMAIN,
|
||||
"name": "test_last",
|
||||
"type": "last",
|
||||
"entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
|
||||
"unique_id": "very_unique_id_last_sensor",
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, "sensor", config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_ids = config["sensor"]["entities"]
|
||||
|
||||
for entity_id, value in dict(zip(entity_ids, VALUES)).items():
|
||||
hass.states.async_set(entity_id, value)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.test_last")
|
||||
assert str(float(value)) == state.state
|
||||
assert entity_id == state.attributes.get("last_entity_id")
|
||||
|
@ -855,7 +855,7 @@ async def test_wrap_sensor(hass, mock_do_cycle, expected):
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mock_test_state",
|
||||
[(State(ENTITY_ID, "117"), State(f"{ENTITY_ID}_1", "119"))],
|
||||
[(State(ENTITY_ID, "unknown"), State(f"{ENTITY_ID}_1", "119"))],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -21,6 +21,7 @@ from homeassistant.const import (
|
||||
CONF_SSL,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@ -235,7 +236,26 @@ async def test_ssdp_already_configured(hass):
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_ssdp_ipv6(hass):
|
||||
async def test_ssdp_no_serial(hass: HomeAssistant) -> None:
|
||||
"""Test ssdp abort when the ssdp info does not include a serial number."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_SSDP},
|
||||
data=ssdp.SsdpServiceInfo(
|
||||
ssdp_usn="mock_usn",
|
||||
ssdp_st="mock_st",
|
||||
ssdp_location=SSDP_URL,
|
||||
upnp={
|
||||
ssdp.ATTR_UPNP_MODEL_NUMBER: "RBR20",
|
||||
ssdp.ATTR_UPNP_PRESENTATION_URL: URL,
|
||||
},
|
||||
),
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.ABORT
|
||||
assert result["reason"] == "no_serial"
|
||||
|
||||
|
||||
async def test_ssdp_ipv6(hass: HomeAssistant) -> None:
|
||||
"""Test ssdp abort when using a ipv6 address."""
|
||||
MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
|
@ -1,12 +1,16 @@
|
||||
"""Test run history."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components import recorder
|
||||
from homeassistant.components.recorder.db_schema import RecorderRuns
|
||||
from homeassistant.components.recorder.models import process_timestamp
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import SetupRecorderInstanceT
|
||||
|
||||
|
||||
async def test_run_history(recorder_mock, hass):
|
||||
"""Test the run history gives the correct run."""
|
||||
@ -47,12 +51,32 @@ async def test_run_history(recorder_mock, hass):
|
||||
)
|
||||
|
||||
|
||||
async def test_run_history_during_schema_migration(recorder_mock, hass):
|
||||
"""Test the run history during schema migration."""
|
||||
instance = recorder.get_instance(hass)
|
||||
async def test_run_history_while_recorder_is_not_yet_started(
|
||||
async_setup_recorder_instance: SetupRecorderInstanceT,
|
||||
hass: HomeAssistant,
|
||||
recorder_db_url: str,
|
||||
) -> None:
|
||||
"""Test the run history while recorder is not yet started.
|
||||
|
||||
This usually happens during schema migration because
|
||||
we do not start right away.
|
||||
"""
|
||||
# Prevent the run history from starting to ensure
|
||||
# we can test run_history.current.start returns the expected value
|
||||
with patch(
|
||||
"homeassistant.components.recorder.run_history.RunHistory.start",
|
||||
):
|
||||
instance = await async_setup_recorder_instance(hass)
|
||||
run_history = instance.run_history
|
||||
assert run_history.current.start == run_history.recording_start
|
||||
|
||||
def _start_run_history():
|
||||
with instance.get_session() as session:
|
||||
run_history.start(session)
|
||||
|
||||
# Ideally we would run run_history.start in the recorder thread
|
||||
# but since we mocked it out above, we run it directly here
|
||||
# via the database executor to avoid blocking the event loop.
|
||||
await instance.async_add_executor_job(_start_run_history)
|
||||
assert run_history.current.start == run_history.recording_start
|
||||
assert run_history.current.created >= run_history.recording_start
|
||||
|
@ -1,12 +1,28 @@
|
||||
"""Test Xiaomi binary sensors."""
|
||||
|
||||
from datetime import timedelta
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
||||
)
|
||||
from homeassistant.components.xiaomi_ble.const import DOMAIN
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import make_advertisement
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.bluetooth import inject_bluetooth_service_info_bleak
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.components.bluetooth import (
|
||||
inject_bluetooth_service_info_bleak,
|
||||
patch_all_discovered_devices,
|
||||
)
|
||||
|
||||
|
||||
async def test_door_problem_sensors(hass):
|
||||
@ -34,19 +50,19 @@ async def test_door_problem_sensors(hass):
|
||||
|
||||
door_sensor = hass.states.get("binary_sensor.door_lock_be98_door")
|
||||
door_sensor_attribtes = door_sensor.attributes
|
||||
assert door_sensor.state == "off"
|
||||
assert door_sensor.state == STATE_OFF
|
||||
assert door_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Door"
|
||||
|
||||
door_left_open = hass.states.get("binary_sensor.door_lock_be98_door_left_open")
|
||||
door_left_open_attribtes = door_left_open.attributes
|
||||
assert door_left_open.state == "off"
|
||||
assert door_left_open.state == STATE_OFF
|
||||
assert (
|
||||
door_left_open_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Door left open"
|
||||
)
|
||||
|
||||
pry_the_door = hass.states.get("binary_sensor.door_lock_be98_pry_the_door")
|
||||
pry_the_door_attribtes = pry_the_door.attributes
|
||||
assert pry_the_door.state == "off"
|
||||
assert pry_the_door.state == STATE_OFF
|
||||
assert pry_the_door_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Pry the door"
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
@ -77,12 +93,12 @@ async def test_light_motion(hass):
|
||||
|
||||
motion_sensor = hass.states.get("binary_sensor.nightlight_9321_motion")
|
||||
motion_sensor_attribtes = motion_sensor.attributes
|
||||
assert motion_sensor.state == "on"
|
||||
assert motion_sensor.state == STATE_ON
|
||||
assert motion_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Nightlight 9321 Motion"
|
||||
|
||||
light_sensor = hass.states.get("binary_sensor.nightlight_9321_light")
|
||||
light_sensor_attribtes = light_sensor.attributes
|
||||
assert light_sensor.state == "off"
|
||||
assert light_sensor.state == STATE_OFF
|
||||
assert light_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Nightlight 9321 Light"
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
@ -116,7 +132,7 @@ async def test_moisture(hass):
|
||||
|
||||
sensor = hass.states.get("binary_sensor.smart_flower_pot_3e7a_moisture")
|
||||
sensor_attr = sensor.attributes
|
||||
assert sensor.state == "on"
|
||||
assert sensor.state == STATE_ON
|
||||
assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 3E7A Moisture"
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
@ -148,12 +164,12 @@ async def test_opening(hass):
|
||||
|
||||
opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening")
|
||||
opening_sensor_attribtes = opening_sensor.attributes
|
||||
assert opening_sensor.state == "on"
|
||||
|
||||
assert opening_sensor.state == STATE_ON
|
||||
assert (
|
||||
opening_sensor_attribtes[ATTR_FRIENDLY_NAME]
|
||||
== "Door/Window Sensor E567 Opening"
|
||||
)
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@ -183,7 +199,7 @@ async def test_opening_problem_sensors(hass):
|
||||
|
||||
opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening")
|
||||
opening_sensor_attribtes = opening_sensor.attributes
|
||||
assert opening_sensor.state == "off"
|
||||
assert opening_sensor.state == STATE_OFF
|
||||
assert (
|
||||
opening_sensor_attribtes[ATTR_FRIENDLY_NAME]
|
||||
== "Door/Window Sensor E567 Opening"
|
||||
@ -193,7 +209,7 @@ async def test_opening_problem_sensors(hass):
|
||||
"binary_sensor.door_window_sensor_e567_door_left_open"
|
||||
)
|
||||
door_left_open_attribtes = door_left_open.attributes
|
||||
assert door_left_open.state == "off"
|
||||
assert door_left_open.state == STATE_OFF
|
||||
assert (
|
||||
door_left_open_attribtes[ATTR_FRIENDLY_NAME]
|
||||
== "Door/Window Sensor E567 Door left open"
|
||||
@ -203,7 +219,7 @@ async def test_opening_problem_sensors(hass):
|
||||
"binary_sensor.door_window_sensor_e567_device_forcibly_removed"
|
||||
)
|
||||
device_forcibly_removed_attribtes = device_forcibly_removed.attributes
|
||||
assert device_forcibly_removed.state == "off"
|
||||
assert device_forcibly_removed.state == STATE_OFF
|
||||
assert (
|
||||
device_forcibly_removed_attribtes[ATTR_FRIENDLY_NAME]
|
||||
== "Door/Window Sensor E567 Device forcibly removed"
|
||||
@ -238,8 +254,111 @@ async def test_smoke(hass):
|
||||
|
||||
smoke_sensor = hass.states.get("binary_sensor.thermometer_9cbc_smoke")
|
||||
smoke_sensor_attribtes = smoke_sensor.attributes
|
||||
assert smoke_sensor.state == "on"
|
||||
assert smoke_sensor.state == STATE_ON
|
||||
assert smoke_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Thermometer 9CBC Smoke"
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_unavailable(hass):
|
||||
"""Test normal device goes to unavailable after 60 minutes."""
|
||||
start_monotonic = time.monotonic()
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="A4:C1:38:66:E5:67",
|
||||
data={"bindkey": "0fdcc30fe9289254876b5ef7c11ef1f0"},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 0
|
||||
inject_bluetooth_service_info_bleak(
|
||||
hass,
|
||||
make_advertisement(
|
||||
"A4:C1:38:66:E5:67",
|
||||
b"XY\x89\x18\x9ag\xe5f8\xc1\xa4\x9d\xd9z\xf3&\x00\x00\xc8\xa6\x0b\xd5",
|
||||
),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.states.async_all()) == 1
|
||||
|
||||
opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening")
|
||||
|
||||
assert opening_sensor.state == STATE_ON
|
||||
|
||||
# Fastforward time without BLE advertisements
|
||||
monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.bluetooth.manager.MONOTONIC_TIME",
|
||||
return_value=monotonic_now,
|
||||
), patch_all_discovered_devices([]):
|
||||
async_fire_time_changed(
|
||||
hass,
|
||||
dt_util.utcnow()
|
||||
+ timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening")
|
||||
|
||||
# Normal devices should go to unavailable
|
||||
assert opening_sensor.state == STATE_UNAVAILABLE
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_sleepy_device(hass):
|
||||
"""Test sleepy device does not go to unavailable after 60 minutes."""
|
||||
start_monotonic = time.monotonic()
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="A4:C1:38:66:E5:67",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 0
|
||||
inject_bluetooth_service_info_bleak(
|
||||
hass,
|
||||
make_advertisement(
|
||||
"A4:C1:38:66:E5:67",
|
||||
b"@0\xd6\x03$\x19\x10\x01\x00",
|
||||
),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.states.async_all()) == 1
|
||||
|
||||
opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening")
|
||||
|
||||
assert opening_sensor.state == STATE_ON
|
||||
|
||||
# Fastforward time without BLE advertisements
|
||||
monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.bluetooth.manager.MONOTONIC_TIME",
|
||||
return_value=monotonic_now,
|
||||
), patch_all_discovered_devices([]):
|
||||
async_fire_time_changed(
|
||||
hass,
|
||||
dt_util.utcnow()
|
||||
+ timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening")
|
||||
|
||||
# Sleepy devices should keep their state over time
|
||||
assert opening_sensor.state == STATE_ON
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
Loading…
x
Reference in New Issue
Block a user