This commit is contained in:
Franck Nijhof 2023-07-21 17:53:01 +02:00 committed by GitHub
commit 40b5605caf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 393 additions and 105 deletions

View File

@ -1,6 +1,7 @@
"""The Android TV Remote integration."""
from __future__ import annotations
import asyncio
import logging
from androidtvremote2 import (
@ -9,6 +10,7 @@ from androidtvremote2 import (
ConnectionClosed,
InvalidAuth,
)
import async_timeout
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, Platform
@ -43,11 +45,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
api.add_is_available_updated_callback(is_available_updated)
try:
await api.async_connect()
async with async_timeout.timeout(5.0):
await api.async_connect()
except InvalidAuth as exc:
# The Android TV is hard reset or the certificate and key files were deleted.
raise ConfigEntryAuthFailed from exc
except (CannotConnect, ConnectionClosed) as exc:
except (CannotConnect, ConnectionClosed, asyncio.TimeoutError) as exc:
# The Android TV is network unreachable. Raise exception and let Home Assistant retry
# later. If device gets a new IP address the zeroconf flow will update the config.
raise ConfigEntryNotReady from exc

View File

@ -139,7 +139,7 @@ async def async_migrate_unique_id(
dev_reg = dr.async_get(hass)
old_unique_id = config_entry.unique_id
new_unique_id = api.device.mac
new_name = api.device.values["name"]
new_name = api.device.values.get("name")
@callback
def _update_unique_id(entity_entry: er.RegistryEntry) -> dict[str, str] | None:

View File

@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["devolo_plc_api"],
"quality_scale": "platinum",
"requirements": ["devolo-plc-api==1.3.1"],
"requirements": ["devolo-plc-api==1.3.2"],
"zeroconf": [
{
"type": "_dvl-deviceapi._tcp.local.",

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env-canada==0.5.35"]
"requirements": ["env-canada==0.5.36"]
}

View File

@ -63,7 +63,10 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity):
self._attr_native_min_value = static_info.min_value
self._attr_native_max_value = static_info.max_value
self._attr_native_step = static_info.step
self._attr_native_unit_of_measurement = static_info.unit_of_measurement
# protobuf doesn't support nullable strings so we need to check
# if the string is empty
if unit_of_measurement := static_info.unit_of_measurement:
self._attr_native_unit_of_measurement = unit_of_measurement
if mode := static_info.mode:
self._attr_mode = NUMBER_MODES.from_esphome(mode)
else:

View File

@ -76,7 +76,10 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
super()._on_static_info_update(static_info)
static_info = self._static_info
self._attr_force_update = static_info.force_update
self._attr_native_unit_of_measurement = static_info.unit_of_measurement
# protobuf doesn't support nullable strings so we need to check
# if the string is empty
if unit_of_measurement := static_info.unit_of_measurement:
self._attr_native_unit_of_measurement = unit_of_measurement
self._attr_device_class = try_parse_enum(
SensorDeviceClass, static_info.device_class
)

View File

@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["pyfibaro"],
"requirements": ["pyfibaro==0.7.1"]
"requirements": ["pyfibaro==0.7.2"]
}

View File

@ -22,7 +22,12 @@ from .const import (
DOMAIN,
)
REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
REAUTH_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)
class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@ -42,18 +47,12 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) -> FlowResult:
"""Confirm re-authentication with Honeywell."""
errors: dict[str, str] = {}
assert self.entry is not None
if user_input:
assert self.entry is not None
password = user_input[CONF_PASSWORD]
data = {
CONF_USERNAME: self.entry.data[CONF_USERNAME],
CONF_PASSWORD: password,
}
try:
await self.is_valid(
username=data[CONF_USERNAME], password=data[CONF_PASSWORD]
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
)
except aiosomecomfort.AuthError:
@ -71,7 +70,7 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.entry,
data={
**self.entry.data,
CONF_PASSWORD: password,
**user_input,
},
)
await self.hass.config_entries.async_reload(self.entry.entry_id)
@ -79,7 +78,9 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="reauth_confirm",
data_schema=REAUTH_SCHEMA,
data_schema=self.add_suggested_values_to_schema(
REAUTH_SCHEMA, self.entry.data
),
errors=errors,
)

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/honeywell",
"iot_class": "cloud_polling",
"loggers": ["somecomfort"],
"requirements": ["AIOSomecomfort==0.0.14"]
"requirements": ["AIOSomecomfort==0.0.15"]
}

View File

