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

View File

@ -2,7 +2,7 @@
"domain": "august",
"name": "August",
"documentation": "https://www.home-assistant.io/integrations/august",
"requirements": ["yalexs==1.2.6", "yalexs_ble==1.12.8"],
"requirements": ["yalexs==1.2.6", "yalexs_ble==1.12.12"],
"codeowners": ["@bdraco"],
"dhcp": [
{

View File

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

View File

@ -3,7 +3,7 @@
"name": "ESPHome",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/esphome",
"requirements": ["aioesphomeapi==13.1.0", "esphome-dashboard-api==1.2.3"],
"requirements": ["aioesphomeapi==13.3.1", "esphome-dashboard-api==1.2.3"],
"zeroconf": ["_esphomelib._tcp.local."],
"dhcp": [{ "registered_devices": true }],
"codeowners": ["@OttoWinter", "@jesserockz"],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -135,6 +135,9 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
_LOGGER.debug("Netgear ssdp discovery info: %s", discovery_info)
if ssdp.ATTR_UPNP_SERIAL not in discovery_info.upnp:
return self.async_abort(reason="no_serial")
await self.async_set_unique_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL])
self._abort_if_unique_id_configured(updates=updated_data)

View File

@ -14,7 +14,9 @@
"config": "Connection or login error: please check your configuration"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"not_ipv4_address": "No IPv4 address in ssdp discovery information",
"no_serial": "No serial number in ssdp discovery information"
}
},
"options": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
"""Support for Xiaomi binary sensors."""
from __future__ import annotations
from xiaomi_ble import SLEEPY_DEVICE_MODELS
from xiaomi_ble.parser import (
BinarySensorDeviceClass as XiaomiBinarySensorDeviceClass,
ExtendedBinarySensorDeviceClass,
@ -19,6 +20,7 @@ from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothProcessorCoordinator,
PassiveBluetoothProcessorEntity,
)
from homeassistant.const import ATTR_MODEL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info
@ -128,3 +130,12 @@ class XiaomiBluetoothSensorEntity(
def is_on(self) -> bool | None:
"""Return the native value."""
return self.processor.entity_data.get(self.entity_key)
@property
def available(self) -> bool:
"""Return True if entity is available."""
if self.device_info and self.device_info[ATTR_MODEL] in SLEEPY_DEVICE_MODELS:
# These devices sleep for an indeterminate amount of time
# so there is no way to track their availability.
return True
return super().available

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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)
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"

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("state_class") is None
assert state.attributes.get("unit_of_measurement") is None
async def test_last_sensor(hass: HomeAssistant) -> None:
"""Test the last sensor."""
config = {
SENSOR_DOMAIN: {
"platform": GROUP_DOMAIN,
"name": "test_last",
"type": "last",
"entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
"unique_id": "very_unique_id_last_sensor",
}
}
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
entity_ids = config["sensor"]["entities"]
for entity_id, value in dict(zip(entity_ids, VALUES)).items():
hass.states.async_set(entity_id, value)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_last")
assert str(float(value)) == state.state
assert entity_id == state.attributes.get("last_entity_id")

View File

