This commit is contained in:
Paulus Schoutsen 2023-05-14 13:00:18 -04:00 committed by GitHub
commit e0a97ec90d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 723 additions and 354 deletions

View File

@ -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

View File

@ -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]
else:
self._attr_hvac_action = HVACAction.IDLE
self._attr_hvac_mode = HVAC_MODE_LIB_TO_HASS[
self.get_airzone_value(AZD_MODE)
]
else:
self._attr_hvac_action = HVACAction.OFF
self._attr_hvac_mode = HVACMode.OFF
self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET)

View File

@ -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"]
}

View File

@ -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

View File

@ -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"
]

View File

@ -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"]
}

View File

@ -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"
],

View File

@ -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[

View File

@ -174,23 +174,23 @@ 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():
try:
self._state = Decimal(state.state)
except (DecimalException, ValueError) as err:
_LOGGER.warning(
"%s could not restore last state %s: %s",
self.entity_id,
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
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:
_LOGGER.warning(
"%s could not restore last state %s: %s",
self.entity_id,
state.state,
err,
)
self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS)
self._unit_of_measurement = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
@callback
def calc_integration(event: Event) -> None:
"""Handle the sensor state changes."""

View File

@ -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."""

View File

@ -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

View File

@ -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"]
}

View File

@ -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()

View File

@ -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

View File

@ -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,
}

View File

@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aionotion"],
"requirements": ["aionotion==2023.05.0"]
"requirements": ["aionotion==2023.05.4"]
}

View File

@ -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]

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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%]"

View File

@ -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.",

View File

@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["vehicle==1.0.0"]
"requirements": ["vehicle==1.0.1"]
}

View File

@ -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(

View File

@ -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"]
}

View File

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

View File

@ -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
}
await mqtt_client.async_disconnect()
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"

View File

@ -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."

View File

@ -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.

View File

@ -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"]
}

View File

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

View File

@ -147,8 +147,10 @@ async def async_remove_config_entry_device(
api = data.api
serial = api.information.serial
storage = api.storage
# get_all_cameras does not do I/O
all_cameras: list[SynoCamera] = api.surveillance_station.get_all_cameras()
all_cameras: list[SynoCamera] = []
if api.surveillance_station is not None:
# get_all_cameras does not do I/O
all_cameras = api.surveillance_station.get_all_cameras()
device_ids = chain(
(camera.id for camera in all_cameras),
storage.volumes_ids,

View File

@ -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"]
}

View File

@ -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"]
}

View File

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

View File

@ -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": {

View File

@ -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",

View File

@ -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:

View File

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

View File

@ -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,

View File

@ -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:

View File

@ -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

View File

@ -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]

View File

@ -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):

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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."""

View File

@ -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

View File

@ -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,
},
]
},
]
}

View 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()

View File

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

View File