@ -13,7 +13,7 @@ from typing import Any
from aioimaplib import AUTH, IMAP4_SSL, NONAUTH, SELECTED, AioImapException
import async_timeout
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_PASSWORD,
CONF_PORT,
@ -54,6 +54,7 @@ _LOGGER = logging.getLogger(__name__)
BACKOFF_TIME = 10
EVENT_IMAP = "imap_content"
MAX_ERRORS = 3
MAX_EVENT_DATA_BYTES = 32168
@ -174,6 +175,7 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]):
) -> None:
"""Initiate imap client."""
self.imap_client = imap_client
self.auth_errors: int = 0
self._last_message_id: str | None = None
self.custom_event_template = None
_custom_event_template = entry.data.get(CONF_CUSTOM_EVENT_DATA_TEMPLATE)
@ -315,7 +317,9 @@ class ImapPollingDataUpdateCoordinator(ImapDataUpdateCoordinator):
async def _async_update_data(self) -> int | None:
"""Update the number of unread emails."""
try:
return await self._async_fetch_number_of_messages()
messages = await self._async_fetch_number_of_messages()
self.auth_errors = 0
return messages
except (
AioImapException,
UpdateFailed,
@ -330,8 +334,15 @@ class ImapPollingDataUpdateCoordinator(ImapDataUpdateCoordinator):
self.async_set_update_error(ex)
raise ConfigEntryError("Selected mailbox folder is invalid.") from ex
except InvalidAuth as ex:
_LOGGER.warning("Username or password incorrect, starting reauthentication")
await self._cleanup()
self.auth_errors += 1
if self.auth_errors <= MAX_ERRORS:
_LOGGER.warning("Authentication failed, retrying")
else:
_LOGGER.warning(
"Username or password incorrect, starting reauthentication"
)
self.config_entry.async_start_reauth(self.hass)
self.async_set_update_error(ex)
raise ConfigEntryAuthFailed() from ex
@ -359,27 +370,28 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator):
async def _async_wait_push_loop(self) -> None:
"""Wait for data push from server."""
cleanup = False
while True:
try:
number_of_messages = await self._async_fetch_number_of_messages()
except InvalidAuth as ex:
self.auth_errors += 1
await self._cleanup()
_LOGGER.warning(
"Username or password incorrect, starting reauthentication"
)
self.config_entry.async_start_reauth(self.hass)
if self.auth_errors <= MAX_ERRORS:
_LOGGER.warning("Authentication failed, retrying")
else:
_LOGGER.warning(
"Username or password incorrect, starting reauthentication"
)
self.config_entry.async_start_reauth(self.hass)
self.async_set_update_error(ex)
await asyncio.sleep(BACKOFF_TIME)
except InvalidFolder as ex:
_LOGGER.warning("Selected mailbox folder is invalid")
await self._cleanup()
self.config_entry.async_set_state(
self.hass,
ConfigEntryState.SETUP_ERROR,
"Selected mailbox folder is invalid.",
)
self.async_set_update_error(ex)
await asyncio.sleep(BACKOFF_TIME)
continue
except (
UpdateFailed,
AioImapException,
@ -390,6 +402,7 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator):
await asyncio.sleep(BACKOFF_TIME)
continue
else:
self.auth_errors = 0
self.async_set_updated_data(number_of_messages)
try:
idle: asyncio.Future = await self.imap_client.idle_start()
@ -398,6 +411,10 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator):
async with async_timeout.timeout(10):
await idle
# From python 3.11 asyncio.TimeoutError is an alias of TimeoutError
except asyncio.CancelledError as ex:
cleanup = True
raise asyncio.CancelledError from ex
except (AioImapException, asyncio.TimeoutError):
_LOGGER.debug(
"Lost %s (will attempt to reconnect after %s s)",
@ -406,6 +423,9 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator):
)
await self._cleanup()
await asyncio.sleep(BACKOFF_TIME)
finally:
if cleanup:
await self._cleanup()
async def shutdown(self, *_: Any) -> None:
"""Close resources."""

View File

@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["pymazda"],
"quality_scale": "platinum",
"requirements": ["pymazda==0.3.9"]
"requirements": ["pymazda==0.3.10"]
}

View File

@ -53,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
info.setdefault("type", 101)
device_type = info["type"]
if device_type in [101, 106, 107]:
if device_type in [101, 106, 107, 120]:
device = _get_mystrom_switch(host)
platforms = PLATFORMS_SWITCH
await _async_get_device_state(device, info["ip"])
@ -86,7 +86,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
device_type = hass.data[DOMAIN][entry.entry_id].info["type"]
platforms = []
if device_type in [101, 106, 107]:
if device_type in [101, 106, 107, 120]:
platforms.extend(PLATFORMS_SWITCH)
elif device_type in [102, 105]:
platforms.extend(PLATFORMS_BULB)

View File

@ -387,8 +387,12 @@ class ONVIFDevice:
"WSPullPointSupport"
)
LOGGER.debug("%s: WSPullPointSupport: %s", self.name, pull_point_support)
# Even if the camera claims it does not support PullPoint, try anyway
# since at least some AXIS and Bosch models do. The reverse is also
# true where some cameras claim they support PullPoint but don't so
# the only way to know is to try.
return await self.events.async_start(
pull_point_support is not False,
True,
self.config_entry.options.get(
CONF_ENABLE_WEBHOOKS, DEFAULT_ENABLE_WEBHOOKS
),

View File

@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/onvif",
"iot_class": "local_push",
"loggers": ["onvif", "wsdiscovery", "zeep"],
"requirements": ["onvif-zeep-async==3.1.9", "WSDiscovery==2.0.0"]
"requirements": ["onvif-zeep-async==3.1.12", "WSDiscovery==2.0.0"]
}