@ -855,7 +855,7 @@ async def test_wrap_sensor(hass, mock_do_cycle, expected):
@pytest.mark.parametrize(
"mock_test_state",
[(State(ENTITY_ID, "117"), State(f"{ENTITY_ID}_1", "119"))],
[(State(ENTITY_ID, "unknown"), State(f"{ENTITY_ID}_1", "119"))],
indirect=True,
)
@pytest.mark.parametrize(

View File

@ -21,6 +21,7 @@ from homeassistant.const import (
CONF_SSL,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@ -235,7 +236,26 @@ async def test_ssdp_already_configured(hass):
assert result["reason"] == "already_configured"
async def test_ssdp_ipv6(hass):
async def test_ssdp_no_serial(hass: HomeAssistant) -> None:
"""Test ssdp abort when the ssdp info does not include a serial number."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_SSDP},
data=ssdp.SsdpServiceInfo(
ssdp_usn="mock_usn",
ssdp_st="mock_st",
ssdp_location=SSDP_URL,
upnp={
ssdp.ATTR_UPNP_MODEL_NUMBER: "RBR20",
ssdp.ATTR_UPNP_PRESENTATION_URL: URL,
},
),
)
assert result["type"] == data_entry_flow.FlowResultType.ABORT
assert result["reason"] == "no_serial"
async def test_ssdp_ipv6(hass: HomeAssistant) -> None:
"""Test ssdp abort when using a ipv6 address."""
MockConfigEntry(
domain=DOMAIN,

View File

@ -1,12 +1,16 @@
"""Test run history."""
from datetime import timedelta
from unittest.mock import patch
from homeassistant.components import recorder
from homeassistant.components.recorder.db_schema import RecorderRuns
from homeassistant.components.recorder.models import process_timestamp
from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util
from tests.common import SetupRecorderInstanceT
async def test_run_history(recorder_mock, hass):
"""Test the run history gives the correct run."""
@ -47,12 +51,32 @@ async def test_run_history(recorder_mock, hass):
)
async def test_run_history_during_schema_migration(recorder_mock, hass):
"""Test the run history during schema migration."""
instance = recorder.get_instance(hass)
async def test_run_history_while_recorder_is_not_yet_started(
async_setup_recorder_instance: SetupRecorderInstanceT,
hass: HomeAssistant,
recorder_db_url: str,
) -> None:
"""Test the run history while recorder is not yet started.
This usually happens during schema migration because
we do not start right away.
"""
# Prevent the run history from starting to ensure
# we can test run_history.current.start returns the expected value
with patch(
"homeassistant.components.recorder.run_history.RunHistory.start",
):
instance = await async_setup_recorder_instance(hass)
run_history = instance.run_history
assert run_history.current.start == run_history.recording_start
def _start_run_history():
with instance.get_session() as session:
run_history.start(session)
# Ideally we would run run_history.start in the recorder thread
# but since we mocked it out above, we run it directly here
# via the database executor to avoid blocking the event loop.
await instance.async_add_executor_job(_start_run_history)
assert run_history.current.start == run_history.recording_start
assert run_history.current.created >= run_history.recording_start

View File

@ -1,12 +1,28 @@
"""Test Xiaomi binary sensors."""
from datetime import timedelta
import time
from unittest.mock import patch
from homeassistant.components.bluetooth import (
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
)
from homeassistant.components.xiaomi_ble.const import DOMAIN
from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.const import (
ATTR_FRIENDLY_NAME,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
)
from homeassistant.util import dt as dt_util
from . import make_advertisement
from tests.common import MockConfigEntry
from tests.components.bluetooth import inject_bluetooth_service_info_bleak
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.components.bluetooth import (
inject_bluetooth_service_info_bleak,
patch_all_discovered_devices,
)
async def test_door_problem_sensors(hass):
@ -34,19 +50,19 @@ async def test_door_problem_sensors(hass):
door_sensor = hass.states.get("binary_sensor.door_lock_be98_door")
door_sensor_attribtes = door_sensor.attributes
assert door_sensor.state == "off"
assert door_sensor.state == STATE_OFF
assert door_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Door"
door_left_open = hass.states.get("binary_sensor.door_lock_be98_door_left_open")
door_left_open_attribtes = door_left_open.attributes
assert door_left_open.state == "off"
assert door_left_open.state == STATE_OFF
assert (
door_left_open_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Door left open"
)
pry_the_door = hass.states.get("binary_sensor.door_lock_be98_pry_the_door")
pry_the_door_attribtes = pry_the_door.attributes
assert pry_the_door.state == "off"
assert pry_the_door.state == STATE_OFF
assert pry_the_door_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Pry the door"
assert await hass.config_entries.async_unload(entry.entry_id)
@ -77,12 +93,12 @@ async def test_light_motion(hass):
motion_sensor = hass.states.get("binary_sensor.nightlight_9321_motion")
motion_sensor_attribtes = motion_sensor.attributes
assert motion_sensor.state == "on"
assert motion_sensor.state == STATE_ON
assert motion_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Nightlight 9321 Motion"
light_sensor = hass.states.get("binary_sensor.nightlight_9321_light")
light_sensor_attribtes = light_sensor.attributes
assert light_sensor.state == "off"
assert light_sensor.state == STATE_OFF
assert light_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Nightlight 9321 Light"
assert await hass.config_entries.async_unload(entry.entry_id)
@ -116,7 +132,7 @@ async def test_moisture(hass):
sensor = hass.states.get("binary_sensor.smart_flower_pot_3e7a_moisture")
sensor_attr = sensor.attributes
assert sensor.state == "on"
assert sensor.state == STATE_ON
assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 3E7A Moisture"
assert await hass.config_entries.async_unload(entry.entry_id)
@ -148,12 +164,12 @@ async def test_opening(hass):
opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening")
opening_sensor_attribtes = opening_sensor.attributes
assert opening_sensor.state == "on"
assert opening_sensor.state == STATE_ON
assert (
opening_sensor_attribtes[ATTR_FRIENDLY_NAME]
== "Door/Window Sensor E567 Opening"
)
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
@ -183,7 +199,7 @@ async def test_opening_problem_sensors(hass):
opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening")
opening_sensor_attribtes = opening_sensor.attributes
assert opening_sensor.state == "off"
assert opening_sensor.state == STATE_OFF
assert (
opening_sensor_attribtes[ATTR_FRIENDLY_NAME]
== "Door/Window Sensor E567 Opening"
@ -193,7 +209,7 @@ async def test_opening_problem_sensors(hass):
"binary_sensor.door_window_sensor_e567_door_left_open"
)
door_left_open_attribtes = door_left_open.attributes
assert door_left_open.state == "off"
assert door_left_open.state == STATE_OFF
assert (
door_left_open_attribtes[ATTR_FRIENDLY_NAME]
== "Door/Window Sensor E567 Door left open"
@ -203,7 +219,7 @@ async def test_opening_problem_sensors(hass):
"binary_sensor.door_window_sensor_e567_device_forcibly_removed"
)
device_forcibly_removed_attribtes = device_forcibly_removed.attributes
assert device_forcibly_removed.state == "off"
assert device_forcibly_removed.state == STATE_OFF
assert (
device_forcibly_removed_attribtes[ATTR_FRIENDLY_NAME]
== "Door/Window Sensor E567 Device forcibly removed"
@ -238,8 +254,111 @@ async def test_smoke(hass):
smoke_sensor = hass.states.get("binary_sensor.thermometer_9cbc_smoke")
smoke_sensor_attribtes = smoke_sensor.attributes
assert smoke_sensor.state == "on"
assert smoke_sensor.state == STATE_ON
assert smoke_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Thermometer 9CBC Smoke"
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
async def test_unavailable(hass):
"""Test normal device goes to unavailable after 60 minutes."""
start_monotonic = time.monotonic()
entry = MockConfigEntry(
domain=DOMAIN,
unique_id="A4:C1:38:66:E5:67",
data={"bindkey": "0fdcc30fe9289254876b5ef7c11ef1f0"},
)
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
inject_bluetooth_service_info_bleak(
hass,
make_advertisement(
"A4:C1:38:66:E5:67",
b"XY\x89\x18\x9ag\xe5f8\xc1\xa4\x9d\xd9z\xf3&\x00\x00\xc8\xa6\x0b\xd5",
),
)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening")
assert opening_sensor.state == STATE_ON
# Fastforward time without BLE advertisements
monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1
with patch(
"homeassistant.components.bluetooth.manager.MONOTONIC_TIME",
return_value=monotonic_now,
), patch_all_discovered_devices([]):
async_fire_time_changed(
hass,
dt_util.utcnow()
+ timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1),
)
await hass.async_block_till_done()
opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening")
# Normal devices should go to unavailable
assert opening_sensor.state == STATE_UNAVAILABLE
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
async def test_sleepy_device(hass):
"""Test sleepy device does not go to unavailable after 60 minutes."""
start_monotonic = time.monotonic()
entry = MockConfigEntry(
domain=DOMAIN,
unique_id="A4:C1:38:66:E5:67",
)
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
inject_bluetooth_service_info_bleak(
hass,
make_advertisement(
"A4:C1:38:66:E5:67",
b"@0\xd6\x03$\x19\x10\x01\x00",
),
)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening")
assert opening_sensor.state == STATE_ON
# Fastforward time without BLE advertisements
monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1
with patch(
"homeassistant.components.bluetooth.manager.MONOTONIC_TIME",
return_value=monotonic_now,
), patch_all_discovered_devices([]):
async_fire_time_changed(
hass,
dt_util.utcnow()
+ timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1),
)
await hass.async_block_till_done()
opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening")
# Sleepy devices should keep their state over time
assert opening_sensor.state == STATE_ON
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()