@ -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."""

View File

@ -1,50 +1,52 @@
[
{
"id": 12345,
"name": "Bridge 1",
"mode": "home",
"hardware_id": "0x0000000000000000",
"hardware_revision": 4,
"firmware_version": {
"silabs": "1.1.2",
"wifi": "0.121.0",
"wifi_app": "3.3.0"
{
"base_stations": [
{
"id": 12345,
"name": "Bridge 1",
"mode": "home",
"hardware_id": "0x0000000000000000",
"hardware_revision": 4,
"firmware_version": {
"silabs": "1.1.2",
"wifi": "0.121.0",
"wifi_app": "3.3.0"
},
"missing_at": null,
"created_at": "2019-06-27T00:18:44.337Z",
"updated_at": "2023-03-19T03:20:16.061Z",
"system_id": 11111,
"firmware": {
"silabs": "1.1.2",
"wifi": "0.121.0",
"wifi_app": "3.3.0"
},
"links": {
"system": 11111
}
},
"missing_at": null,
"created_at": "2019-06-27T00:18:44.337Z",
"updated_at": "2023-03-19T03:20:16.061Z",
"system_id": 11111,
"firmware": {
"silabs": "1.1.2",
"wifi": "0.121.0",
"wifi_app": "3.3.0"
},
"links": {
"system": 11111
{
"id": 67890,
"name": "Bridge 2",
"mode": "home",
"hardware_id": "0x0000000000000000",
"hardware_revision": 4,
"firmware_version": {
"wifi": "0.121.0",
"wifi_app": "3.3.0",
"silabs": "1.1.2"
},
"missing_at": null,
"created_at": "2019-04-30T01:43:50.497Z",
"updated_at": "2023-01-02T19:09:58.251Z",
"system_id": 11111,
"firmware": {
"wifi": "0.121.0",
"wifi_app": "3.3.0",
"silabs": "1.1.2"
},
"links": {
"system": 11111
}
}
},
{
"id": 67890,
"name": "Bridge 2",
"mode": "home",
"hardware_id": "0x0000000000000000",
"hardware_revision": 4,
"firmware_version": {
"wifi": "0.121.0",
"wifi_app": "3.3.0",
"silabs": "1.1.2"
},
"missing_at": null,
"created_at": "2019-04-30T01:43:50.497Z",
"updated_at": "2023-01-02T19:09:58.251Z",
"system_id": 11111,
"firmware": {
"wifi": "0.121.0",
"wifi_app": "3.3.0",
"silabs": "1.1.2"
},
"links": {
"system": 11111
}
}
]
]
}

View File

@ -1,55 +1,57 @@
[
{
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"definition_id": 4,
"created_at": "2019-06-28T22:12:49.651Z",
"type": "sensor",
"model_version": "2.1",
"sensor_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"status": {
"trigger_value": "no_leak",
"data_received_at": "2022-03-20T08:00:29.763Z"
},
"status_localized": {
"state": "No Leak",
"description": "Mar 20 at 2:00am"
},
"insights": {
"primary": {
"origin": {
"type": "Sensor",
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
},
"value": "no_leak",
{
"listeners": [
{
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"definition_id": 4,
"created_at": "2019-06-28T22:12:49.651Z",
"type": "sensor",
"model_version": "2.1",
"sensor_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"status": {
"trigger_value": "no_leak",
"data_received_at": "2022-03-20T08:00:29.763Z"
}
},
"status_localized": {
"state": "No Leak",
"description": "Mar 20 at 2:00am"
},
"insights": {
"primary": {
"origin": {
"type": "Sensor",
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
},
"value": "no_leak",
"data_received_at": "2022-03-20T08:00:29.763Z"
}
},
"configuration": {},
"pro_monitoring_status": "eligible"
},
"configuration": {},
"pro_monitoring_status": "eligible"
},
{
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"definition_id": 7,
"created_at": "2019-07-10T22:40:48.847Z",
"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.516Z"
},
"status_localized": {
"state": "No Sound",
"description": "Jun 28 at 4:12pm"
},
"insights": {
"primary": {
"origin": {},
"value": "no_alarm",
{
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"definition_id": 7,
"created_at": "2019-07-10T22:40:48.847Z",
"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.516Z"
}
},
"configuration": {},
"pro_monitoring_status": "eligible"
}
]
},
"status_localized": {
"state": "No Sound",
"description": "Jun 28 at 4:12pm"
},
"insights": {
"primary": {
"origin": {},
"value": "no_alarm",
"data_received_at": "2019-06-28T22:12:49.516Z"
}
},
"configuration": {},
"pro_monitoring_status": "eligible"
}
]
}

View File

@ -1,34 +1,36 @@
[
{
"id": 123456,
"uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"user": {
"id": 12345,
"email": "user@email.com"
},
"bridge": {
"id": 67890,
"hardware_id": "0x0000000000000000"
},
"last_bridge_hardware_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"name": "Sensor 1",
"location_id": 123456,
"system_id": 12345,
"hardware_id": "0x0000000000000000",
"hardware_revision": 5,
"firmware_version": "1.1.2",
"device_key": "0x0000000000000000",
"encryption_key": true,
"installed_at": "2019-06-28T22:12:51.209Z",
"calibrated_at": "2023-03-07T19:51:56.838Z",
"last_reported_at": "2023-04-19T18:09:40.479Z",
"missing_at": null,
"updated_at": "2023-03-28T13:33:33.801Z",
"created_at": "2019-06-28T22:12:20.256Z",
"signal_strength": 4,
"firmware": {
"status": "valid"
},
"surface_type": null
}
]
{
"sensors": [
{
"id": 123456,
"uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"user": {
"id": 12345,
"email": "user@email.com"
},
"bridge": {
"id": 67890,
"hardware_id": "0x0000000000000000"
},
"last_bridge_hardware_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"name": "Sensor 1",
"location_id": 123456,
"system_id": 12345,
"hardware_id": "0x0000000000000000",
"hardware_revision": 5,
"firmware_version": "1.1.2",
"device_key": "0x0000000000000000",
"encryption_key": true,
"installed_at": "2019-06-28T22:12:51.209Z",
"calibrated_at": "2023-03-07T19:51:56.838Z",
"last_reported_at": "2023-04-19T18:09:40.479Z",
"missing_at": null,
"updated_at": "2023-03-28T13:33:33.801Z",
"created_at": "2019-06-28T22:12:20.256Z",
"signal_strength": 4,
"firmware": {
"status": "valid"
},
"surface_type": null
}
]
}

View 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
}
}

View File

@ -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,
},
},
}

View File

@ -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,

View File

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

View File

@ -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": {

View File

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

View File

@ -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

View File

@ -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()

View File

@ -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"]