View File

@ -50,8 +50,8 @@ async def _title(hass: HomeAssistant, discovery_info: HassioServiceInfo) -> str:
addon_info = await async_get_addon_info(hass, discovery_info.slug)
device = addon_info.get("options", {}).get("device")
if _is_yellow(hass) and device == "/dev/TTYAMA1":
return "Home Assistant Yellow"
if _is_yellow(hass) and device == "/dev/ttyAMA1":
return f"Home Assistant Yellow ({discovery_info.name})"
if device and "SkyConnect" in device:
return "Home Assistant SkyConnect"
@ -130,6 +130,11 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN):
url = f"http://{config['host']}:{config['port']}"
config_entry_data = {"url": url}
if self._async_in_progress(include_uninitialized=True):
# We currently don't handle multiple config entries, abort if hassio
# discovers multiple addons with otbr support
return self.async_abort(reason="single_instance_allowed")
if current_entries := self._async_current_entries():
for current_entry in current_entries:
if current_entry.source != SOURCE_HASSIO:

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/rainbird",
"iot_class": "local_polling",
"loggers": ["pyrainbird"],
"requirements": ["pyrainbird==2.1.0"]
"requirements": ["pyrainbird==3.0.0"]
}

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/roborock",
"iot_class": "local_polling",
"loggers": ["roborock"],
"requirements": ["python-roborock==0.30.0"]
"requirements": ["python-roborock==0.30.1"]
}

View File

@ -151,9 +151,10 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity):
@async_handle_api_call
async def async_turn_on_timer(self, key: str, value: bool) -> bool:
"""Make service call to api for setting timer."""
new_state = not self.device_data.device_on
data = {
"minutesFromNow": 60,
"acState": {**self.device_data.ac_states, "on": value},
"acState": {**self.device_data.ac_states, "on": new_state},
}
result = await self._client.async_set_timer(self._device_id, data)
return bool(result.get("status") == "success")

View File

@ -61,6 +61,8 @@ ENTITY_DESCRIPTION_ALARM = SIAAlarmControlPanelEntityDescription(
"OS": STATE_ALARM_DISARMED,
"NC": STATE_ALARM_ARMED_NIGHT,
"NL": STATE_ALARM_ARMED_NIGHT,
"NE": STATE_ALARM_ARMED_CUSTOM_BYPASS,
"NF": STATE_ALARM_ARMED_CUSTOM_BYPASS,
"BR": PREVIOUS_STATE,
"NP": PREVIOUS_STATE,
"NO": PREVIOUS_STATE,

View File

@ -127,6 +127,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY
)
_attr_name = None
def __init__(self, simplisafe: SimpliSafe, system: SystemType) -> None:
"""Initialize the SimpliSafe alarm."""

View File

@ -111,7 +111,6 @@ class BatteryBinarySensor(SimpliSafeEntity, BinarySensorEntity):
"""Initialize."""
super().__init__(simplisafe, system, device=device)
self._attr_name = "Battery"
self._attr_unique_id = f"{super().unique_id}-battery"
self._device: DeviceV3

View File

@ -44,7 +44,7 @@ async def _async_clear_notifications(system: System) -> None:
BUTTON_DESCRIPTIONS = (
SimpliSafeButtonDescription(
key=BUTTON_KIND_CLEAR_NOTIFICATIONS,
name="Clear notifications",
translation_key=BUTTON_KIND_CLEAR_NOTIFICATIONS,
push_action=_async_clear_notifications,
),
)

View File

@ -29,5 +29,12 @@
}
}
}
},
"entity": {
"button": {
"clear_notifications": {
"name": "Clear notifications"
}
}
}
}

View File

