mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 02:37:08 +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",
|
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,
|
||||||
|
@ -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": [
|
||||||
{
|
{
|
||||||
|
@ -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."""
|
||||||
|
@ -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"],
|
||||||
|
@ -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,
|
||||||
|
@ -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",
|
||||||
|
@ -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)
|
||||||
|
@ -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"]
|
||||||
|
@ -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"
|
||||||
],
|
],
|
||||||
|
@ -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."""
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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": {
|
||||||
|
@ -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"
|
||||||
|
@ -988,7 +988,9 @@ 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."""
|
||||||
|
try:
|
||||||
self._close_event_session()
|
self._close_event_session()
|
||||||
|
finally:
|
||||||
self._close_connection()
|
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()
|
||||||
@ -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()
|
||||||
|
try:
|
||||||
self._end_session()
|
self._end_session()
|
||||||
|
finally:
|
||||||
self._close_connection()
|
self._close_connection()
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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,9 +275,8 @@ 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
|
||||||
@ -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:
|
||||||
|
for channel in channels:
|
||||||
async_dispatcher_send(hass, f"{webhook_id}_{channel}", {})
|
async_dispatcher_send(hass, f"{webhook_id}_{channel}", {})
|
||||||
|
@ -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",
|
||||||
|
@ -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"],
|
||||||
|
@ -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 (
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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": [
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
def _start_run_history():
|
||||||
with instance.get_session() as session:
|
with instance.get_session() as session:
|
||||||
run_history.start(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
|
||||||
|
@ -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()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user