mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
2023.7.3 (#96936)
This commit is contained in:
commit
40b5605caf
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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.",
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyfibaro"],
|
||||
"requirements": ["pyfibaro==0.7.1"]
|
||||
"requirements": ["pyfibaro==0.7.2"]
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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."""
|
||||
|
@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pymazda"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pymazda==0.3.9"]
|
||||
"requirements": ["pymazda==0.3.10"]
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
),
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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,
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
@ -29,5 +29,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"clear_notifications": {
|
||||
"name": "Clear notifications"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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"
|
||||
]
|
||||
|
@ -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__(
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -33,6 +33,7 @@ COMMENT_REQUIREMENTS = (
|
||||
"face-recognition",
|
||||
"opencv-python-headless",
|
||||
"pybluez",
|
||||
"pycocotools",
|
||||
"pycups",
|
||||
"python-eq3bt",
|
||||
"python-gammu",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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."""
|
||||
|
Loading…
x
Reference in New Issue
Block a user