@ -62,7 +62,11 @@ def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None:
# Must have one of the min_required
if any(capability in capabilities for capability in min_required):
# Return all capabilities supported/consumed
return min_required + [Capability.battery, Capability.switch_level]
return min_required + [
Capability.battery,
Capability.switch_level,
Capability.window_shade_level,
]
return None
@ -74,12 +78,16 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity):
"""Initialize the cover class."""
super().__init__(device)
self._device_class = None
self._current_cover_position = None
self._state = None
self._state_attrs = None
self._attr_supported_features = (
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
)
if Capability.switch_level in device.capabilities:
if (
Capability.switch_level in device.capabilities
or Capability.window_shade_level in device.capabilities
):
self._attr_supported_features |= CoverEntityFeature.SET_POSITION
async def async_close_cover(self, **kwargs: Any) -> None:
@ -103,7 +111,12 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity):
if not self.supported_features & CoverEntityFeature.SET_POSITION:
return
# Do not set_status=True as device will report progress.
await self._device.set_level(kwargs[ATTR_POSITION], 0)
if Capability.window_shade_level in self._device.capabilities:
await self._device.set_window_shade_level(
kwargs[ATTR_POSITION], set_status=False
)
else:
await self._device.set_level(kwargs[ATTR_POSITION], set_status=False)
async def async_update(self) -> None:
"""Update the attrs of the cover."""
@ -117,6 +130,11 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity):
self._device_class = CoverDeviceClass.GARAGE
self._state = VALUE_TO_STATE.get(self._device.status.door)
if Capability.window_shade_level in self._device.capabilities:
self._current_cover_position = self._device.status.shade_level
elif Capability.switch_level in self._device.capabilities:
self._current_cover_position = self._device.status.level
self._state_attrs = {}
battery = self._device.status.attributes[Attribute.battery].value
if battery is not None:
@ -142,9 +160,7 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity):
@property
def current_cover_position(self) -> int | None:
"""Return current position of cover."""
if not self.supported_features & CoverEntityFeature.SET_POSITION:
return None
return self._device.status.level
return self._current_cover_position
@property
def device_class(self) -> CoverDeviceClass | None:

View File

@ -30,5 +30,5 @@
"documentation": "https://www.home-assistant.io/integrations/smartthings",
"iot_class": "cloud_push",
"loggers": ["httpsig", "pysmartapp", "pysmartthings"],
"requirements": ["pysmartapp==0.3.3", "pysmartthings==0.7.6"]
"requirements": ["pysmartapp==0.3.5", "pysmartthings==0.7.8"]
}

View File

@ -42,11 +42,12 @@ from async_upnp_client.utils import CaseInsensitiveDict
from homeassistant import config_entries
from homeassistant.components import network
from homeassistant.const import (
EVENT_HOMEASSISTANT_STARTED,
EVENT_HOMEASSISTANT_STOP,
MATCH_ALL,
__version__ as current_version,
)
from homeassistant.core import HomeAssistant, callback as core_callback
from homeassistant.core import Event, HomeAssistant, callback as core_callback
from homeassistant.data_entry_flow import BaseServiceInfo
from homeassistant.helpers import config_validation as cv, discovery_flow
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -728,15 +729,18 @@ class Server:
async def async_start(self) -> None:
"""Start the server."""
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop)
await self._async_start_upnp_servers()
bus = self.hass.bus
bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop)
bus.async_listen_once(
EVENT_HOMEASSISTANT_STARTED, self._async_start_upnp_servers
)
async def _async_get_instance_udn(self) -> str:
"""Get Unique Device Name for this instance."""
instance_id = await async_get_instance_id(self.hass)
return f"uuid:{instance_id[0:8]}-{instance_id[8:12]}-{instance_id[12:16]}-{instance_id[16:20]}-{instance_id[20:32]}".upper()
async def _async_start_upnp_servers(self) -> None:
async def _async_start_upnp_servers(self, event: Event) -> None:
"""Start the UPnP/SSDP servers."""
# Update UDN with our instance UDN.
udn = await self._async_get_instance_udn()

View File

@ -36,6 +36,7 @@ class StookalertBinarySensor(BinarySensorEntity):
_attr_attribution = "Data provided by rivm.nl"
_attr_device_class = BinarySensorDeviceClass.SAFETY
_attr_has_entity_name = True
_attr_name = None
def __init__(self, client: stookalert.stookalert, entry: ConfigEntry) -> None:
"""Initialize a Stookalert device."""

View File

@ -151,7 +151,7 @@ def find_moov(mp4_io: BufferedIOBase) -> int:
while 1:
mp4_io.seek(index)
box_header = mp4_io.read(8)
if len(box_header) != 8:
if len(box_header) != 8 or box_header[0:4] == b"\x00\x00\x00\x00":
raise HomeAssistantError("moov atom not found")
if box_header[4:8] == b"moov":
return index

View File

