mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
2023.5.3 (#93066)
This commit is contained in:
commit
e0a97ec90d
@ -783,6 +783,7 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/netdata/ @fabaff
|
||||
/homeassistant/components/netgear/ @hacf-fr @Quentame @starkillerOG
|
||||
/tests/components/netgear/ @hacf-fr @Quentame @starkillerOG
|
||||
/homeassistant/components/netgear_lte/ @tkdrob
|
||||
/homeassistant/components/network/ @home-assistant/core
|
||||
/tests/components/network/ @home-assistant/core
|
||||
/homeassistant/components/nexia/ @bdraco
|
||||
|
@ -3,12 +3,12 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any, Final
|
||||
|
||||
from aioairzone.common import OperationMode
|
||||
from aioairzone.common import OperationAction, OperationMode
|
||||
from aioairzone.const import (
|
||||
API_MODE,
|
||||
API_ON,
|
||||
API_SET_POINT,
|
||||
AZD_DEMAND,
|
||||
AZD_ACTION,
|
||||
AZD_HUMIDITY,
|
||||
AZD_MASTER,
|
||||
AZD_MODE,
|
||||
@ -39,12 +39,13 @@ from .const import API_TEMPERATURE_STEP, DOMAIN, TEMP_UNIT_LIB_TO_HASS
|
||||
from .coordinator import AirzoneUpdateCoordinator
|
||||
from .entity import AirzoneZoneEntity
|
||||
|
||||
HVAC_ACTION_LIB_TO_HASS: Final[dict[OperationMode, HVACAction]] = {
|
||||
OperationMode.STOP: HVACAction.OFF,
|
||||
OperationMode.COOLING: HVACAction.COOLING,
|
||||
OperationMode.HEATING: HVACAction.HEATING,
|
||||
OperationMode.FAN: HVACAction.FAN,
|
||||
OperationMode.DRY: HVACAction.DRYING,
|
||||
HVAC_ACTION_LIB_TO_HASS: Final[dict[OperationAction, HVACAction]] = {
|
||||
OperationAction.COOLING: HVACAction.COOLING,
|
||||
OperationAction.DRYING: HVACAction.DRYING,
|
||||
OperationAction.FAN: HVACAction.FAN,
|
||||
OperationAction.HEATING: HVACAction.HEATING,
|
||||
OperationAction.IDLE: HVACAction.IDLE,
|
||||
OperationAction.OFF: HVACAction.OFF,
|
||||
}
|
||||
HVAC_MODE_LIB_TO_HASS: Final[dict[OperationMode, HVACMode]] = {
|
||||
OperationMode.STOP: HVACMode.OFF,
|
||||
@ -156,14 +157,13 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
||||
"""Update climate attributes."""
|
||||
self._attr_current_temperature = self.get_airzone_value(AZD_TEMP)
|
||||
self._attr_current_humidity = self.get_airzone_value(AZD_HUMIDITY)
|
||||
self._attr_hvac_action = HVAC_ACTION_LIB_TO_HASS[
|
||||
self.get_airzone_value(AZD_ACTION)
|
||||
]
|
||||
if self.get_airzone_value(AZD_ON):
|
||||
mode = self.get_airzone_value(AZD_MODE)
|
||||
self._attr_hvac_mode = HVAC_MODE_LIB_TO_HASS[mode]
|
||||
if self.get_airzone_value(AZD_DEMAND):
|
||||
self._attr_hvac_action = HVAC_ACTION_LIB_TO_HASS[mode]
|
||||
self._attr_hvac_mode = HVAC_MODE_LIB_TO_HASS[
|
||||
self.get_airzone_value(AZD_MODE)
|
||||
]
|
||||
else:
|
||||
self._attr_hvac_action = HVACAction.IDLE
|
||||
else:
|
||||
self._attr_hvac_action = HVACAction.OFF
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET)
|
||||
|
@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==0.5.2"]
|
||||
"requirements": ["aioairzone==0.5.5"]
|
||||
}
|
||||
|
@ -17,11 +17,12 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class AbstractConfig(ABC):
|
||||
"""Hold the configuration for Alexa."""
|
||||
|
||||
_unsub_proactive_report: asyncio.Task[CALLBACK_TYPE] | None = None
|
||||
_unsub_proactive_report: CALLBACK_TYPE | None = None
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize abstract config."""
|
||||
self.hass = hass
|
||||
self._enable_proactive_mode_lock = asyncio.Lock()
|
||||
self._store = None
|
||||
|
||||
async def async_initialize(self):
|
||||
@ -67,20 +68,17 @@ class AbstractConfig(ABC):
|
||||
async def async_enable_proactive_mode(self):
|
||||
"""Enable proactive mode."""
|
||||
_LOGGER.debug("Enable proactive mode")
|
||||
if self._unsub_proactive_report is None:
|
||||
self._unsub_proactive_report = self.hass.async_create_task(
|
||||
async_enable_proactive_mode(self.hass, self)
|
||||
async with self._enable_proactive_mode_lock:
|
||||
if self._unsub_proactive_report is not None:
|
||||
return
|
||||
self._unsub_proactive_report = await async_enable_proactive_mode(
|
||||
self.hass, self
|
||||
)
|
||||
try:
|
||||
await self._unsub_proactive_report
|
||||
except Exception:
|
||||
self._unsub_proactive_report = None
|
||||
raise
|
||||
|
||||
async def async_disable_proactive_mode(self):
|
||||
"""Disable proactive mode."""
|
||||
_LOGGER.debug("Disable proactive mode")
|
||||
if unsub_func := await self._unsub_proactive_report:
|
||||
if unsub_func := self._unsub_proactive_report:
|
||||
unsub_func()
|
||||
self._unsub_proactive_report = None
|
||||
|
||||
|
@ -18,7 +18,7 @@
|
||||
"bleak==0.20.2",
|
||||
"bleak-retry-connector==3.0.2",
|
||||
"bluetooth-adapters==0.15.3",
|
||||
"bluetooth-auto-recovery==1.1.2",
|
||||
"bluetooth-auto-recovery==1.2.0",
|
||||
"bluetooth-data-tools==0.4.0",
|
||||
"dbus-fast==1.85.0"
|
||||
]
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["bimmer_connected"],
|
||||
"requirements": ["bimmer_connected==0.13.2"]
|
||||
"requirements": ["bimmer_connected==0.13.3"]
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol"],
|
||||
"requirements": [
|
||||
"aioesphomeapi==13.7.3",
|
||||
"aioesphomeapi==13.7.4",
|
||||
"bluetooth-data-tools==0.4.0",
|
||||
"esphome-dashboard-api==1.2.3"
|
||||
],
|
||||
|
@ -283,7 +283,7 @@ class FritzBoxTools(
|
||||
entity_data["entity_states"][
|
||||
key
|
||||
] = await self.hass.async_add_executor_job(
|
||||
update_fn, self.fritz_status, self.data.get(key)
|
||||
update_fn, self.fritz_status, self.data["entity_states"].get(key)
|
||||
)
|
||||
if self.has_call_deflections:
|
||||
entity_data[
|
||||
|
@ -174,7 +174,10 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
await super().async_added_to_hass()
|
||||
if state := await self.async_get_last_state():
|
||||
if (state := await self.async_get_last_state()) is not None:
|
||||
if state.state == STATE_UNAVAILABLE:
|
||||
self._attr_available = False
|
||||
elif state.state != STATE_UNKNOWN:
|
||||
try:
|
||||
self._state = Decimal(state.state)
|
||||
except (DecimalException, ValueError) as err:
|
||||
@ -184,12 +187,9 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
|
||||
state.state,
|
||||
err,
|
||||
)
|
||||
else:
|
||||
|
||||
self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
if self._unit_of_measurement is None:
|
||||
self._unit_of_measurement = state.attributes.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT
|
||||
)
|
||||
self._unit_of_measurement = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
@callback
|
||||
def calc_integration(event: Event) -> None:
|
||||
|
@ -11,6 +11,7 @@ from typing import Any, cast
|
||||
|
||||
from aiolifx.aiolifx import (
|
||||
Light,
|
||||
Message,
|
||||
MultiZoneDirection,
|
||||
MultiZoneEffectType,
|
||||
TileEffectType,
|
||||
@ -56,6 +57,8 @@ from .util import (
|
||||
LIGHT_UPDATE_INTERVAL = 10
|
||||
REQUEST_REFRESH_DELAY = 0.35
|
||||
LIFX_IDENTIFY_DELAY = 3.0
|
||||
ZONES_PER_COLOR_UPDATE_REQUEST = 8
|
||||
|
||||
RSSI_DBM_FW = AwesomeVersion("2.77")
|
||||
|
||||
|
||||
@ -208,18 +211,50 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
def get_number_of_zones(self) -> int:
|
||||
"""Return the number of zones.
|
||||
|
||||
If the number of zones is not yet populated, return 0
|
||||
If the number of zones is not yet populated, return 1 since
|
||||
the device will have a least one zone.
|
||||
"""
|
||||
return len(self.device.color_zones) if self.device.color_zones else 0
|
||||
return len(self.device.color_zones) if self.device.color_zones else 1
|
||||
|
||||
@callback
|
||||
def _async_build_color_zones_update_requests(self) -> list[Callable]:
|
||||
"""Build a color zones update request."""
|
||||
device = self.device
|
||||
return [
|
||||
partial(device.get_color_zones, start_index=zone)
|
||||
for zone in range(0, self.get_number_of_zones(), 8)
|
||||
]
|
||||
calls: list[Callable] = []
|
||||
for zone in range(
|
||||
0, self.get_number_of_zones(), ZONES_PER_COLOR_UPDATE_REQUEST
|
||||
):
|
||||
|
||||
def _wrap_get_color_zones(
|
||||
callb: Callable[[Message, dict[str, Any] | None], None],
|
||||
get_color_zones_args: dict[str, Any],
|
||||
) -> None:
|
||||
"""Capture the callback and make sure resp_set_multizonemultizone is called before."""
|
||||
|
||||
def _wrapped_callback(
|
||||
bulb: Light,
|
||||
response: Message,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
# We need to call resp_set_multizonemultizone to populate
|
||||
# the color_zones attribute before calling the callback
|
||||
device.resp_set_multizonemultizone(response)
|
||||
# Now call the original callback
|
||||
callb(bulb, response, **kwargs)
|
||||
|
||||
device.get_color_zones(**get_color_zones_args, callb=_wrapped_callback)
|
||||
|
||||
calls.append(
|
||||
partial(
|
||||
_wrap_get_color_zones,
|
||||
get_color_zones_args={
|
||||
"start_index": zone,
|
||||
"end_index": zone + ZONES_PER_COLOR_UPDATE_REQUEST - 1,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return calls
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Fetch all device data from the api."""
|
||||
|
@ -51,7 +51,7 @@ PLATFORMS = [
|
||||
]
|
||||
|
||||
|
||||
async def with_timeout(task, timeout_seconds=10):
|
||||
async def with_timeout(task, timeout_seconds=30):
|
||||
"""Run an async task with a timeout."""
|
||||
async with async_timeout.timeout(timeout_seconds):
|
||||
return await task
|
||||
|
@ -1,9 +1,9 @@
|
||||
{
|
||||
"domain": "netgear_lte",
|
||||
"name": "NETGEAR LTE",
|
||||
"codeowners": [],
|
||||
"codeowners": ["@tkdrob"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/netgear_lte",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eternalegypt"],
|
||||
"requirements": ["eternalegypt==0.0.15"]
|
||||
"requirements": ["eternalegypt==0.0.16"]
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field, fields
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import traceback
|
||||
@ -10,9 +10,16 @@ from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from aionotion import async_get_client
|
||||
from aionotion.bridge.models import Bridge
|
||||
from aionotion.bridge.models import Bridge, BridgeAllResponse
|
||||
from aionotion.errors import InvalidCredentialsError, NotionError
|
||||
from aionotion.sensor.models import Listener, ListenerKind, Sensor
|
||||
from aionotion.sensor.models import (
|
||||
Listener,
|
||||
ListenerAllResponse,
|
||||
ListenerKind,
|
||||
Sensor,
|
||||
SensorAllResponse,
|
||||
)
|
||||
from aionotion.user.models import UserPreferences, UserPreferencesResponse
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
@ -51,6 +58,11 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
ATTR_SYSTEM_MODE = "system_mode"
|
||||
ATTR_SYSTEM_NAME = "system_name"
|
||||
|
||||
DATA_BRIDGES = "bridges"
|
||||
DATA_LISTENERS = "listeners"
|
||||
DATA_SENSORS = "sensors"
|
||||
DATA_USER_PREFERENCES = "user_preferences"
|
||||
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
||||
@ -84,6 +96,9 @@ def is_uuid(value: str) -> bool:
|
||||
class NotionData:
|
||||
"""Define a manager class for Notion data."""
|
||||
|
||||
hass: HomeAssistant
|
||||
entry: ConfigEntry
|
||||
|
||||
# Define a dict of bridges, indexed by bridge ID (an integer):
|
||||
bridges: dict[int, Bridge] = field(default_factory=dict)
|
||||
|
||||
@ -93,12 +108,40 @@ class NotionData:
|
||||
# Define a dict of sensors, indexed by sensor UUID (a string):
|
||||
sensors: dict[str, Sensor] = field(default_factory=dict)
|
||||
|
||||
# Define a user preferences response object:
|
||||
user_preferences: UserPreferences | None = field(default=None)
|
||||
|
||||
def update_data_from_response(
|
||||
self,
|
||||
response: BridgeAllResponse
|
||||
| ListenerAllResponse
|
||||
| SensorAllResponse
|
||||
| UserPreferencesResponse,
|
||||
) -> None:
|
||||
"""Update data from an aionotion response."""
|
||||
if isinstance(response, BridgeAllResponse):
|
||||
for bridge in response.bridges:
|
||||
# If a new bridge is discovered, register it:
|
||||
if bridge.id not in self.bridges:
|
||||
_async_register_new_bridge(self.hass, self.entry, bridge)
|
||||
self.bridges[bridge.id] = bridge
|
||||
elif isinstance(response, ListenerAllResponse):
|
||||
self.listeners = {listener.id: listener for listener in response.listeners}
|
||||
elif isinstance(response, SensorAllResponse):
|
||||
self.sensors = {sensor.uuid: sensor for sensor in response.sensors}
|
||||
elif isinstance(response, UserPreferencesResponse):
|
||||
self.user_preferences = response.user_preferences
|
||||
|
||||
def asdict(self) -> dict[str, Any]:
|
||||
"""Represent this dataclass (and its Pydantic contents) as a dict."""
|
||||
return {
|
||||
field.name: [obj.dict() for obj in getattr(self, field.name).values()]
|
||||
for field in fields(self)
|
||||
data: dict[str, Any] = {
|
||||
DATA_BRIDGES: [bridge.dict() for bridge in self.bridges.values()],
|
||||
DATA_LISTENERS: [listener.dict() for listener in self.listeners.values()],
|
||||
DATA_SENSORS: [sensor.dict() for sensor in self.sensors.values()],
|
||||
}
|
||||
if self.user_preferences:
|
||||
data[DATA_USER_PREFERENCES] = self.user_preferences.dict()
|
||||
return data
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@ -121,11 +164,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_update() -> NotionData:
|
||||
"""Get the latest data from the Notion API."""
|
||||
data = NotionData()
|
||||
data = NotionData(hass=hass, entry=entry)
|
||||
tasks = {
|
||||
"bridges": client.bridge.async_all(),
|
||||
"listeners": client.sensor.async_listeners(),
|
||||
"sensors": client.sensor.async_all(),
|
||||
DATA_BRIDGES: client.bridge.async_all(),
|
||||
DATA_LISTENERS: client.sensor.async_listeners(),
|
||||
DATA_SENSORS: client.sensor.async_all(),
|
||||
DATA_USER_PREFERENCES: client.user.async_preferences(),
|
||||
}
|
||||
|
||||
results = await asyncio.gather(*tasks.values(), return_exceptions=True)
|
||||
@ -145,16 +189,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
f"There was an unknown error while updating {attr}: {result}"
|
||||
) from result
|
||||
|
||||
for item in result:
|
||||
if attr == "bridges":
|
||||
# If a new bridge is discovered, register it:
|
||||
if item.id not in data.bridges:
|
||||
_async_register_new_bridge(hass, item, entry)
|
||||
data.bridges[item.id] = item
|
||||
elif attr == "listeners":
|
||||
data.listeners[item.id] = item
|
||||
else:
|
||||
data.sensors[item.uuid] = item
|
||||
data.update_data_from_response(result)
|
||||
|
||||
return data
|
||||
|
||||
@ -216,7 +251,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
@callback
|
||||
def _async_register_new_bridge(
|
||||
hass: HomeAssistant, bridge: Bridge, entry: ConfigEntry
|
||||
hass: HomeAssistant, entry: ConfigEntry, bridge: Bridge
|
||||
) -> None:
|
||||
"""Register a new bridge."""
|
||||
if name := bridge.name:
|
||||
@ -279,6 +314,11 @@ class NotionEntity(CoordinatorEntity[DataUpdateCoordinator[NotionData]]):
|
||||
and self._listener_id in self.coordinator.data.listeners
|
||||
)
|
||||
|
||||
@property
|
||||
def listener(self) -> Listener:
|
||||
"""Return the listener related to this entity."""
|
||||
return self.coordinator.data.listeners[self._listener_id]
|
||||
|
||||
@callback
|
||||
def _async_update_bridge_id(self) -> None:
|
||||
"""Update the entity's bridge ID if it has changed.
|
||||
@ -310,21 +350,9 @@ class NotionEntity(CoordinatorEntity[DataUpdateCoordinator[NotionData]]):
|
||||
this_device.id, via_device_id=bridge_device.id
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_update_from_latest_data(self) -> None:
|
||||
"""Update the entity from the latest data."""
|
||||
raise NotImplementedError
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Respond to a DataUpdateCoordinator update."""
|
||||
if self._listener_id in self.coordinator.data.listeners:
|
||||
self._async_update_bridge_id()
|
||||
self._async_update_from_latest_data()
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
await super().async_added_to_hass()
|
||||
self._async_update_from_latest_data()
|
||||
super()._handle_coordinator_update()
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.components.binary_sensor import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import NotionEntity
|
||||
@ -37,7 +37,7 @@ from .model import NotionEntityDescriptionMixin
|
||||
class NotionBinarySensorDescriptionMixin:
|
||||
"""Define an entity description mixin for binary and regular sensors."""
|
||||
|
||||
on_state: Literal["alarm", "critical", "leak", "not_missing", "open"]
|
||||
on_state: Literal["alarm", "leak", "low", "not_missing", "open"]
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -56,7 +56,7 @@ BINARY_SENSOR_DESCRIPTIONS = (
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
listener_kind=ListenerKind.BATTERY,
|
||||
on_state="critical",
|
||||
on_state="low",
|
||||
),
|
||||
NotionBinarySensorDescription(
|
||||
key=SENSOR_DOOR,
|
||||
@ -146,17 +146,10 @@ class NotionBinarySensor(NotionEntity, BinarySensorEntity):
|
||||
|
||||
entity_description: NotionBinarySensorDescription
|
||||
|
||||
@callback
|
||||
def _async_update_from_latest_data(self) -> None:
|
||||
"""Fetch new state data for the sensor."""
|
||||
listener = self.coordinator.data.listeners[self._listener_id]
|
||||
|
||||
if listener.status.trigger_value:
|
||||
state = listener.status.trigger_value
|
||||
elif listener.insights.primary.value:
|
||||
state = listener.insights.primary.value
|
||||
else:
|
||||
LOGGER.warning("Unknown listener structure: %s", listener)
|
||||
state = None
|
||||
|
||||
self._attr_is_on = self.entity_description.on_state == state
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
if not self.listener.insights.primary.value:
|
||||
LOGGER.warning("Unknown listener structure: %s", self.listener.dict())
|
||||
return False
|
||||
return self.listener.insights.primary.value == self.entity_description.on_state
|
||||
|
@ -16,6 +16,7 @@ CONF_DEVICE_KEY = "device_key"
|
||||
CONF_HARDWARE_ID = "hardware_id"
|
||||
CONF_LAST_BRIDGE_HARDWARE_ID = "last_bridge_hardware_id"
|
||||
CONF_TITLE = "title"
|
||||
CONF_USER_ID = "user_id"
|
||||
|
||||
TO_REDACT = {
|
||||
CONF_DEVICE_KEY,
|
||||
@ -27,6 +28,7 @@ TO_REDACT = {
|
||||
CONF_TITLE,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_USERNAME,
|
||||
CONF_USER_ID,
|
||||
}
|
||||
|
||||
|
||||
|
@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aionotion"],
|
||||
"requirements": ["aionotion==2023.05.0"]
|
||||
"requirements": ["aionotion==2023.05.4"]
|
||||
}
|
||||
|
@ -11,11 +11,11 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import NotionEntity
|
||||
from .const import DOMAIN, LOGGER, SENSOR_TEMPERATURE
|
||||
from .const import DOMAIN, SENSOR_TEMPERATURE
|
||||
from .model import NotionEntityDescriptionMixin
|
||||
|
||||
|
||||
@ -63,15 +63,24 @@ async def async_setup_entry(
|
||||
class NotionSensor(NotionEntity, SensorEntity):
|
||||
"""Define a Notion sensor."""
|
||||
|
||||
@callback
|
||||
def _async_update_from_latest_data(self) -> None:
|
||||
"""Fetch new state data for the sensor."""
|
||||
listener = self.coordinator.data.listeners[self._listener_id]
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement of the sensor."""
|
||||
if self.listener.listener_kind == ListenerKind.TEMPERATURE:
|
||||
if not self.coordinator.data.user_preferences:
|
||||
return None
|
||||
if self.coordinator.data.user_preferences.celsius_enabled:
|
||||
return UnitOfTemperature.CELSIUS
|
||||
return UnitOfTemperature.FAHRENHEIT
|
||||
return None
|
||||
|
||||
if listener.listener_kind == ListenerKind.TEMPERATURE:
|
||||
self._attr_native_value = round(listener.status.temperature, 1) # type: ignore[attr-defined]
|
||||
else:
|
||||
LOGGER.error(
|
||||
"Unknown listener type for sensor %s",
|
||||
self.coordinator.data.sensors[self._sensor_id],
|
||||
)
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
"""Return the value reported by the sensor.
|
||||
|
||||
The Notion API only returns a localized string for temperature (e.g. "70°"); we
|
||||
simply remove the degree symbol:
|
||||
"""
|
||||
if not self.listener.status_localized:
|
||||
return None
|
||||
return self.listener.status_localized.state[:-1]
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""The ONVIF integration."""
|
||||
import asyncio
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
|
||||
from httpx import RequestError
|
||||
@ -56,7 +57,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except ONVIFError as err:
|
||||
await device.device.close()
|
||||
raise ConfigEntryNotReady(
|
||||
f"Could not setup camera {device.device.host}:{device.device.port}: {err}"
|
||||
f"Could not setup camera {device.device.host}:{device.device.port}: {stringify_onvif_error(err)}"
|
||||
) from err
|
||||
except TransportError as err:
|
||||
await device.device.close()
|
||||
stringified_onvif_error = stringify_onvif_error(err)
|
||||
if err.status_code in (
|
||||
HTTPStatus.UNAUTHORIZED.value,
|
||||
HTTPStatus.FORBIDDEN.value,
|
||||
):
|
||||
raise ConfigEntryAuthFailed(
|
||||
f"Auth Failed: {stringified_onvif_error}"
|
||||
) from err
|
||||
raise ConfigEntryNotReady(
|
||||
f"Could not setup camera {device.device.host}:{device.device.port}: {stringified_onvif_error}"
|
||||
) from err
|
||||
except asyncio.CancelledError as err:
|
||||
# After https://github.com/agronholm/anyio/issues/374 is resolved
|
||||
|
@ -142,10 +142,14 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
hass.async_create_task(hass.config_entries.async_reload(entry_id))
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
username = (user_input or {}).get(CONF_USERNAME) or entry.data[CONF_USERNAME]
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
|
||||
{
|
||||
vol.Required(CONF_USERNAME, default=username): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=description_placeholders,
|
||||
|
@ -27,6 +27,10 @@ async def async_get_config_entry_diagnostics(
|
||||
"info": asdict(device.info),
|
||||
"capabilities": asdict(device.capabilities),
|
||||
"profiles": [asdict(profile) for profile in device.profiles],
|
||||
"services": {
|
||||
str(key): service.url for key, service in device.device.services.items()
|
||||
},
|
||||
"xaddrs": device.device.xaddrs,
|
||||
}
|
||||
data["events"] = {
|
||||
"webhook_manager_state": device.events.webhook_manager.state,
|
||||
|
@ -47,6 +47,7 @@
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "Reauthenticate the ONVIF device",
|
||||
"description": "Some devices will reject authentication if the time is out of sync by more than 5 seconds. If authentication is unsuccessful, verify the time on the device is correct and try again.",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
|
@ -13,7 +13,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
|
||||
"requirements": ["pyoverkiz==1.7.7"],
|
||||
"requirements": ["pyoverkiz==1.7.8"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_kizbox._tcp.local.",
|
||||
|
@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["vehicle==1.0.0"]
|
||||
"requirements": ["vehicle==1.0.1"]
|
||||
}
|
||||
|
@ -46,28 +46,28 @@ BUTTON_ENTITIES = (
|
||||
key="ptz_left",
|
||||
name="PTZ left",
|
||||
icon="mdi:pan",
|
||||
supported=lambda api, ch: api.supported(ch, "pan_tilt"),
|
||||
supported=lambda api, ch: api.supported(ch, "pan"),
|
||||
method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.left.value),
|
||||
),
|
||||
ReolinkButtonEntityDescription(
|
||||
key="ptz_right",
|
||||
name="PTZ right",
|
||||
icon="mdi:pan",
|
||||
supported=lambda api, ch: api.supported(ch, "pan_tilt"),
|
||||
supported=lambda api, ch: api.supported(ch, "pan"),
|
||||
method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.right.value),
|
||||
),
|
||||
ReolinkButtonEntityDescription(
|
||||
key="ptz_up",
|
||||
name="PTZ up",
|
||||
icon="mdi:pan",
|
||||
supported=lambda api, ch: api.supported(ch, "pan_tilt"),
|
||||
supported=lambda api, ch: api.supported(ch, "tilt"),
|
||||
method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.up.value),
|
||||
),
|
||||
ReolinkButtonEntityDescription(
|
||||
key="ptz_down",
|
||||
name="PTZ down",
|
||||
icon="mdi:pan",
|
||||
supported=lambda api, ch: api.supported(ch, "pan_tilt"),
|
||||
supported=lambda api, ch: api.supported(ch, "tilt"),
|
||||
method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.down.value),
|
||||
),
|
||||
ReolinkButtonEntityDescription(
|
||||
|
@ -18,5 +18,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"requirements": ["reolink-aio==0.5.13"]
|
||||
"requirements": ["reolink-aio==0.5.15"]
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ SIREN_ENTITIES = (
|
||||
key="siren",
|
||||
name="Siren",
|
||||
icon="mdi:alarm-light",
|
||||
supported=lambda api, ch: api.supported(ch, "siren"),
|
||||
supported=lambda api, ch: api.supported(ch, "siren_play"),
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -8,6 +8,7 @@ import logging
|
||||
from roborock.api import RoborockApiClient
|
||||
from roborock.cloud_api import RoborockMqttClient
|
||||
from roborock.containers import HomeDataDevice, RoborockDeviceInfo, UserData
|
||||
from roborock.exceptions import RoborockException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_USERNAME
|
||||
@ -44,7 +45,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
for device, result in zip(devices, network_results)
|
||||
if result is not None
|
||||
}
|
||||
try:
|
||||
await mqtt_client.async_disconnect()
|
||||
except RoborockException as err:
|
||||
_LOGGER.warning("Failed disconnecting from the mqtt server %s", err)
|
||||
if not network_info:
|
||||
raise ConfigEntryNotReady(
|
||||
"Could not get network information about your devices"
|
||||
|
@ -17,7 +17,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass.data[DOMAIN][entry.entry_id] = hub
|
||||
try:
|
||||
if hub.sia_client:
|
||||
await hub.sia_client.start(reuse_port=True)
|
||||
await hub.sia_client.async_start(reuse_port=True)
|
||||
except OSError as exc:
|
||||
raise ConfigEntryNotReady(
|
||||
f"SIA Server at port {entry.data[CONF_PORT]} could not start."
|
||||
|
@ -71,7 +71,7 @@ class SIAHub:
|
||||
async def async_shutdown(self, _: Event | None = None) -> None:
|
||||
"""Shutdown the SIA server."""
|
||||
if self.sia_client:
|
||||
await self.sia_client.stop()
|
||||
await self.sia_client.async_stop()
|
||||
|
||||
async def async_create_and_fire_event(self, event: SIAEvent) -> None:
|
||||
"""Create a event on HA dispatcher and then on HA's bus, with the data from the SIAEvent.
|
||||
|
@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sleepiq",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["asyncsleepiq"],
|
||||
"requirements": ["asyncsleepiq==1.3.4"]
|
||||
"requirements": ["asyncsleepiq==1.3.5"]
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sonos",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["soco"],
|
||||
"requirements": ["soco==0.29.1", "sonos-websocket==0.1.0"],
|
||||
"requirements": ["soco==0.29.1", "sonos-websocket==0.1.1"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:schemas-upnp-org:device:ZonePlayer:1"
|
||||
|
@ -147,8 +147,10 @@ async def async_remove_config_entry_device(
|
||||
api = data.api
|
||||
serial = api.information.serial
|
||||
storage = api.storage
|
||||
all_cameras: list[SynoCamera] = []
|
||||
if api.surveillance_station is not None:
|
||||
# get_all_cameras does not do I/O
|
||||
all_cameras: list[SynoCamera] = api.surveillance_station.get_all_cameras()
|
||||
all_cameras = api.surveillance_station.get_all_cameras()
|
||||
device_ids = chain(
|
||||
(camera.id for camera in all_cameras),
|
||||
storage.volumes_ids,
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/upb",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["upb_lib"],
|
||||
"requirements": ["upb_lib==0.5.3"]
|
||||
"requirements": ["upb_lib==0.5.4"]
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/volvooncall",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["geopy", "hbmqtt", "volvooncall"],
|
||||
"requirements": ["volvooncall==0.10.2"]
|
||||
"requirements": ["volvooncall==0.10.3"]
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiowebostv"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiowebostv==0.3.2"],
|
||||
"requirements": ["aiowebostv==0.3.3"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:lge-com:service:webos-second-screen:1"
|
||||
|
@ -1,7 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"incorrect_province": "Incorrect subdivision from yaml import"
|
||||
"incorrect_province": "Incorrect subdivision from yaml import",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
@ -31,8 +32,7 @@
|
||||
},
|
||||
"error": {
|
||||
"add_holiday_error": "Incorrect format on date (YYYY-MM-DD)",
|
||||
"remove_holiday_error": "Incorrect format on date (YYYY-MM-DD) or holiday name not found",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
"remove_holiday_error": "Incorrect format on date (YYYY-MM-DD) or holiday name not found"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@ -59,7 +59,7 @@
|
||||
"error": {
|
||||
"add_holiday_error": "Incorrect format on date (YYYY-MM-DD)",
|
||||
"remove_holiday_error": "Incorrect format on date (YYYY-MM-DD) or holiday name not found",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
"already_configured": "Service with this configuration already exist"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
|
@ -20,7 +20,7 @@
|
||||
"zigpy_znp"
|
||||
],
|
||||
"requirements": [
|
||||
"bellows==0.35.2",
|
||||
"bellows==0.35.5",
|
||||
"pyserial==3.5",
|
||||
"pyserial-asyncio==0.6",
|
||||
"zha-quirks==0.0.99",
|
||||
|
@ -84,7 +84,7 @@ bulk_set_partial_config_parameters:
|
||||
value:
|
||||
name: Value
|
||||
description: The new value(s) to set for this configuration parameter. Can either be a raw integer value to represent the bulk change or a mapping where the key is the bitmask (either in hex or integer form) and the value is the new value you want to set for that partial parameter.
|
||||
example:
|
||||
example: |
|
||||
"0x1": 1
|
||||
"0x10": 1
|
||||
"0x20": 1
|
||||
@ -287,7 +287,7 @@ invoke_cc_api:
|
||||
parameters:
|
||||
name: Parameters
|
||||
description: A list of parameters to pass to the API method. Refer to the Z-Wave JS Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for parameters.
|
||||
example: [1, 1]
|
||||
example: "[1, 1]"
|
||||
required: true
|
||||
selector:
|
||||
object:
|
||||
|
@ -8,7 +8,7 @@ from .backports.enum import StrEnum
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2023
|
||||
MINOR_VERSION: Final = 5
|
||||
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)
|
||||
|
@ -37,6 +37,11 @@ SERVER_SOFTWARE = "{0}/{1} aiohttp/{2} Python/{3[0]}.{3[1]}".format(
|
||||
APPLICATION_NAME, __version__, aiohttp.__version__, sys.version_info
|
||||
)
|
||||
|
||||
ENABLE_CLEANUP_CLOSED = sys.version_info < (3, 11, 1)
|
||||
# Enabling cleanup closed on python 3.11.1+ leaks memory relatively quickly
|
||||
# see https://github.com/aio-libs/aiohttp/issues/7252
|
||||
# aiohttp interacts poorly with https://github.com/python/cpython/pull/98540
|
||||
|
||||
WARN_CLOSE_MSG = "closes the Home Assistant aiohttp session"
|
||||
|
||||
#
|
||||
@ -276,7 +281,7 @@ def _async_get_connector(
|
||||
ssl_context = ssl_util.get_default_no_verify_context()
|
||||
|
||||
connector = aiohttp.TCPConnector(
|
||||
enable_cleanup_closed=True,
|
||||
enable_cleanup_closed=ENABLE_CLEANUP_CLOSED,
|
||||
ssl=ssl_context,
|
||||
limit=MAXIMUM_CONNECTIONS,
|
||||
limit_per_host=MAXIMUM_CONNECTIONS_PER_HOST,
|
||||
|
@ -763,13 +763,6 @@ class Entity(ABC):
|
||||
hass = self.hass
|
||||
assert hass is not None
|
||||
|
||||
if hasattr(self, "async_update"):
|
||||
coro: asyncio.Future[None] = self.async_update()
|
||||
elif hasattr(self, "update"):
|
||||
coro = hass.async_add_executor_job(self.update)
|
||||
else:
|
||||
return
|
||||
|
||||
self._update_staged = True
|
||||
|
||||
# Process update sequential
|
||||
@ -780,8 +773,14 @@ class Entity(ABC):
|
||||
update_warn = hass.loop.call_later(
|
||||
SLOW_UPDATE_WARNING, self._async_slow_update_warning
|
||||
)
|
||||
|
||||
try:
|
||||
await coro
|
||||
if hasattr(self, "async_update"):
|
||||
await self.async_update()
|
||||
elif hasattr(self, "update"):
|
||||
await hass.async_add_executor_job(self.update)
|
||||
else:
|
||||
return
|
||||
finally:
|
||||
self._update_staged = False
|
||||
if warning:
|
||||
|
@ -14,7 +14,7 @@ bcrypt==4.0.1
|
||||
bleak-retry-connector==3.0.2
|
||||
bleak==0.20.2
|
||||
bluetooth-adapters==0.15.3
|
||||
bluetooth-auto-recovery==1.1.2
|
||||
bluetooth-auto-recovery==1.2.0
|
||||
bluetooth-data-tools==0.4.0
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.3.0
|
||||
|
@ -54,6 +54,20 @@ def is_region(language: str, region: str | None) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def is_language_match(lang_1: str, lang_2: str) -> bool:
|
||||
"""Return true if two languages are considered the same."""
|
||||
if lang_1 == lang_2:
|
||||
# Exact match
|
||||
return True
|
||||
|
||||
if {lang_1, lang_2} == {"no", "nb"}:
|
||||
# no = spoken Norwegian
|
||||
# nb = written Norwegian (Bokmål)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@dataclass
|
||||
class Dialect:
|
||||
"""Language with optional region and script/code."""
|
||||
@ -71,26 +85,35 @@ class Dialect:
|
||||
# Regions are upper-cased
|
||||
self.region = self.region.upper()
|
||||
|
||||
def score(self, dialect: Dialect, country: str | None = None) -> float:
|
||||
def score(
|
||||
self, dialect: Dialect, country: str | None = None
|
||||
) -> tuple[float, float]:
|
||||
"""Return score for match with another dialect where higher is better.
|
||||
|
||||
Score < 0 indicates a failure to match.
|
||||
"""
|
||||
if self.language != dialect.language:
|
||||
if not is_language_match(self.language, dialect.language):
|
||||
# Not a match
|
||||
return -1
|
||||
return (-1, 0)
|
||||
|
||||
is_exact_language = self.language == dialect.language
|
||||
|
||||
if (self.region is None) and (dialect.region is None):
|
||||
# Weak match with no region constraint
|
||||
return 1
|
||||
# Prefer exact language match
|
||||
return (2 if is_exact_language else 1, 0)
|
||||
|
||||
if (self.region is not None) and (dialect.region is not None):
|
||||
if self.region == dialect.region:
|
||||
# Exact language + region match
|
||||
return math.inf
|
||||
# Same language + region match
|
||||
# Prefer exact language match
|
||||
return (
|
||||
math.inf,
|
||||
1 if is_exact_language else 0,
|
||||
)
|
||||
|
||||
# Regions are both set, but don't match
|
||||
return 0
|
||||
return (0, 0)
|
||||
|
||||
# Generate ordered list of preferred regions
|
||||
pref_regions = list(
|
||||
@ -113,13 +136,13 @@ class Dialect:
|
||||
|
||||
# More preferred regions are at the front.
|
||||
# Add 1 to boost above a weak match where no regions are set.
|
||||
return 1 + (len(pref_regions) - region_idx)
|
||||
return (1 + (len(pref_regions) - region_idx), 0)
|
||||
except ValueError:
|
||||
# Region was not in preferred list
|
||||
pass
|
||||
|
||||
# Not a preferred region
|
||||
return 0
|
||||
return (0, 0)
|
||||
|
||||
@staticmethod
|
||||
def parse(tag: str) -> Dialect:
|
||||
@ -169,4 +192,4 @@ def matches(
|
||||
)
|
||||
|
||||
# Score < 0 is not a match
|
||||
return [tag for _dialect, score, tag in scored if score >= 0]
|
||||
return [tag for _dialect, score, tag in scored if score[0] >= 0]
|
||||
|
@ -73,8 +73,6 @@ def create_no_verify_ssl_context(
|
||||
https://github.com/aio-libs/aiohttp/blob/33953f110e97eecc707e1402daa8d543f38a189b/aiohttp/connector.py#L911
|
||||
"""
|
||||
sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
sslcontext.options |= ssl.OP_NO_SSLv2
|
||||
sslcontext.options |= ssl.OP_NO_SSLv3
|
||||
sslcontext.check_hostname = False
|
||||
sslcontext.verify_mode = ssl.CERT_NONE
|
||||
with contextlib.suppress(AttributeError):
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2023.5.2"
|
||||
version = "2023.5.3"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
@ -116,7 +116,7 @@ aio_georss_gdacs==0.8
|
||||
aioairq==0.2.4
|
||||
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==0.5.2
|
||||
aioairzone==0.5.5
|
||||
|
||||
# homeassistant.components.ambient_station
|
||||
aioambient==2023.04.0
|
||||
@ -156,7 +156,7 @@ aioecowitt==2023.01.0
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==13.7.3
|
||||
aioesphomeapi==13.7.4
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
@ -223,7 +223,7 @@ aionanoleaf==0.2.1
|
||||
aionotify==0.2.0
|
||||
|
||||
# homeassistant.components.notion
|
||||
aionotion==2023.05.0
|
||||
aionotion==2023.05.4
|
||||
|
||||
# homeassistant.components.oncue
|
||||
aiooncue==0.3.4
|
||||
@ -300,7 +300,7 @@ aiovlc==0.1.0
|
||||
aiowatttime==0.1.1
|
||||
|
||||
# homeassistant.components.webostv
|
||||
aiowebostv==0.3.2
|
||||
aiowebostv==0.3.3
|
||||
|
||||
# homeassistant.components.yandex_transport
|
||||
aioymaps==1.2.2
|
||||
@ -383,7 +383,7 @@ async-upnp-client==0.33.1
|
||||
asyncpysupla==0.0.5
|
||||
|
||||
# homeassistant.components.sleepiq
|
||||
asyncsleepiq==1.3.4
|
||||
asyncsleepiq==1.3.5
|
||||
|
||||
# homeassistant.components.aten_pe
|
||||
# atenpdu==0.3.2
|
||||
@ -428,10 +428,10 @@ beautifulsoup4==4.11.1
|
||||
# beewi_smartclim==0.0.10
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows==0.35.2
|
||||
bellows==0.35.5
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.13.2
|
||||
bimmer_connected==0.13.3
|
||||
|
||||
# homeassistant.components.bizkaibus
|
||||
bizkaibus==0.1.1
|
||||
@ -465,7 +465,7 @@ bluemaestro-ble==0.2.3
|
||||
bluetooth-adapters==0.15.3
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-auto-recovery==1.1.2
|
||||
bluetooth-auto-recovery==1.2.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
# homeassistant.components.esphome
|
||||
@ -683,7 +683,7 @@ epsonprinter==0.0.9
|
||||
esphome-dashboard-api==1.2.3
|
||||
|
||||
# homeassistant.components.netgear_lte
|
||||
eternalegypt==0.0.15
|
||||
eternalegypt==0.0.16
|
||||
|
||||
# homeassistant.components.eufylife_ble
|
||||
eufylife_ble_client==0.1.7
|
||||
@ -1859,7 +1859,7 @@ pyotgw==2.1.3
|
||||
pyotp==2.8.0
|
||||
|
||||
# homeassistant.components.overkiz
|
||||
pyoverkiz==1.7.7
|
||||
pyoverkiz==1.7.8
|
||||
|
||||
# homeassistant.components.openweathermap
|
||||
pyowm==3.2.0
|
||||
@ -2242,7 +2242,7 @@ regenmaschine==2022.11.0
|
||||
renault-api==0.1.13
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.5.13
|
||||
reolink-aio==0.5.15
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==6.0
|
||||
@ -2390,7 +2390,7 @@ solax==0.3.0
|
||||
somfy-mylink-synergy==1.0.6
|
||||
|
||||
# homeassistant.components.sonos
|
||||
sonos-websocket==0.1.0
|
||||
sonos-websocket==0.1.1
|
||||
|
||||
# homeassistant.components.marytts
|
||||
speak2mary==1.4.0
|
||||
@ -2565,7 +2565,7 @@ unifi-discovery==1.1.7
|
||||
unifiled==0.11
|
||||
|
||||
# homeassistant.components.upb
|
||||
upb_lib==0.5.3
|
||||
upb_lib==0.5.4
|
||||
|
||||
# homeassistant.components.upcloud
|
||||
upcloud-api==2.0.0
|
||||
@ -2582,7 +2582,7 @@ uvcclient==0.11.0
|
||||
vallox-websocket-api==3.2.1
|
||||
|
||||
# homeassistant.components.rdw
|
||||
vehicle==1.0.0
|
||||
vehicle==1.0.1
|
||||
|
||||
# homeassistant.components.velbus
|
||||
velbus-aio==2023.2.0
|
||||
@ -2600,7 +2600,7 @@ voip-utils==0.0.7
|
||||
volkszaehler==0.4.0
|
||||
|
||||
# homeassistant.components.volvooncall
|
||||
volvooncall==0.10.2
|
||||
volvooncall==0.10.3
|
||||
|
||||
# homeassistant.components.verisure
|
||||
vsure==2.6.1
|
||||
|
@ -106,7 +106,7 @@ aio_georss_gdacs==0.8
|
||||
aioairq==0.2.4
|
||||
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==0.5.2
|
||||
aioairzone==0.5.5
|
||||
|
||||
# homeassistant.components.ambient_station
|
||||
aioambient==2023.04.0
|
||||
@ -146,7 +146,7 @@ aioecowitt==2023.01.0
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==13.7.3
|
||||
aioesphomeapi==13.7.4
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
@ -204,7 +204,7 @@ aiomusiccast==0.14.8
|
||||
aionanoleaf==0.2.1
|
||||
|
||||
# homeassistant.components.notion
|
||||
aionotion==2023.05.0
|
||||
aionotion==2023.05.4
|
||||
|
||||
# homeassistant.components.oncue
|
||||
aiooncue==0.3.4
|
||||
@ -281,7 +281,7 @@ aiovlc==0.1.0
|
||||
aiowatttime==0.1.1
|
||||
|
||||
# homeassistant.components.webostv
|
||||
aiowebostv==0.3.2
|
||||
aiowebostv==0.3.3
|
||||
|
||||
# homeassistant.components.yandex_transport
|
||||
aioymaps==1.2.2
|
||||
@ -340,7 +340,7 @@ arcam-fmj==1.3.0
|
||||
async-upnp-client==0.33.1
|
||||
|
||||
# homeassistant.components.sleepiq
|
||||
asyncsleepiq==1.3.4
|
||||
asyncsleepiq==1.3.5
|
||||
|
||||
# homeassistant.components.aurora
|
||||
auroranoaa==0.0.3
|
||||
@ -361,10 +361,10 @@ base36==0.1.1
|
||||
beautifulsoup4==4.11.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows==0.35.2
|
||||
bellows==0.35.5
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.13.2
|
||||
bimmer_connected==0.13.3
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak-retry-connector==3.0.2
|
||||
@ -385,7 +385,7 @@ bluemaestro-ble==0.2.3
|
||||
bluetooth-adapters==0.15.3
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-auto-recovery==1.1.2
|
||||
bluetooth-auto-recovery==1.2.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
# homeassistant.components.esphome
|
||||
@ -1357,7 +1357,7 @@ pyotgw==2.1.3
|
||||
pyotp==2.8.0
|
||||
|
||||
# homeassistant.components.overkiz
|
||||
pyoverkiz==1.7.7
|
||||
pyoverkiz==1.7.8
|
||||
|
||||
# homeassistant.components.openweathermap
|
||||
pyowm==3.2.0
|
||||
@ -1611,7 +1611,7 @@ regenmaschine==2022.11.0
|
||||
renault-api==0.1.13
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.5.13
|
||||
reolink-aio==0.5.15
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==6.0
|
||||
@ -1714,7 +1714,7 @@ solax==0.3.0
|
||||
somfy-mylink-synergy==1.0.6
|
||||
|
||||
# homeassistant.components.sonos
|
||||
sonos-websocket==0.1.0
|
||||
sonos-websocket==0.1.1
|
||||
|
||||
# homeassistant.components.marytts
|
||||
speak2mary==1.4.0
|
||||
@ -1841,7 +1841,7 @@ ultraheat-api==0.5.1
|
||||
unifi-discovery==1.1.7
|
||||
|
||||
# homeassistant.components.upb
|
||||
upb_lib==0.5.3
|
||||
upb_lib==0.5.4
|
||||
|
||||
# homeassistant.components.upcloud
|
||||
upcloud-api==2.0.0
|
||||
@ -1858,7 +1858,7 @@ uvcclient==0.11.0
|
||||
vallox-websocket-api==3.2.1
|
||||
|
||||
# homeassistant.components.rdw
|
||||
vehicle==1.0.0
|
||||
vehicle==1.0.1
|
||||
|
||||
# homeassistant.components.velbus
|
||||
velbus-aio==2023.2.0
|
||||
@ -1873,7 +1873,7 @@ vilfo-api-client==0.3.2
|
||||
voip-utils==0.0.7
|
||||
|
||||
# homeassistant.components.volvooncall
|
||||
volvooncall==0.10.2
|
||||
volvooncall==0.10.3
|
||||
|
||||
# homeassistant.components.verisure
|
||||
vsure==2.6.1
|
||||
|
@ -84,3 +84,9 @@ async def test_airzone_create_binary_sensors(hass: HomeAssistant) -> None:
|
||||
|
||||
state = hass.states.get("binary_sensor.airzone_2_1_problem")
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
state = hass.states.get("binary_sensor.dkn_plus_battery_low")
|
||||
assert state is None
|
||||
|
||||
state = hass.states.get("binary_sensor.dkn_plus_problem")
|
||||
assert state.state == STATE_OFF
|
||||
|
@ -145,6 +145,24 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None:
|
||||
assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP
|
||||
assert state.attributes.get(ATTR_TEMPERATURE) == 19.0
|
||||
|
||||
state = hass.states.get("climate.dkn_plus")
|
||||
assert state.state == HVACMode.HEAT_COOL
|
||||
assert state.attributes.get(ATTR_CURRENT_HUMIDITY) is None
|
||||
assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 21.7
|
||||
assert state.attributes.get(ATTR_HVAC_ACTION) == HVACAction.COOLING
|
||||
assert state.attributes.get(ATTR_HVAC_MODES) == [
|
||||
HVACMode.FAN_ONLY,
|
||||
HVACMode.COOL,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.DRY,
|
||||
HVACMode.HEAT_COOL,
|
||||
HVACMode.OFF,
|
||||
]
|
||||
assert state.attributes.get(ATTR_MAX_TEMP) == 32.2
|
||||
assert state.attributes.get(ATTR_MIN_TEMP) == 17.8
|
||||
assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP
|
||||
assert state.attributes.get(ATTR_TEMPERATURE) == 22.8
|
||||
|
||||
|
||||
async def test_airzone_climate_turn_on_off(hass: HomeAssistant) -> None:
|
||||
"""Test turning on."""
|
||||
|
@ -52,3 +52,9 @@ async def test_airzone_create_sensors(
|
||||
|
||||
state = hass.states.get("sensor.airzone_2_1_humidity")
|
||||
assert state.state == "62"
|
||||
|
||||
state = hass.states.get("sensor.dkn_plus_temperature")
|
||||
assert state.state == "21.7"
|
||||
|
||||
state = hass.states.get("sensor.dkn_plus_humidity")
|
||||
assert state is None
|
||||
|
@ -7,10 +7,16 @@ from aioairzone.const import (
|
||||
API_COLD_ANGLE,
|
||||
API_COLD_STAGE,
|
||||
API_COLD_STAGES,
|
||||
API_COOL_MAX_TEMP,
|
||||
API_COOL_MIN_TEMP,
|
||||
API_COOL_SET_POINT,
|
||||
API_DATA,
|
||||
API_ERRORS,
|
||||
API_FLOOR_DEMAND,
|
||||
API_HEAT_ANGLE,
|
||||
API_HEAT_MAX_TEMP,
|
||||
API_HEAT_MIN_TEMP,
|
||||
API_HEAT_SET_POINT,
|
||||
API_HEAT_STAGE,
|
||||
API_HEAT_STAGES,
|
||||
API_HUMIDITY,
|
||||
@ -25,6 +31,8 @@ from aioairzone.const import (
|
||||
API_ROOM_TEMP,
|
||||
API_SET_POINT,
|
||||
API_SLEEP,
|
||||
API_SPEED,
|
||||
API_SPEEDS,
|
||||
API_SYSTEM_FIRMWARE,
|
||||
API_SYSTEM_ID,
|
||||
API_SYSTEM_TYPE,
|
||||
@ -216,6 +224,39 @@ HVAC_MOCK = {
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
API_DATA: [
|
||||
{
|
||||
API_SYSTEM_ID: 3,
|
||||
API_ZONE_ID: 1,
|
||||
API_NAME: "DKN Plus",
|
||||
API_ON: 1,
|
||||
API_COOL_SET_POINT: 73,
|
||||
API_COOL_MAX_TEMP: 90,
|
||||
API_COOL_MIN_TEMP: 64,
|
||||
API_HEAT_SET_POINT: 77,
|
||||
API_HEAT_MAX_TEMP: 86,
|
||||
API_HEAT_MIN_TEMP: 50,
|
||||
API_MAX_TEMP: 90,
|
||||
API_MIN_TEMP: 64,
|
||||
API_SET_POINT: 73,
|
||||
API_ROOM_TEMP: 71,
|
||||
API_MODES: [4, 2, 3, 5, 7],
|
||||
API_MODE: 7,
|
||||
API_SPEEDS: 5,
|
||||
API_SPEED: 2,
|
||||
API_COLD_STAGES: 0,
|
||||
API_COLD_STAGE: 0,
|
||||
API_HEAT_STAGES: 0,
|
||||
API_HEAT_STAGE: 0,
|
||||
API_HUMIDITY: 0,
|
||||
API_UNITS: 1,
|
||||
API_ERRORS: [],
|
||||
API_AIR_DEMAND: 1,
|
||||
API_FLOOR_DEMAND: 0,
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
21
tests/components/alexa/test_config.py
Normal file
21
tests/components/alexa/test_config.py
Normal file
@ -0,0 +1,21 @@
|
||||
"""Test config."""
|
||||
import asyncio
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .test_common import get_default_config
|
||||
|
||||
|
||||
async def test_enable_proactive_mode_in_parallel(hass: HomeAssistant) -> None:
|
||||
"""Test enabling proactive mode does not happen in parallel."""
|
||||
config = get_default_config(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.alexa.config.async_enable_proactive_mode"
|
||||
) as mock_enable_proactive_mode:
|
||||
await asyncio.gather(
|
||||
config.async_enable_proactive_mode(), config.async_enable_proactive_mode()
|
||||
)
|
||||
|
||||
mock_enable_proactive_mode.assert_awaited_once()
|
@ -1754,6 +1754,8 @@ async def test_light_strip_zones_not_populated_yet(hass: HomeAssistant) -> None:
|
||||
bulb.power_level = 65535
|
||||
bulb.color_zones = None
|
||||
bulb.color = [65535, 65535, 65535, 65535]
|
||||
assert bulb.get_color_zones.calls == []
|
||||
|
||||
with _patch_discovery(device=bulb), _patch_config_flow_try_connect(
|
||||
device=bulb
|
||||
), _patch_device(device=bulb):
|
||||
@ -1761,6 +1763,14 @@ async def test_light_strip_zones_not_populated_yet(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
# Make sure we at least try to fetch the first zone
|
||||
# to ensure we populate the zones from the 503 response
|
||||
assert len(bulb.get_color_zones.calls) == 3
|
||||
# Once to populate the number of zones
|
||||
assert bulb.get_color_zones.calls[0][1]["start_index"] == 0
|
||||
# Again once we know the number of zones
|
||||
assert bulb.get_color_zones.calls[1][1]["start_index"] == 0
|
||||
assert bulb.get_color_zones.calls[2][1]["start_index"] == 8
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "on"
|
||||
|
@ -3,8 +3,9 @@ from collections.abc import Generator
|
||||
import json
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from aionotion.bridge.models import Bridge
|
||||
from aionotion.sensor.models import Listener, Sensor
|
||||
from aionotion.bridge.models import BridgeAllResponse
|
||||
from aionotion.sensor.models import ListenerAllResponse, SensorAllResponse
|
||||
from aionotion.user.models import UserPreferencesResponse
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.notion import DOMAIN
|
||||
@ -27,24 +28,23 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||
|
||||
|
||||
@pytest.fixture(name="client")
|
||||
def client_fixture(data_bridge, data_listener, data_sensor):
|
||||
def client_fixture(data_bridge, data_listener, data_sensor, data_user_preferences):
|
||||
"""Define a fixture for an aionotion client."""
|
||||
return Mock(
|
||||
bridge=Mock(
|
||||
async_all=AsyncMock(
|
||||
return_value=[Bridge.parse_obj(bridge) for bridge in data_bridge]
|
||||
)
|
||||
async_all=AsyncMock(return_value=BridgeAllResponse.parse_obj(data_bridge))
|
||||
),
|
||||
sensor=Mock(
|
||||
async_all=AsyncMock(
|
||||
return_value=[Sensor.parse_obj(sensor) for sensor in data_sensor]
|
||||
),
|
||||
async_all=AsyncMock(return_value=SensorAllResponse.parse_obj(data_sensor)),
|
||||
async_listeners=AsyncMock(
|
||||
return_value=[
|
||||
Listener.parse_obj(listener) for listener in data_listener
|
||||
]
|
||||
return_value=ListenerAllResponse.parse_obj(data_listener)
|
||||
),
|
||||
),
|
||||
user=Mock(
|
||||
async_preferences=AsyncMock(
|
||||
return_value=UserPreferencesResponse.parse_obj(data_user_preferences)
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@ -83,6 +83,12 @@ def data_sensor_fixture():
|
||||
return json.loads(load_fixture("sensor_data.json", "notion"))
|
||||
|
||||
|
||||
@pytest.fixture(name="data_user_preferences", scope="package")
|
||||
def data_user_preferences_fixture():
|
||||
"""Define user preferences data."""
|
||||
return json.loads(load_fixture("user_preferences_data.json", "notion"))
|
||||
|
||||
|
||||
@pytest.fixture(name="get_client")
|
||||
def get_client_fixture(client):
|
||||
"""Define a fixture to mock the async_get_client method."""
|
||||
|
@ -1,4 +1,5 @@
|
||||
[
|
||||
{
|
||||
"base_stations": [
|
||||
{
|
||||
"id": 12345,
|
||||
"name": "Bridge 1",
|
||||
@ -48,3 +49,4 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
[
|
||||
{
|
||||
"listeners": [
|
||||
{
|
||||
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"definition_id": 4,
|
||||
@ -53,3 +54,4 @@
|
||||
"pro_monitoring_status": "eligible"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
[
|
||||
{
|
||||
"sensors": [
|
||||
{
|
||||
"id": 123456,
|
||||
"uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
@ -32,3 +33,4 @@
|
||||
"surface_type": null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
10
tests/components/notion/fixtures/user_preferences_data.json
Normal file
10
tests/components/notion/fixtures/user_preferences_data.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"user_preferences": {
|
||||
"user_id": 12345,
|
||||
"military_time_enabled": false,
|
||||
"celsius_enabled": false,
|
||||
"disconnect_alerts_enabled": true,
|
||||
"home_away_alerts_enabled": false,
|
||||
"battery_alerts_enabled": true
|
||||
}
|
||||
}
|
@ -86,14 +86,6 @@ async def test_entry_diagnostics(
|
||||
"device_type": "sensor",
|
||||
"model_version": "3.1",
|
||||
"sensor_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"status": {
|
||||
"trigger_value": "no_alarm",
|
||||
"data_received_at": "2019-06-28T22:12:49.516000+00:00",
|
||||
},
|
||||
"status_localized": {
|
||||
"state": "No Sound",
|
||||
"description": "Jun 28 at 4:12pm",
|
||||
},
|
||||
"insights": {
|
||||
"primary": {
|
||||
"origin": {"type": None, "id": None},
|
||||
@ -103,6 +95,14 @@ async def test_entry_diagnostics(
|
||||
},
|
||||
"configuration": {},
|
||||
"pro_monitoring_status": "eligible",
|
||||
"status": {
|
||||
"trigger_value": "no_alarm",
|
||||
"data_received_at": "2019-06-28T22:12:49.516000+00:00",
|
||||
},
|
||||
"status_localized": {
|
||||
"state": "No Sound",
|
||||
"description": "Jun 28 at 4:12pm",
|
||||
},
|
||||
}
|
||||
],
|
||||
"sensors": [
|
||||
@ -131,5 +131,13 @@ async def test_entry_diagnostics(
|
||||
"surface_type": None,
|
||||
}
|
||||
],
|
||||
"user_preferences": {
|
||||
"user_id": REDACTED,
|
||||
"military_time_enabled": False,
|
||||
"celsius_enabled": False,
|
||||
"disconnect_alerts_enabled": True,
|
||||
"home_away_alerts_enabled": False,
|
||||
"battery_alerts_enabled": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -101,6 +101,8 @@ def setup_mock_onvif_camera(
|
||||
mock_onvif_camera.create_devicemgmt_service = AsyncMock(return_value=devicemgmt)
|
||||
mock_onvif_camera.create_media_service = AsyncMock(return_value=media_service)
|
||||
mock_onvif_camera.close = AsyncMock(return_value=None)
|
||||
mock_onvif_camera.xaddrs = {}
|
||||
mock_onvif_camera.services = {}
|
||||
|
||||
def mock_constructor(
|
||||
host,
|
||||
|
@ -5,7 +5,7 @@ from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components import dhcp
|
||||
from homeassistant.components.onvif import DOMAIN, config_flow
|
||||
from homeassistant.config_entries import SOURCE_DHCP
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
@ -710,6 +710,14 @@ async def test_discovered_by_dhcp_does_not_update_if_no_matching_entry(
|
||||
assert result["reason"] == "no_devices_found"
|
||||
|
||||
|
||||
def _get_schema_default(schema, key_name):
|
||||
"""Iterate schema to find a key."""
|
||||
for schema_key in schema:
|
||||
if schema_key == key_name:
|
||||
return schema_key.default()
|
||||
raise KeyError(f"{key_name} not found in schema")
|
||||
|
||||
|
||||
async def test_form_reauth(hass: HomeAssistant) -> None:
|
||||
"""Test reauthenticate."""
|
||||
entry, _, _ = await setup_onvif_integration(hass)
|
||||
@ -721,6 +729,10 @@ async def test_form_reauth(hass: HomeAssistant) -> None:
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert (
|
||||
_get_schema_default(result["data_schema"].schema, CONF_USERNAME)
|
||||
== entry.data[CONF_USERNAME]
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.onvif.config_flow.get_device"
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""Test ONVIF diagnostics."""
|
||||
from unittest.mock import ANY
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import (
|
||||
@ -71,6 +73,8 @@ async def test_diagnostics(
|
||||
"video_source_token": None,
|
||||
}
|
||||
],
|
||||
"services": ANY,
|
||||
"xaddrs": ANY,
|
||||
},
|
||||
"events": {
|
||||
"pullpoint_manager_state": {
|
||||
|
@ -7,6 +7,7 @@ from roborock.containers import (
|
||||
Consumable,
|
||||
DNDTimer,
|
||||
HomeData,
|
||||
NetworkInfo,
|
||||
Status,
|
||||
UserData,
|
||||
)
|
||||
@ -368,3 +369,7 @@ STATUS = Status.from_dict(
|
||||
)
|
||||
|
||||
PROP = DeviceProp(STATUS, DND_TIMER, CLEAN_SUMMARY, CONSUMABLE, CLEAN_RECORD)
|
||||
|
||||
NETWORK_INFO = NetworkInfo(
|
||||
ip="123.232.12.1", ssid="wifi", mac="ac:cc:cc:cc:cc", bssid="bssid", rssi=90
|
||||
)
|
||||
|
@ -1,6 +1,8 @@
|
||||
"""Test for Roborock init."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from roborock.exceptions import RoborockTimeout
|
||||
|
||||
from homeassistant.components.roborock.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
@ -8,6 +10,7 @@ from homeassistant.helpers.update_coordinator import UpdateFailed
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.roborock.mock_data import HOME_DATA, NETWORK_INFO
|
||||
|
||||
|
||||
async def test_unload_entry(
|
||||
@ -38,3 +41,23 @@ async def test_config_entry_not_ready(
|
||||
):
|
||||
await async_setup_component(hass, DOMAIN, {})
|
||||
assert mock_roborock_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_continue_setup_mqtt_disconnect_fail(
|
||||
hass: HomeAssistant, mock_roborock_entry: MockConfigEntry
|
||||
):
|
||||
"""Test that if disconnect fails, we still continue setting up."""
|
||||
with patch(
|
||||
"homeassistant.components.roborock.RoborockApiClient.get_home_data",
|
||||
return_value=HOME_DATA,
|
||||
), patch(
|
||||
"homeassistant.components.roborock.RoborockMqttClient.get_networking",
|
||||
return_value=NETWORK_INFO,
|
||||
), patch(
|
||||
"homeassistant.components.roborock.RoborockMqttClient.async_disconnect",
|
||||
side_effect=RoborockTimeout(),
|
||||
), patch(
|
||||
"homeassistant.components.roborock.RoborockDataUpdateCoordinator.async_config_entry_first_refresh"
|
||||
):
|
||||
await async_setup_component(hass, DOMAIN, {})
|
||||
assert mock_roborock_entry.state is ConfigEntryState.LOADED
|
||||
|
@ -531,6 +531,41 @@ async def test_async_parallel_updates_with_two(hass: HomeAssistant) -> None:
|
||||
test_lock.release()
|
||||
|
||||
|
||||
async def test_async_parallel_updates_with_one_using_executor(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test parallel updates with 1 (sequential) using the executor."""
|
||||
test_semaphore = asyncio.Semaphore(1)
|
||||
locked = []
|
||||
|
||||
class SyncEntity(entity.Entity):
|
||||
"""Test entity."""
|
||||
|
||||
def __init__(self, entity_id):
|
||||
"""Initialize sync test entity."""
|
||||
self.entity_id = entity_id
|
||||
self.hass = hass
|
||||
self.parallel_updates = test_semaphore
|
||||
|
||||
def update(self):
|
||||
"""Test update."""
|
||||
locked.append(self.parallel_updates.locked())
|
||||
|
||||
entities = [SyncEntity(f"sensor.test_{i}") for i in range(3)]
|
||||
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.async_create_task(
|
||||
ent.async_update_ha_state(True),
|
||||
f"Entity schedule update ha state {ent.entity_id}",
|
||||
)
|
||||
for ent in entities
|
||||
]
|
||||
)
|
||||
|
||||
assert locked == [True, True, True]
|
||||
|
||||
|
||||
async def test_async_remove_no_platform(hass: HomeAssistant) -> None:
|
||||
"""Test async_remove method when no platform set."""
|
||||
ent = entity.Entity()
|
||||
|
@ -190,3 +190,39 @@ def test_sr_latn() -> None:
|
||||
"sr-CS",
|
||||
"sr-RS",
|
||||
]
|
||||
|
||||
|
||||
def test_no_nb_same() -> None:
|
||||
"""Test that the no/nb are interchangeable."""
|
||||
assert language.matches(
|
||||
"no",
|
||||
["en-US", "en-GB", "nb"],
|
||||
) == ["nb"]
|
||||
assert language.matches(
|
||||
"nb",
|
||||
["en-US", "en-GB", "no"],
|
||||
) == ["no"]
|
||||
|
||||
|
||||
def test_no_nb_prefer_exact() -> None:
|
||||
"""Test that the exact language is preferred even if an interchangeable language is available."""
|
||||
assert language.matches(
|
||||
"no",
|
||||
["en-US", "en-GB", "nb", "no"],
|
||||
) == ["no", "nb"]
|
||||
assert language.matches(
|
||||
"no",
|
||||
["en-US", "en-GB", "no", "nb"],
|
||||
) == ["no", "nb"]
|
||||
|
||||
|
||||
def test_no_nb_prefer_exact_regions() -> None:
|
||||
"""Test that the exact language/region is preferred."""
|
||||
assert language.matches(
|
||||
"no-AA",
|
||||
["en-US", "en-GB", "nb-AA", "no-AA"],
|
||||
) == ["no-AA", "nb-AA"]
|
||||
assert language.matches(
|
||||
"no-AA",
|
||||
["en-US", "en-GB", "no-AA", "nb-AA"],
|
||||
) == ["no-AA", "nb-AA"]
|
||||
|
Loading…
x
Reference in New Issue
Block a user