This commit is contained in:
Paulus Schoutsen 2023-02-13 10:06:16 -05:00 committed by GitHub
commit 2fa35e174a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 355 additions and 124 deletions

View File

@ -168,28 +168,28 @@ BINARY_SENSOR_DESCRIPTIONS = (
name="Leak detector battery 1", name="Leak detector battery 1",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_LEAK2, key=TYPE_BATT_LEAK2,
name="Leak detector battery 2", name="Leak detector battery 2",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_LEAK3, key=TYPE_BATT_LEAK3,
name="Leak detector battery 3", name="Leak detector battery 3",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_LEAK4, key=TYPE_BATT_LEAK4,
name="Leak detector battery 4", name="Leak detector battery 4",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM1, key=TYPE_BATT_SM1,
@ -273,7 +273,7 @@ BINARY_SENSOR_DESCRIPTIONS = (
name="Lightning detector battery", name="Lightning detector battery",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_LEAK1, key=TYPE_LEAK1,

View File

@ -2,7 +2,7 @@
"domain": "august", "domain": "august",
"name": "August", "name": "August",
"documentation": "https://www.home-assistant.io/integrations/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"], "codeowners": ["@bdraco"],
"dhcp": [ "dhcp": [
{ {

View File

@ -216,13 +216,7 @@ class BluetoothManager:
if address in seen: if address in seen:
continue continue
seen.add(address) seen.add(address)
for domain in self._integration_matcher.match_domains(service_info): self._async_trigger_matching_discovery(service_info)
discovery_flow.async_create_flow(
self.hass,
domain,
{"source": config_entries.SOURCE_BLUETOOTH},
service_info,
)
@hass_callback @hass_callback
def async_stop(self, event: Event) -> None: def async_stop(self, event: Event) -> None:
@ -649,10 +643,27 @@ class BluetoothManager:
"""Return the last service info for an address.""" """Return the last service info for an address."""
return self._get_history_by_type(connectable).get(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 @hass_callback
def async_rediscover_address(self, address: str) -> None: def async_rediscover_address(self, address: str) -> None:
"""Trigger discovery of devices which have already been seen.""" """Trigger discovery of devices which have already been seen."""
self._integration_matcher.async_clear_address(address) 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]: def _get_scanners_by_type(self, connectable: bool) -> list[BaseHaScanner]:
"""Return the scanners by type.""" """Return the scanners by type."""

View File

@ -3,7 +3,7 @@
"name": "ESPHome", "name": "ESPHome",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/esphome", "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."], "zeroconf": ["_esphomelib._tcp.local."],
"dhcp": [{ "registered_devices": true }], "dhcp": [{ "registered_devices": true }],
"codeowners": ["@OttoWinter", "@jesserockz"], "codeowners": ["@OttoWinter", "@jesserockz"],

View File

@ -137,7 +137,7 @@ async def async_setup_entry(
def calc_min( def calc_min(
sensor_values: list[tuple[str, float, State]] sensor_values: list[tuple[str, float, State]]
) -> tuple[dict[str, str | None], float]: ) -> tuple[dict[str, str | None], float | None]:
"""Calculate min value.""" """Calculate min value."""
val: float | None = None val: float | None = None
entity_id: str | None = None entity_id: str | None = None
@ -153,7 +153,7 @@ def calc_min(
def calc_max( def calc_max(
sensor_values: list[tuple[str, float, State]] sensor_values: list[tuple[str, float, State]]
) -> tuple[dict[str, str | None], float]: ) -> tuple[dict[str, str | None], float | None]:
"""Calculate max value.""" """Calculate max value."""
val: float | None = None val: float | None = None
entity_id: str | None = None entity_id: str | None = None
@ -169,7 +169,7 @@ def calc_max(
def calc_mean( def calc_mean(
sensor_values: list[tuple[str, float, State]] sensor_values: list[tuple[str, float, State]]
) -> tuple[dict[str, str | None], float]: ) -> tuple[dict[str, str | None], float | None]:
"""Calculate mean value.""" """Calculate mean value."""
result = (sensor_value for _, sensor_value, _ in sensor_values) result = (sensor_value for _, sensor_value, _ in sensor_values)
@ -179,7 +179,7 @@ def calc_mean(
def calc_median( def calc_median(
sensor_values: list[tuple[str, float, State]] sensor_values: list[tuple[str, float, State]]
) -> tuple[dict[str, str | None], float]: ) -> tuple[dict[str, str | None], float | None]:
"""Calculate median value.""" """Calculate median value."""
result = (sensor_value for _, sensor_value, _ in sensor_values) result = (sensor_value for _, sensor_value, _ in sensor_values)
@ -189,10 +189,11 @@ def calc_median(
def calc_last( def calc_last(
sensor_values: list[tuple[str, float, State]] sensor_values: list[tuple[str, float, State]]
) -> tuple[dict[str, str | None], float]: ) -> tuple[dict[str, str | None], float | None]:
"""Calculate last value.""" """Calculate last value."""
last_updated: datetime | None = None last_updated: datetime | None = None
last_entity_id: str | None = None last_entity_id: str | None = None
last: float | None = None
for entity_id, state_f, state in sensor_values: for entity_id, state_f, state in sensor_values:
if last_updated is None or state.last_updated > last_updated: if last_updated is None or state.last_updated > last_updated:
last_updated = state.last_updated last_updated = state.last_updated
@ -227,7 +228,9 @@ def calc_sum(
CALC_TYPES: dict[ CALC_TYPES: dict[
str, 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, "min": calc_min,
"max": calc_max, "max": calc_max,

View File

@ -7,7 +7,7 @@ from functools import wraps
import logging import logging
from typing import Any, Concatenate, ParamSpec, TypeVar from typing import Any, Concatenate, ParamSpec, TypeVar
import aiohttp.client_exceptions import httpx
from iaqualink.client import AqualinkClient from iaqualink.client import AqualinkClient
from iaqualink.device import ( from iaqualink.device import (
AqualinkBinarySensor, AqualinkBinarySensor,
@ -77,10 +77,7 @@ async def async_setup_entry( # noqa: C901
_LOGGER.error("Failed to login: %s", login_exception) _LOGGER.error("Failed to login: %s", login_exception)
await aqualink.close() await aqualink.close()
return False return False
except ( except (asyncio.TimeoutError, httpx.HTTPError) as aio_exception:
asyncio.TimeoutError,
aiohttp.client_exceptions.ClientConnectorError,
) as aio_exception:
await aqualink.close() await aqualink.close()
raise ConfigEntryNotReady( raise ConfigEntryNotReady(
f"Error while attempting login: {aio_exception}" f"Error while attempting login: {aio_exception}"
@ -149,7 +146,7 @@ async def async_setup_entry( # noqa: C901
try: try:
await system.update() await system.update()
except AqualinkServiceException as svc_exception: except (AqualinkServiceException, httpx.HTTPError) as svc_exception:
if prev is not None: if prev is not None:
_LOGGER.warning( _LOGGER.warning(
"Failed to refresh system %s state: %s", "Failed to refresh system %s state: %s",

View File

@ -3,6 +3,7 @@ from __future__ import annotations
from typing import Any from typing import Any
import httpx
from iaqualink.client import AqualinkClient from iaqualink.client import AqualinkClient
from iaqualink.exception import ( from iaqualink.exception import (
AqualinkServiceException, AqualinkServiceException,
@ -42,7 +43,7 @@ class AqualinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
pass pass
except AqualinkServiceUnauthorizedException: except AqualinkServiceUnauthorizedException:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except AqualinkServiceException: except (AqualinkServiceException, httpx.HTTPError):
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
else: else:
return self.async_create_entry(title=username, data=user_input) return self.async_create_entry(title=username, data=user_input)

View File

@ -3,7 +3,7 @@
"name": "Instituto Portugu\u00eas do Mar e Atmosfera (IPMA)", "name": "Instituto Portugu\u00eas do Mar e Atmosfera (IPMA)",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ipma", "documentation": "https://www.home-assistant.io/integrations/ipma",
"requirements": ["pyipma==3.0.5"], "requirements": ["pyipma==3.0.6"],
"codeowners": ["@dgomes", "@abmantis"], "codeowners": ["@dgomes", "@abmantis"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["geopy", "pyipma"] "loggers": ["geopy", "pyipma"]

View File

@ -4,7 +4,7 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/lifx", "documentation": "https://www.home-assistant.io/integrations/lifx",
"requirements": [ "requirements": [
"aiolifx==0.8.7", "aiolifx==0.8.9",
"aiolifx_effects==0.3.1", "aiolifx_effects==0.3.1",
"aiolifx_themes==0.4.0" "aiolifx_themes==0.4.0"
], ],

View File

@ -5,7 +5,11 @@ from datetime import datetime
import logging import logging
from typing import Any 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 ( from homeassistant.const import (
CONF_NAME, CONF_NAME,
CONF_SENSORS, CONF_SENSORS,
@ -14,7 +18,6 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import (
CoordinatorEntity, CoordinatorEntity,
@ -53,7 +56,7 @@ async def async_setup_platform(
async_add_entities(sensors) async_add_entities(sensors)
class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity):
"""Modbus register sensor.""" """Modbus register sensor."""
def __init__( def __init__(
@ -90,8 +93,9 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity):
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Handle entity which will be added.""" """Handle entity which will be added."""
await self.async_base_added_to_hass() await self.async_base_added_to_hass()
if state := await self.async_get_last_state(): state = await self.async_get_last_sensor_data()
self._attr_native_value = state.state if state:
self._attr_native_value = state.native_value
async def async_update(self, now: datetime | None = None) -> None: async def async_update(self, now: datetime | None = None) -> None:
"""Update the state of the sensor.""" """Update the state of the sensor."""
@ -135,7 +139,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity):
class SlaveSensor( class SlaveSensor(
CoordinatorEntity[DataUpdateCoordinator[list[int] | None]], CoordinatorEntity[DataUpdateCoordinator[list[int] | None]],
RestoreEntity, RestoreSensor,
SensorEntity, SensorEntity,
): ):
"""Modbus slave register sensor.""" """Modbus slave register sensor."""

View File

@ -135,6 +135,9 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
_LOGGER.debug("Netgear ssdp discovery info: %s", discovery_info) _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]) await self.async_set_unique_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL])
self._abort_if_unique_id_configured(updates=updated_data) self._abort_if_unique_id_configured(updates=updated_data)

View File

@ -14,7 +14,9 @@
"config": "Connection or login error: please check your configuration" "config": "Connection or login error: please check your configuration"
}, },
"abort": { "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": { "options": {

View File

@ -8,7 +8,7 @@
"manufacturer_id": 220 "manufacturer_id": 220
} }
], ],
"requirements": ["oralb-ble==0.17.4"], "requirements": ["oralb-ble==0.17.5"],
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"codeowners": ["@bdraco", "@Lash-L"], "codeowners": ["@bdraco", "@Lash-L"],
"iot_class": "local_push" "iot_class": "local_push"

View File

@ -988,8 +988,10 @@ class Recorder(threading.Thread):
def _handle_sqlite_corruption(self) -> None: def _handle_sqlite_corruption(self) -> None:
"""Handle the sqlite3 database being corrupt.""" """Handle the sqlite3 database being corrupt."""
self._close_event_session() try:
self._close_connection() self._close_event_session()
finally:
self._close_connection()
move_away_broken_database(dburl_to_path(self.db_url)) move_away_broken_database(dburl_to_path(self.db_url))
self.run_history.reset() self.run_history.reset()
self._setup_recorder() self._setup_recorder()
@ -1212,18 +1214,21 @@ class Recorder(threading.Thread):
"""End the recorder session.""" """End the recorder session."""
if self.event_session is None: if self.event_session is None:
return return
try: if self.run_history.active:
self.run_history.end(self.event_session) self.run_history.end(self.event_session)
try:
self._commit_event_session_or_retry() self._commit_event_session_or_retry()
self.event_session.close()
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Error saving the event session during shutdown: %s", err) _LOGGER.exception("Error saving the event session during shutdown: %s", err)
self.event_session.close()
self.run_history.clear() self.run_history.clear()
def _shutdown(self) -> None: def _shutdown(self) -> None:
"""Save end time for current run.""" """Save end time for current run."""
self.hass.add_job(self._async_stop_listeners) self.hass.add_job(self._async_stop_listeners)
self._stop_executor() self._stop_executor()
self._end_session() try:
self._close_connection() self._end_session()
finally:
self._close_connection()

View File

@ -72,6 +72,11 @@ class RunHistory:
start=self.recording_start, created=dt_util.utcnow() 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: def get(self, start: datetime) -> RecorderRuns | None:
"""Return the recorder run that started before or at start. """Return the recorder run that started before or at start.
@ -141,6 +146,5 @@ class RunHistory:
Must run in the recorder thread. Must run in the recorder thread.
""" """
assert self._current_run_info is not None if self._current_run_info:
assert self._current_run_info.end is not None self._current_run_info = None
self._current_run_info = None

View File

@ -54,6 +54,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
raise ConfigEntryNotReady( raise ConfigEntryNotReady(
f"Error while trying to setup {host.api.host}:{host.api.port}: {str(err)}" f"Error while trying to setup {host.api.host}:{host.api.port}: {str(err)}"
) from err ) from err
except Exception: # pylint: disable=broad-except
await host.stop()
raise
config_entry.async_on_unload( config_entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, host.stop) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, host.stop)

View File

@ -68,8 +68,6 @@ class ReolinkHost:
async def async_init(self) -> None: async def async_init(self) -> None:
"""Connect to Reolink host.""" """Connect to Reolink host."""
self._api.expire_session()
await self._api.get_host_data() await self._api.get_host_data()
if self._api.mac_address is None: if self._api.mac_address is None:
@ -138,24 +136,27 @@ class ReolinkHost:
async def disconnect(self): async def disconnect(self):
"""Disconnect from the API, so the connection will be released.""" """Disconnect from the API, so the connection will be released."""
await self._api.unsubscribe()
try: try:
await self._api.logout() await self._api.unsubscribe()
except aiohttp.ClientConnectorError as err: except (
aiohttp.ClientConnectorError,
asyncio.TimeoutError,
ReolinkError,
) as err:
_LOGGER.error( _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.host,
self._api.port, self._api.port,
str(err), str(err),
) )
except asyncio.TimeoutError:
_LOGGER.error( try:
"Reolink connection timeout while logging out for host %s:%s", await self._api.logout()
self._api.host, except (
self._api.port, aiohttp.ClientConnectorError,
) asyncio.TimeoutError,
except ReolinkError as err: ReolinkError,
) as err:
_LOGGER.error( _LOGGER.error(
"Reolink error while logging out for host %s:%s: %s", "Reolink error while logging out for host %s:%s: %s",
self._api.host, self._api.host,
@ -165,13 +166,13 @@ class ReolinkHost:
async def stop(self, event=None): async def stop(self, event=None):
"""Disconnect the API.""" """Disconnect the API."""
await self.unregister_webhook() self.unregister_webhook()
await self.disconnect() await self.disconnect()
async def subscribe(self) -> None: async def subscribe(self) -> None:
"""Subscribe to motion events and register the webhook as a callback.""" """Subscribe to motion events and register the webhook as a callback."""
if self.webhook_id is None: if self.webhook_id is None:
await self.register_webhook() self.register_webhook()
if self._api.subscribed: if self._api.subscribed:
_LOGGER.debug( _LOGGER.debug(
@ -248,7 +249,7 @@ class ReolinkHost:
self._api.host, self._api.host,
) )
async def register_webhook(self) -> None: def register_webhook(self) -> None:
"""Register the webhook for motion events.""" """Register the webhook for motion events."""
self.webhook_id = f"{DOMAIN}_{self.unique_id.replace(':', '')}_ONVIF" self.webhook_id = f"{DOMAIN}_{self.unique_id.replace(':', '')}_ONVIF"
event_id = self.webhook_id event_id = self.webhook_id
@ -263,8 +264,7 @@ class ReolinkHost:
try: try:
base_url = get_url(self._hass, prefer_external=True) base_url = get_url(self._hass, prefer_external=True)
except NoURLAvailableError as err: except NoURLAvailableError as err:
webhook.async_unregister(self._hass, event_id) self.unregister_webhook()
self.webhook_id = None
raise ReolinkWebhookException( raise ReolinkWebhookException(
f"Error registering URL for webhook {event_id}: " f"Error registering URL for webhook {event_id}: "
"HomeAssistant URL is not available" "HomeAssistant URL is not available"
@ -275,11 +275,10 @@ class ReolinkHost:
_LOGGER.debug("Registered webhook: %s", event_id) _LOGGER.debug("Registered webhook: %s", event_id)
async def unregister_webhook(self): def unregister_webhook(self):
"""Unregister the webhook for motion events.""" """Unregister the webhook for motion events."""
if self.webhook_id: _LOGGER.debug("Unregistering webhook %s", self.webhook_id)
_LOGGER.debug("Unregistering webhook %s", self.webhook_id) webhook.async_unregister(self._hass, self.webhook_id)
webhook.async_unregister(self._hass, self.webhook_id)
self.webhook_id = None self.webhook_id = None
async def handle_webhook( async def handle_webhook(
@ -300,9 +299,10 @@ class ReolinkHost:
) )
return 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", {}) async_dispatcher_send(hass, f"{webhook_id}_all", {})
else: else:
async_dispatcher_send(hass, f"{webhook_id}_{channel}", {}) for channel in channels:
async_dispatcher_send(hass, f"{webhook_id}_{channel}", {})

View File

@ -3,7 +3,7 @@
"name": "Reolink IP NVR/camera", "name": "Reolink IP NVR/camera",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/reolink", "documentation": "https://www.home-assistant.io/integrations/reolink",
"requirements": ["reolink-aio==0.3.4"], "requirements": ["reolink-aio==0.4.0"],
"dependencies": ["webhook"], "dependencies": ["webhook"],
"codeowners": ["@starkillerOG"], "codeowners": ["@starkillerOG"],
"iot_class": "local_polling", "iot_class": "local_polling",

View File

@ -2,7 +2,7 @@
"domain": "volvooncall", "domain": "volvooncall",
"name": "Volvo On Call", "name": "Volvo On Call",
"documentation": "https://www.home-assistant.io/integrations/volvooncall", "documentation": "https://www.home-assistant.io/integrations/volvooncall",
"requirements": ["volvooncall==0.10.1"], "requirements": ["volvooncall==0.10.2"],
"codeowners": ["@molobrakos"], "codeowners": ["@molobrakos"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["geopy", "hbmqtt", "volvooncall"], "loggers": ["geopy", "hbmqtt", "volvooncall"],

View File

@ -3,8 +3,7 @@ from __future__ import annotations
import logging import logging
from xiaomi_ble import SensorUpdate, XiaomiBluetoothDeviceData from xiaomi_ble import EncryptionScheme, SensorUpdate, XiaomiBluetoothDeviceData
from xiaomi_ble.parser import EncryptionScheme
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.bluetooth import ( from homeassistant.components.bluetooth import (

View File

@ -1,6 +1,7 @@
"""Support for Xiaomi binary sensors.""" """Support for Xiaomi binary sensors."""
from __future__ import annotations from __future__ import annotations
from xiaomi_ble import SLEEPY_DEVICE_MODELS
from xiaomi_ble.parser import ( from xiaomi_ble.parser import (
BinarySensorDeviceClass as XiaomiBinarySensorDeviceClass, BinarySensorDeviceClass as XiaomiBinarySensorDeviceClass,
ExtendedBinarySensorDeviceClass, ExtendedBinarySensorDeviceClass,
@ -19,6 +20,7 @@ from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorCoordinator,
PassiveBluetoothProcessorEntity, PassiveBluetoothProcessorEntity,
) )
from homeassistant.const import ATTR_MODEL
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info
@ -128,3 +130,12 @@ class XiaomiBluetoothSensorEntity(
def is_on(self) -> bool | None: def is_on(self) -> bool | None:
"""Return the native value.""" """Return the native value."""
return self.processor.entity_data.get(self.entity_key) 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

View File

@ -14,7 +14,7 @@
} }
], ],
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"requirements": ["xiaomi-ble==0.16.1"], "requirements": ["xiaomi-ble==0.16.3"],
"codeowners": ["@Jc2k", "@Ernst79"], "codeowners": ["@Jc2k", "@Ernst79"],
"iot_class": "local_push" "iot_class": "local_push"
} }

View File

@ -3,7 +3,7 @@
"name": "Yale Access Bluetooth", "name": "Yale Access Bluetooth",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble", "documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
"requirements": ["yalexs-ble==1.12.8"], "requirements": ["yalexs-ble==1.12.12"],
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"codeowners": ["@bdraco"], "codeowners": ["@bdraco"],
"bluetooth": [ "bluetooth": [

View File

@ -8,7 +8,7 @@ from .backports.enum import StrEnum
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023 MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 2 MINOR_VERSION: Final = 2
PATCH_VERSION: Final = "3" PATCH_VERSION: Final = "4"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0)

View File

@ -17,7 +17,7 @@ bluetooth-auto-recovery==1.0.3
bluetooth-data-tools==0.3.1 bluetooth-data-tools==0.3.1
certifi>=2021.5.30 certifi>=2021.5.30
ciso8601==2.3.0 ciso8601==2.3.0
cryptography==39.0.0 cryptography==39.0.1
dbus-fast==1.84.0 dbus-fast==1.84.0
fnvhash==0.1.0 fnvhash==0.1.0
hass-nabucasa==0.61.0 hass-nabucasa==0.61.0

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2023.2.3" version = "2023.2.4"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"
@ -41,7 +41,7 @@ dependencies = [
"lru-dict==1.1.8", "lru-dict==1.1.8",
"PyJWT==2.5.0", "PyJWT==2.5.0",
# PyJWT has loose dependency. We want the latest one. # 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 is required to work with cryptography 39+
"pyOpenSSL==23.0.0", "pyOpenSSL==23.0.0",
"orjson==3.8.5", "orjson==3.8.5",

View File

@ -16,7 +16,7 @@ ifaddr==0.1.7
jinja2==3.1.2 jinja2==3.1.2
lru-dict==1.1.8 lru-dict==1.1.8
PyJWT==2.5.0 PyJWT==2.5.0
cryptography==39.0.0 cryptography==39.0.1
pyOpenSSL==23.0.0 pyOpenSSL==23.0.0
orjson==3.8.5 orjson==3.8.5
pip>=21.0,<22.4 pip>=21.0,<22.4

View File

@ -156,7 +156,7 @@ aioecowitt==2023.01.0
aioemonitor==1.0.5 aioemonitor==1.0.5
# homeassistant.components.esphome # homeassistant.components.esphome
aioesphomeapi==13.1.0 aioesphomeapi==13.3.1
# homeassistant.components.flo # homeassistant.components.flo
aioflo==2021.11.0 aioflo==2021.11.0
@ -193,7 +193,7 @@ aiokafka==0.7.2
aiokef==0.2.16 aiokef==0.2.16
# homeassistant.components.lifx # homeassistant.components.lifx
aiolifx==0.8.7 aiolifx==0.8.9
# homeassistant.components.lifx # homeassistant.components.lifx
aiolifx_effects==0.3.1 aiolifx_effects==0.3.1
@ -1299,7 +1299,7 @@ openwrt-luci-rpc==1.1.11
openwrt-ubus-rpc==0.0.2 openwrt-ubus-rpc==0.0.2
# homeassistant.components.oralb # homeassistant.components.oralb
oralb-ble==0.17.4 oralb-ble==0.17.5
# homeassistant.components.oru # homeassistant.components.oru
oru==0.1.11 oru==0.1.11
@ -1687,7 +1687,7 @@ pyinsteon==1.2.0
pyintesishome==1.8.0 pyintesishome==1.8.0
# homeassistant.components.ipma # homeassistant.components.ipma
pyipma==3.0.5 pyipma==3.0.6
# homeassistant.components.ipp # homeassistant.components.ipp
pyipp==0.12.1 pyipp==0.12.1
@ -2227,7 +2227,7 @@ regenmaschine==2022.11.0
renault-api==0.1.11 renault-api==0.1.11
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.3.4 reolink-aio==0.4.0
# homeassistant.components.python_script # homeassistant.components.python_script
restrictedpython==6.0 restrictedpython==6.0
@ -2576,7 +2576,7 @@ vilfo-api-client==0.3.2
volkszaehler==0.4.0 volkszaehler==0.4.0
# homeassistant.components.volvooncall # homeassistant.components.volvooncall
volvooncall==0.10.1 volvooncall==0.10.2
# homeassistant.components.verisure # homeassistant.components.verisure
vsure==1.8.1 vsure==1.8.1
@ -2637,7 +2637,7 @@ xbox-webapi==2.0.11
xboxapi==2.0.1 xboxapi==2.0.1
# homeassistant.components.xiaomi_ble # homeassistant.components.xiaomi_ble
xiaomi-ble==0.16.1 xiaomi-ble==0.16.3
# homeassistant.components.knx # homeassistant.components.knx
xknx==2.3.0 xknx==2.3.0
@ -2657,13 +2657,13 @@ xs1-api-client==3.0.0
yalesmartalarmclient==0.3.9 yalesmartalarmclient==0.3.9
# homeassistant.components.yalexs_ble # homeassistant.components.yalexs_ble
yalexs-ble==1.12.8 yalexs-ble==1.12.12
# homeassistant.components.august # homeassistant.components.august
yalexs==1.2.6 yalexs==1.2.6
# homeassistant.components.august # homeassistant.components.august
yalexs_ble==1.12.8 yalexs_ble==1.12.12
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.7.10 yeelight==0.7.10

View File

@ -143,7 +143,7 @@ aioecowitt==2023.01.0
aioemonitor==1.0.5 aioemonitor==1.0.5
# homeassistant.components.esphome # homeassistant.components.esphome
aioesphomeapi==13.1.0 aioesphomeapi==13.3.1
# homeassistant.components.flo # homeassistant.components.flo
aioflo==2021.11.0 aioflo==2021.11.0
@ -174,7 +174,7 @@ aioimaplib==1.0.1
aiokafka==0.7.2 aiokafka==0.7.2
# homeassistant.components.lifx # homeassistant.components.lifx
aiolifx==0.8.7 aiolifx==0.8.9
# homeassistant.components.lifx # homeassistant.components.lifx
aiolifx_effects==0.3.1 aiolifx_effects==0.3.1
@ -947,7 +947,7 @@ openai==0.26.2
openerz-api==0.2.0 openerz-api==0.2.0
# homeassistant.components.oralb # homeassistant.components.oralb
oralb-ble==0.17.4 oralb-ble==0.17.5
# homeassistant.components.ovo_energy # homeassistant.components.ovo_energy
ovoenergy==1.2.0 ovoenergy==1.2.0
@ -1209,7 +1209,7 @@ pyicloud==1.0.0
pyinsteon==1.2.0 pyinsteon==1.2.0
# homeassistant.components.ipma # homeassistant.components.ipma
pyipma==3.0.5 pyipma==3.0.6
# homeassistant.components.ipp # homeassistant.components.ipp
pyipp==0.12.1 pyipp==0.12.1
@ -1572,7 +1572,7 @@ regenmaschine==2022.11.0
renault-api==0.1.11 renault-api==0.1.11
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.3.4 reolink-aio==0.4.0
# homeassistant.components.python_script # homeassistant.components.python_script
restrictedpython==6.0 restrictedpython==6.0
@ -1819,7 +1819,7 @@ venstarcolortouch==0.19
vilfo-api-client==0.3.2 vilfo-api-client==0.3.2
# homeassistant.components.volvooncall # homeassistant.components.volvooncall
volvooncall==0.10.1 volvooncall==0.10.2
# homeassistant.components.verisure # homeassistant.components.verisure
vsure==1.8.1 vsure==1.8.1
@ -1862,7 +1862,7 @@ wolf_smartset==0.1.11
xbox-webapi==2.0.11 xbox-webapi==2.0.11
# homeassistant.components.xiaomi_ble # homeassistant.components.xiaomi_ble
xiaomi-ble==0.16.1 xiaomi-ble==0.16.3
# homeassistant.components.knx # homeassistant.components.knx
xknx==2.3.0 xknx==2.3.0
@ -1879,13 +1879,13 @@ xmltodict==0.13.0
yalesmartalarmclient==0.3.9 yalesmartalarmclient==0.3.9
# homeassistant.components.yalexs_ble # homeassistant.components.yalexs_ble
yalexs-ble==1.12.8 yalexs-ble==1.12.12
# homeassistant.components.august # homeassistant.components.august
yalexs==1.2.6 yalexs==1.2.6
# homeassistant.components.august # homeassistant.components.august
yalexs_ble==1.12.8 yalexs_ble==1.12.12
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.7.10 yeelight==0.7.10

View File

@ -980,7 +980,7 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth):
inject_advertisement(hass, switchbot_device, switchbot_adv_2) inject_advertisement(hass, switchbot_device, switchbot_adv_2)
await hass.async_block_till_done() 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" assert mock_config_flow.mock_calls[1][1][0] == "switchbot"

View File

@ -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("device_class") is None
assert state.attributes.get("state_class") is None assert state.attributes.get("state_class") is None
assert state.attributes.get("unit_of_measurement") 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")

View File

@ -855,7 +855,7 @@ async def test_wrap_sensor(hass, mock_do_cycle, expected):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"mock_test_state", "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, indirect=True,
) )
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -21,6 +21,7 @@ from homeassistant.const import (
CONF_SSL, CONF_SSL,
CONF_USERNAME, CONF_USERNAME,
) )
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -235,7 +236,26 @@ async def test_ssdp_already_configured(hass):
assert result["reason"] == "already_configured" 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.""" """Test ssdp abort when using a ipv6 address."""
MockConfigEntry( MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,

View File

@ -1,12 +1,16 @@
"""Test run history.""" """Test run history."""
from datetime import timedelta from datetime import timedelta
from unittest.mock import patch
from homeassistant.components import recorder from homeassistant.components import recorder
from homeassistant.components.recorder.db_schema import RecorderRuns from homeassistant.components.recorder.db_schema import RecorderRuns
from homeassistant.components.recorder.models import process_timestamp from homeassistant.components.recorder.models import process_timestamp
from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from tests.common import SetupRecorderInstanceT
async def test_run_history(recorder_mock, hass): async def test_run_history(recorder_mock, hass):
"""Test the run history gives the correct run.""" """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): async def test_run_history_while_recorder_is_not_yet_started(
"""Test the run history during schema migration.""" async_setup_recorder_instance: SetupRecorderInstanceT,
instance = recorder.get_instance(hass) 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 run_history = instance.run_history
assert run_history.current.start == run_history.recording_start assert run_history.current.start == run_history.recording_start
with instance.get_session() as session:
run_history.start(session) 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.start == run_history.recording_start
assert run_history.current.created >= run_history.recording_start assert run_history.current.created >= run_history.recording_start

View File

@ -1,12 +1,28 @@
"""Test Xiaomi binary sensors.""" """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.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 . import make_advertisement
from tests.common import MockConfigEntry from tests.common import MockConfigEntry, async_fire_time_changed
from tests.components.bluetooth import inject_bluetooth_service_info_bleak from tests.components.bluetooth import (
inject_bluetooth_service_info_bleak,
patch_all_discovered_devices,
)
async def test_door_problem_sensors(hass): 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 = hass.states.get("binary_sensor.door_lock_be98_door")
door_sensor_attribtes = door_sensor.attributes 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" 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 = hass.states.get("binary_sensor.door_lock_be98_door_left_open")
door_left_open_attribtes = door_left_open.attributes door_left_open_attribtes = door_left_open.attributes
assert door_left_open.state == "off" assert door_left_open.state == STATE_OFF
assert ( assert (
door_left_open_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Door left open" 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 = hass.states.get("binary_sensor.door_lock_be98_pry_the_door")
pry_the_door_attribtes = pry_the_door.attributes 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 pry_the_door_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Pry the door"
assert await hass.config_entries.async_unload(entry.entry_id) 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 = hass.states.get("binary_sensor.nightlight_9321_motion")
motion_sensor_attribtes = motion_sensor.attributes 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" assert motion_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Nightlight 9321 Motion"
light_sensor = hass.states.get("binary_sensor.nightlight_9321_light") light_sensor = hass.states.get("binary_sensor.nightlight_9321_light")
light_sensor_attribtes = light_sensor.attributes 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 light_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Nightlight 9321 Light"
assert await hass.config_entries.async_unload(entry.entry_id) 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 = hass.states.get("binary_sensor.smart_flower_pot_3e7a_moisture")
sensor_attr = sensor.attributes 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 sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 3E7A Moisture"
assert await hass.config_entries.async_unload(entry.entry_id) 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 = hass.states.get("binary_sensor.door_window_sensor_e567_opening")
opening_sensor_attribtes = opening_sensor.attributes opening_sensor_attribtes = opening_sensor.attributes
assert opening_sensor.state == "on"
assert opening_sensor.state == STATE_ON
assert ( assert (
opening_sensor_attribtes[ATTR_FRIENDLY_NAME] opening_sensor_attribtes[ATTR_FRIENDLY_NAME]
== "Door/Window Sensor E567 Opening" == "Door/Window Sensor E567 Opening"
) )
assert await hass.config_entries.async_unload(entry.entry_id) assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done() 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 = hass.states.get("binary_sensor.door_window_sensor_e567_opening")
opening_sensor_attribtes = opening_sensor.attributes opening_sensor_attribtes = opening_sensor.attributes
assert opening_sensor.state == "off" assert opening_sensor.state == STATE_OFF
assert ( assert (
opening_sensor_attribtes[ATTR_FRIENDLY_NAME] opening_sensor_attribtes[ATTR_FRIENDLY_NAME]
== "Door/Window Sensor E567 Opening" == "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" "binary_sensor.door_window_sensor_e567_door_left_open"
) )
door_left_open_attribtes = door_left_open.attributes door_left_open_attribtes = door_left_open.attributes
assert door_left_open.state == "off" assert door_left_open.state == STATE_OFF
assert ( assert (
door_left_open_attribtes[ATTR_FRIENDLY_NAME] door_left_open_attribtes[ATTR_FRIENDLY_NAME]
== "Door/Window Sensor E567 Door left open" == "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" "binary_sensor.door_window_sensor_e567_device_forcibly_removed"
) )
device_forcibly_removed_attribtes = device_forcibly_removed.attributes device_forcibly_removed_attribtes = device_forcibly_removed.attributes
assert device_forcibly_removed.state == "off" assert device_forcibly_removed.state == STATE_OFF
assert ( assert (
device_forcibly_removed_attribtes[ATTR_FRIENDLY_NAME] device_forcibly_removed_attribtes[ATTR_FRIENDLY_NAME]
== "Door/Window Sensor E567 Device forcibly removed" == "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 = hass.states.get("binary_sensor.thermometer_9cbc_smoke")
smoke_sensor_attribtes = smoke_sensor.attributes 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 smoke_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Thermometer 9CBC Smoke"
assert await hass.config_entries.async_unload(entry.entry_id) assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done() 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()