@ -205,7 +205,6 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
key="process",
name="Process",
icon=CPU_ICON,
state_class=SensorStateClass.MEASUREMENT,
mandatory_arg=True,
),
"processor_use": SysMonitorSensorEntityDescription(

View File

@ -8,7 +8,7 @@
"requirements": [
"tensorflow==2.5.0",
"tf-models-official==2.5.0",
"pycocotools==2.0.1",
"pycocotools==2.0.6",
"numpy==1.23.2",
"Pillow==9.5.0"
]

View File

@ -84,6 +84,7 @@ class ValloxFanEntity(ValloxEntity, FanEntity):
"""Representation of the fan."""
_attr_has_entity_name = True
_attr_name = None
_attr_supported_features = FanEntityFeature.PRESET_MODE | FanEntityFeature.SET_SPEED
def __init__(

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/vallox",
"iot_class": "local_polling",
"loggers": ["vallox_websocket_api"],
"requirements": ["vallox-websocket-api==3.2.1"]
"requirements": ["vallox-websocket-api==3.3.0"]
}

View File

@ -694,7 +694,7 @@ class ConfigEntry:
if self._on_unload is not None:
while self._on_unload:
if job := self._on_unload.pop()():
self._tasks.add(hass.async_create_task(job))
self.async_create_task(hass, job)
if not self._tasks and not self._background_tasks:
return

View File

@ -8,7 +8,7 @@ from .backports.enum import StrEnum
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 7
PATCH_VERSION: Final = "2"
PATCH_VERSION: Final = "3"
__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

@ -1,6 +1,6 @@
aiodiscover==1.4.16
aiohttp-cors==0.7.0
aiohttp==3.8.4
aiohttp==3.8.5
astral==2.2
async-timeout==4.0.2
async-upnp-client==0.33.2
@ -42,7 +42,7 @@ pyserial==3.5
python-slugify==4.0.1
PyTurboJPEG==1.6.7
pyudev==0.23.2
PyYAML==6.0
PyYAML==6.0.1
requests==2.31.0
scapy==2.5.0
SQLAlchemy==2.0.15

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2023.7.2"
version = "2023.7.3"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
@ -24,7 +24,7 @@ classifiers = [
]
requires-python = ">=3.10.0"
dependencies = [
"aiohttp==3.8.4",
"aiohttp==3.8.5",
"astral==2.2",
"async-timeout==4.0.2",
"attrs==22.2.0",
@ -48,7 +48,7 @@ dependencies = [
"orjson==3.9.1",
"pip>=21.3.1,<23.2",
"python-slugify==4.0.1",
"PyYAML==6.0",
"PyYAML==6.0.1",
"requests==2.31.0",
"typing_extensions>=4.6.3,<5.0",
"ulid-transform==0.7.2",

View File

@ -1,7 +1,7 @@
-c homeassistant/package_constraints.txt
# Home Assistant Core
aiohttp==3.8.4
aiohttp==3.8.5
astral==2.2
async-timeout==4.0.2
attrs==22.2.0
@ -21,7 +21,7 @@ pyOpenSSL==23.2.0
orjson==3.9.1
pip>=21.3.1,<23.2
python-slugify==4.0.1
PyYAML==6.0
PyYAML==6.0.1
requests==2.31.0
typing_extensions>=4.6.3,<5.0
ulid-transform==0.7.2

View File

@ -8,7 +8,7 @@ AEMET-OpenData==0.2.2
AIOAladdinConnect==0.1.56
# homeassistant.components.honeywell
AIOSomecomfort==0.0.14
AIOSomecomfort==0.0.15
# homeassistant.components.adax
Adax-local==0.1.5
@ -658,7 +658,7 @@ denonavr==0.11.2
devolo-home-control-api==0.18.2
# homeassistant.components.devolo_home_network
devolo-plc-api==1.3.1
devolo-plc-api==1.3.2
# homeassistant.components.directv
directv==0.4.0
@ -730,7 +730,7 @@ enocean==0.50
enturclient==0.2.4
# homeassistant.components.environment_canada
env-canada==0.5.35
env-canada==0.5.36
# homeassistant.components.enphase_envoy
envoy-reader==0.20.1
@ -1327,7 +1327,7 @@ ondilo==0.2.0
onkyo-eiscp==1.2.7
# homeassistant.components.onvif
onvif-zeep-async==3.1.9
onvif-zeep-async==3.1.12
# homeassistant.components.opengarage
open-garage==0.2.0
@ -1600,7 +1600,7 @@ pycketcasts==1.0.1
pycmus==0.1.1
# homeassistant.components.tensorflow
pycocotools==2.0.1
# pycocotools==2.0.6
# homeassistant.components.comfoconnect
pycomfoconnect==0.5.1
@ -1666,7 +1666,7 @@ pyevilgenius==2.0.0
pyezviz==0.2.1.2
# homeassistant.components.fibaro
pyfibaro==0.7.1
pyfibaro==0.7.2
# homeassistant.components.fido
pyfido==2.1.2
@ -1810,7 +1810,7 @@ pymailgunner==1.4
pymata-express==1.19
# homeassistant.components.mazda
pymazda==0.3.9
pymazda==0.3.10
# homeassistant.components.mediaroom
pymediaroom==0.6.5.4
@ -1938,7 +1938,7 @@ pyqwikswitch==0.93
pyrail==0.0.3
# homeassistant.components.rainbird
pyrainbird==2.1.0
pyrainbird==3.0.0
# homeassistant.components.recswitch
pyrecswitch==1.0.2
@ -2000,10 +2000,10 @@ pysma==0.7.3
pysmappee==0.2.29
# homeassistant.components.smartthings
pysmartapp==0.3.3
pysmartapp==0.3.5
# homeassistant.components.smartthings
pysmartthings==0.7.6
pysmartthings==0.7.8
# homeassistant.components.edl21
pysml==0.0.12
@ -2139,7 +2139,7 @@ python-qbittorrent==0.4.3
python-ripple-api==0.0.3
# homeassistant.components.roborock
python-roborock==0.30.0
python-roborock==0.30.1
# homeassistant.components.smarttub
python-smarttub==0.0.33
@ -2599,7 +2599,7 @@ url-normalize==1.4.3
uvcclient==0.11.0
# homeassistant.components.vallox
vallox-websocket-api==3.2.1
vallox-websocket-api==3.3.0
# homeassistant.components.rdw
vehicle==1.0.1

View File

@ -10,7 +10,7 @@ AEMET-OpenData==0.2.2
AIOAladdinConnect==0.1.56
# homeassistant.components.honeywell
AIOSomecomfort==0.0.14
AIOSomecomfort==0.0.15
# homeassistant.components.adax
Adax-local==0.1.5
@ -532,7 +532,7 @@ denonavr==0.11.2
devolo-home-control-api==0.18.2
# homeassistant.components.devolo_home_network
devolo-plc-api==1.3.1
devolo-plc-api==1.3.2
# homeassistant.components.directv
directv==0.4.0
@ -583,7 +583,7 @@ energyzero==0.4.1
enocean==0.50
# homeassistant.components.environment_canada
env-canada==0.5.35
env-canada==0.5.36
# homeassistant.components.enphase_envoy
envoy-reader==0.20.1
@ -1011,7 +1011,7 @@ omnilogic==0.4.5
ondilo==0.2.0
# homeassistant.components.onvif
onvif-zeep-async==3.1.9
onvif-zeep-async==3.1.12
# homeassistant.components.opengarage
open-garage==0.2.0
@ -1227,7 +1227,7 @@ pyevilgenius==2.0.0
pyezviz==0.2.1.2
# homeassistant.components.fibaro
pyfibaro==0.7.1
pyfibaro==0.7.2
# homeassistant.components.fido
pyfido==2.1.2
@ -1338,7 +1338,7 @@ pymailgunner==1.4
pymata-express==1.19
# homeassistant.components.mazda
pymazda==0.3.9
pymazda==0.3.10
# homeassistant.components.melcloud
pymelcloud==2.5.8
@ -1439,7 +1439,7 @@ pyps4-2ndscreen==1.3.1
pyqwikswitch==0.93
# homeassistant.components.rainbird
pyrainbird==2.1.0
pyrainbird==3.0.0
# homeassistant.components.risco
pyrisco==0.5.7
@ -1486,10 +1486,10 @@ pysma==0.7.3
pysmappee==0.2.29
# homeassistant.components.smartthings
pysmartapp==0.3.3
pysmartapp==0.3.5
# homeassistant.components.smartthings
pysmartthings==0.7.6
pysmartthings==0.7.8
# homeassistant.components.edl21
pysml==0.0.12
@ -1565,7 +1565,7 @@ python-picnic-api==1.1.0
python-qbittorrent==0.4.3
# homeassistant.components.roborock
python-roborock==0.30.0
python-roborock==0.30.1
# homeassistant.components.smarttub
python-smarttub==0.0.33
@ -1899,7 +1899,7 @@ url-normalize==1.4.3
uvcclient==0.11.0
# homeassistant.components.vallox
vallox-websocket-api==3.2.1
vallox-websocket-api==3.3.0
# homeassistant.components.rdw
vehicle==1.0.1

View File

@ -33,6 +33,7 @@ COMMENT_REQUIREMENTS = (
"face-recognition",
"opencv-python-headless",
"pybluez",
"pycocotools",
"pycups",
"python-eq3bt",
"python-gammu",

View File

@ -15,7 +15,7 @@ from homeassistant.components.number import (
DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN
from homeassistant.const import ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
@ -89,3 +89,36 @@ async def test_generic_number_nan(
state = hass.states.get("number.test_my_number")
assert state is not None
assert state.state == STATE_UNKNOWN
async def test_generic_number_with_unit_of_measurement_as_empty_string(
hass: HomeAssistant,
mock_client: APIClient,
mock_generic_device_entry,
) -> None:
"""Test a generic number entity with nan state."""
entity_info = [
NumberInfo(
object_id="mynumber",
key=1,
name="my number",
unique_id="my_number",
max_value=100,
min_value=0,
step=1,
unit_of_measurement="",
mode=ESPHomeNumberMode.SLIDER,
)
]
states = [NumberState(key=1, state=42)]
user_service = []
await mock_generic_device_entry(
mock_client=mock_client,
entity_info=entity_info,
user_service=user_service,
states=states,
)
state = hass.states.get("number.test_my_number")
assert state is not None
assert state.state == "42"
assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes

View File

@ -13,7 +13,7 @@ from aioesphomeapi import (
)
from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass
from homeassistant.const import ATTR_ICON, STATE_UNKNOWN
from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity import EntityCategory
@ -275,3 +275,30 @@ async def test_generic_text_sensor(
state = hass.states.get("sensor.test_my_sensor")
assert state is not None
assert state.state == "i am a teapot"
async def test_generic_numeric_sensor_empty_string_uom(
hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry
) -> None:
"""Test a generic numeric sensor that has an empty string as the uom."""
entity_info = [
SensorInfo(
object_id="mysensor",
key=1,
name="my sensor",
unique_id="my_sensor",
unit_of_measurement="",
)
]
states = [SensorState(key=1, state=123, missing_state=False)]
user_service = []
await mock_generic_device_entry(
mock_client=mock_client,
entity_info=entity_info,
user_service=user_service,
states=states,
)
state = hass.states.get("sensor.test_my_sensor")
assert state is not None
assert state.state == "123"
assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes

View File

@ -156,14 +156,14 @@ async def test_reauth_flow(hass: HomeAssistant) -> None:
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_PASSWORD: "new-password"},
{CONF_USERNAME: "new-username", CONF_PASSWORD: "new-password"},
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.ABORT
assert result2["reason"] == "reauth_successful"
assert mock_entry.data == {
CONF_USERNAME: "test-username",
CONF_USERNAME: "new-username",
CONF_PASSWORD: "new-password",
}
@ -200,7 +200,7 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, client: MagicMock) ->
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_PASSWORD: "new-password"},
{CONF_USERNAME: "new-username", CONF_PASSWORD: "new-password"},
)
await hass.async_block_till_done()
@ -246,7 +246,7 @@ async def test_reauth_flow_connnection_error(
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_PASSWORD: "new-password"},
{CONF_USERNAME: "new-username", CONF_PASSWORD: "new-password"},
)
await hass.async_block_till_done()

View File

@ -228,6 +228,48 @@ async def test_initial_invalid_folder_error(
assert (state is not None) == success
@patch("homeassistant.components.imap.coordinator.MAX_ERRORS", 1)
@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"])
async def test_late_authentication_retry(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
mock_imap_protocol: MagicMock,
) -> None:
"""Test retrying authentication after a search was failed."""
# Mock an error in waiting for a pushed update
mock_imap_protocol.wait_server_push.side_effect = AioImapException(
"Something went wrong"
)
config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
async_fire_time_changed(hass, utcnow() + timedelta(seconds=60))
await hass.async_block_till_done()
# Mock that the search fails, this will trigger
# that the connection will be restarted
# Then fail selecting the folder
mock_imap_protocol.search.return_value = Response(*BAD_RESPONSE)
mock_imap_protocol.login.side_effect = Response(*BAD_RESPONSE)
async_fire_time_changed(hass, utcnow() + timedelta(seconds=60))
await hass.async_block_till_done()
async_fire_time_changed(hass, utcnow() + timedelta(seconds=60))
await hass.async_block_till_done()
assert "Authentication failed, retrying" in caplog.text
# we still should have an entity with an unavailable state
state = hass.states.get("sensor.imap_email_email_com")
assert state is not None
assert state.state == STATE_UNAVAILABLE
@patch("homeassistant.components.imap.coordinator.MAX_ERRORS", 0)
@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"])
async def test_late_authentication_error(
hass: HomeAssistant,

View File

@ -68,7 +68,7 @@ async def test_init_switch_and_unload(
(110, "sensor", ConfigEntryState.SETUP_ERROR, True),
(113, "switch", ConfigEntryState.SETUP_ERROR, True),
(118, "button", ConfigEntryState.SETUP_ERROR, True),
(120, "switch", ConfigEntryState.SETUP_ERROR, True),
(120, "switch", ConfigEntryState.LOADED, False),
],
)
async def test_init_bulb(

View File

@ -23,6 +23,12 @@ HASSIO_DATA = hassio.HassioServiceInfo(
slug="otbr",
uuid="12345",
)
HASSIO_DATA_2 = hassio.HassioServiceInfo(
config={"host": "core-silabs-multiprotocol_2", "port": 8082},
name="Silicon Labs Multiprotocol",
slug="other_addon",
uuid="23456",
)
@pytest.fixture(name="addon_info")
@ -234,7 +240,7 @@ async def test_hassio_discovery_flow_yellow(
addon_info.return_value = {
"available": True,
"hostname": None,
"options": {"device": "/dev/TTYAMA1"},
"options": {"device": "/dev/ttyAMA1"},
"state": None,
"update_available": False,
"version": None,
@ -255,7 +261,7 @@ async def test_hassio_discovery_flow_yellow(
}
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == "Home Assistant Yellow"
assert result["title"] == "Home Assistant Yellow (Silicon Labs Multiprotocol)"
assert result["data"] == expected_data
assert result["options"] == {}
assert len(mock_setup_entry.mock_calls) == 1
@ -263,7 +269,7 @@ async def test_hassio_discovery_flow_yellow(
config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0]
assert config_entry.data == expected_data
assert config_entry.options == {}
assert config_entry.title == "Home Assistant Yellow"
assert config_entry.title == "Home Assistant Yellow (Silicon Labs Multiprotocol)"
assert config_entry.unique_id == HASSIO_DATA.uuid
@ -313,6 +319,80 @@ async def test_hassio_discovery_flow_sky_connect(
assert config_entry.unique_id == HASSIO_DATA.uuid
async def test_hassio_discovery_flow_2x_addons(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, addon_info
) -> None:
"""Test the hassio discovery flow when the user has 2 addons with otbr support."""
url1 = "http://core-silabs-multiprotocol:8081"
url2 = "http://core-silabs-multiprotocol_2:8081"
aioclient_mock.get(f"{url1}/node/dataset/active", text="aa")
aioclient_mock.get(f"{url2}/node/dataset/active", text="bb")
async def _addon_info(hass, slug):
await asyncio.sleep(0)
if slug == "otbr":
return {
"available": True,
"hostname": None,
"options": {
"device": (
"/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_"
"9e2adbd75b8beb119fe564a0f320645d-if00-port0"
)
},
"state": None,
"update_available": False,
"version": None,
}
return {
"available": True,
"hostname": None,
"options": {
"device": (
"/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_"
"9e2adbd75b8beb119fe564a0f320645d-if00-port1"
)
},
"state": None,
"update_available": False,
"version": None,
}
addon_info.side_effect = _addon_info
with patch(
"homeassistant.components.otbr.async_setup_entry",
return_value=True,
) as mock_setup_entry:
results = await asyncio.gather(
hass.config_entries.flow.async_init(
otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA
),
hass.config_entries.flow.async_init(
otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA_2
),
)
expected_data = {
"url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}",
}
assert results[0]["type"] == FlowResultType.CREATE_ENTRY
assert results[0]["title"] == "Home Assistant SkyConnect"
assert results[0]["data"] == expected_data
assert results[0]["options"] == {}
assert results[1]["type"] == FlowResultType.ABORT
assert results[1]["reason"] == "single_instance_allowed"
assert len(hass.config_entries.async_entries(otbr.DOMAIN)) == 1
assert len(mock_setup_entry.mock_calls) == 1
config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0]
assert config_entry.data == expected_data
assert config_entry.options == {}
assert config_entry.title == "Home Assistant SkyConnect"
assert config_entry.unique_id == HASSIO_DATA.uuid
async def test_hassio_discovery_flow_router_not_setup(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, addon_info
) -> None:

View File

@ -103,8 +103,10 @@ async def test_close(hass: HomeAssistant, device_factory) -> None:
assert state.state == STATE_CLOSING
async def test_set_cover_position(hass: HomeAssistant, device_factory) -> None:
"""Test the cover sets to the specific position."""
async def test_set_cover_position_switch_level(
hass: HomeAssistant, device_factory
) -> None:
"""Test the cover sets to the specific position for legacy devices that use Capability.switch_level."""
# Arrange
device = device_factory(
"Shade",
@ -130,6 +132,37 @@ async def test_set_cover_position(hass: HomeAssistant, device_factory) -> None:
assert device._api.post_device_command.call_count == 1 # type: ignore
async def test_set_cover_position(hass: HomeAssistant, device_factory) -> None:
"""Test the cover sets to the specific position."""
# Arrange
device = device_factory(
"Shade",
[Capability.window_shade, Capability.battery, Capability.window_shade_level],
{
Attribute.window_shade: "opening",
Attribute.battery: 95,
Attribute.shade_level: 10,
},
)
await setup_platform(hass, COVER_DOMAIN, devices=[device])
# Act
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_SET_COVER_POSITION,
{ATTR_POSITION: 50, "entity_id": "all"},
blocking=True,
)
state = hass.states.get("cover.shade")
# Result of call does not update state
assert state.state == STATE_OPENING
assert state.attributes[ATTR_BATTERY_LEVEL] == 95
assert state.attributes[ATTR_CURRENT_POSITION] == 10
# Ensure API called
assert device._api.post_device_command.call_count == 1 # type: ignore
async def test_set_cover_position_unsupported(
hass: HomeAssistant, device_factory
) -> None:

View File

@ -742,6 +742,8 @@ async def test_bind_failure_skips_adapter(
SsdpListener.async_start = _async_start
UpnpServer.async_start = _async_start
await init_ssdp_component(hass)
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert "Failed to setup listener for" in caplog.text

View File

@ -245,7 +245,7 @@ class FakePyAvBuffer:
# Forward to appropriate FakeStream
packet.stream.mux(packet)
# Make new init/part data available to the worker
self.memory_file.write(b"\x00\x00\x00\x00moov")
self.memory_file.write(b"\x00\x00\x00\x08moov")
def close(self):
"""Close the buffer."""