mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
2022.11.2 (#81780)
This commit is contained in:
commit
c757c9b99f
@ -7,13 +7,9 @@ from math import ceil
|
||||
from typing import Any
|
||||
|
||||
from pyairvisual import CloudAPI, NodeSamba
|
||||
from pyairvisual.errors import (
|
||||
AirVisualError,
|
||||
InvalidKeyError,
|
||||
KeyExpiredError,
|
||||
NodeProError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
from pyairvisual.cloud_api import InvalidKeyError, KeyExpiredError, UnauthorizedError
|
||||
from pyairvisual.errors import AirVisualError
|
||||
from pyairvisual.node import NodeProError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
|
@ -6,14 +6,14 @@ from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from pyairvisual import CloudAPI, NodeSamba
|
||||
from pyairvisual.errors import (
|
||||
AirVisualError,
|
||||
from pyairvisual.cloud_api import (
|
||||
InvalidKeyError,
|
||||
KeyExpiredError,
|
||||
NodeProError,
|
||||
NotFoundError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
from pyairvisual.errors import AirVisualError
|
||||
from pyairvisual.node import NodeProError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "AirVisual",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airvisual",
|
||||
"requirements": ["pyairvisual==2022.07.0"],
|
||||
"requirements": ["pyairvisual==2022.11.1"],
|
||||
"codeowners": ["@bachya"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyairvisual", "pysmb"],
|
||||
|
@ -6,9 +6,9 @@
|
||||
"after_dependencies": ["hassio"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"bleak==0.19.1",
|
||||
"bleak-retry-connector==2.8.2",
|
||||
"bluetooth-adapters==0.6.0",
|
||||
"bleak==0.19.2",
|
||||
"bleak-retry-connector==2.8.3",
|
||||
"bluetooth-adapters==0.7.0",
|
||||
"bluetooth-auto-recovery==0.3.6",
|
||||
"dbus-fast==1.61.1"
|
||||
],
|
||||
|
@ -262,7 +262,11 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
self.config_entry.entry_id
|
||||
]
|
||||
|
||||
await coordinator.async_update_sources()
|
||||
try:
|
||||
await coordinator.async_update_sources()
|
||||
except BraviaTVError:
|
||||
return self.async_abort(reason="failed_update")
|
||||
|
||||
sources = coordinator.source_map.values()
|
||||
self.source_list = [item["title"] for item in sources]
|
||||
return await self.async_step_user()
|
||||
|
@ -48,6 +48,9 @@
|
||||
"ignored_sources": "List of ignored sources"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"failed_update": "An error occurred while updating the list of sources.\n\n Ensure that your TV is turned on before trying to set it up."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,9 @@
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"abort": {
|
||||
"failed_update": "An error occurred while updating the list of sources.\n\n Ensure that your TV is turned on before trying to set it up."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -89,6 +89,7 @@ T = TypeVar(
|
||||
class DeconzSensorDescriptionMixin(Generic[T]):
|
||||
"""Required values when describing secondary sensor attributes."""
|
||||
|
||||
supported_fn: Callable[[T], bool]
|
||||
update_key: str
|
||||
value_fn: Callable[[T], datetime | StateType]
|
||||
|
||||
@ -105,6 +106,7 @@ class DeconzSensorDescription(SensorEntityDescription, DeconzSensorDescriptionMi
|
||||
ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
DeconzSensorDescription[AirQuality](
|
||||
key="air_quality",
|
||||
supported_fn=lambda device: device.air_quality is not None,
|
||||
update_key="airquality",
|
||||
value_fn=lambda device: device.air_quality,
|
||||
instance_check=AirQuality,
|
||||
@ -112,6 +114,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[AirQuality](
|
||||
key="air_quality_ppb",
|
||||
supported_fn=lambda device: device.air_quality_ppb is not None,
|
||||
update_key="airqualityppb",
|
||||
value_fn=lambda device: device.air_quality_ppb,
|
||||
instance_check=AirQuality,
|
||||
@ -122,6 +125,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[Consumption](
|
||||
key="consumption",
|
||||
supported_fn=lambda device: device.consumption is not None,
|
||||
update_key="consumption",
|
||||
value_fn=lambda device: device.scaled_consumption,
|
||||
instance_check=Consumption,
|
||||
@ -131,6 +135,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[Daylight](
|
||||
key="daylight_status",
|
||||
supported_fn=lambda device: True,
|
||||
update_key="status",
|
||||
value_fn=lambda device: DAYLIGHT_STATUS[device.daylight_status],
|
||||
instance_check=Daylight,
|
||||
@ -139,12 +144,14 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[GenericStatus](
|
||||
key="status",
|
||||
supported_fn=lambda device: device.status is not None,
|
||||
update_key="status",
|
||||
value_fn=lambda device: device.status,
|
||||
instance_check=GenericStatus,
|
||||
),
|
||||
DeconzSensorDescription[Humidity](
|
||||
key="humidity",
|
||||
supported_fn=lambda device: device.humidity is not None,
|
||||
update_key="humidity",
|
||||
value_fn=lambda device: device.scaled_humidity,
|
||||
instance_check=Humidity,
|
||||
@ -154,6 +161,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[LightLevel](
|
||||
key="light_level",
|
||||
supported_fn=lambda device: device.light_level is not None,
|
||||
update_key="lightlevel",
|
||||
value_fn=lambda device: device.scaled_light_level,
|
||||
instance_check=LightLevel,
|
||||
@ -163,6 +171,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[Power](
|
||||
key="power",
|
||||
supported_fn=lambda device: device.power is not None,
|
||||
update_key="power",
|
||||
value_fn=lambda device: device.power,
|
||||
instance_check=Power,
|
||||
@ -172,6 +181,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[Pressure](
|
||||
key="pressure",
|
||||
supported_fn=lambda device: device.pressure is not None,
|
||||
update_key="pressure",
|
||||
value_fn=lambda device: device.pressure,
|
||||
instance_check=Pressure,
|
||||
@ -181,6 +191,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[Temperature](
|
||||
key="temperature",
|
||||
supported_fn=lambda device: device.temperature is not None,
|
||||
update_key="temperature",
|
||||
value_fn=lambda device: device.scaled_temperature,
|
||||
instance_check=Temperature,
|
||||
@ -190,6 +201,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[Time](
|
||||
key="last_set",
|
||||
supported_fn=lambda device: device.last_set is not None,
|
||||
update_key="lastset",
|
||||
value_fn=lambda device: dt_util.parse_datetime(device.last_set),
|
||||
instance_check=Time,
|
||||
@ -197,6 +209,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[SensorResources](
|
||||
key="battery",
|
||||
supported_fn=lambda device: device.battery is not None,
|
||||
update_key="battery",
|
||||
value_fn=lambda device: device.battery,
|
||||
name_suffix="Battery",
|
||||
@ -208,6 +221,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[SensorResources](
|
||||
key="internal_temperature",
|
||||
supported_fn=lambda device: device.internal_temperature is not None,
|
||||
update_key="temperature",
|
||||
value_fn=lambda device: device.internal_temperature,
|
||||
name_suffix="Temperature",
|
||||
@ -268,7 +282,7 @@ async def async_setup_entry(
|
||||
continue
|
||||
|
||||
no_sensor_data = False
|
||||
if description.value_fn(sensor) is None:
|
||||
if not description.supported_fn(sensor):
|
||||
no_sensor_data = True
|
||||
|
||||
if description.instance_check is None:
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "DLNA Digital Media Renderer",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
||||
"requirements": ["async-upnp-client==0.32.1"],
|
||||
"requirements": ["async-upnp-client==0.32.2"],
|
||||
"dependencies": ["ssdp"],
|
||||
"after_dependencies": ["media_source"],
|
||||
"ssdp": [
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "DLNA Digital Media Server",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
|
||||
"requirements": ["async-upnp-client==0.32.1"],
|
||||
"requirements": ["async-upnp-client==0.32.2"],
|
||||
"dependencies": ["ssdp"],
|
||||
"after_dependencies": ["media_source"],
|
||||
"ssdp": [
|
||||
|
@ -7,11 +7,11 @@ import logging
|
||||
import re
|
||||
from types import MappingProxyType
|
||||
from typing import Any, cast
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import async_timeout
|
||||
from elkm1_lib.elements import Element
|
||||
from elkm1_lib.elk import Elk
|
||||
from elkm1_lib.util import parse_url
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
@ -96,6 +96,11 @@ SET_TIME_SERVICE_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
def hostname_from_url(url: str) -> str:
|
||||
"""Return the hostname from a url."""
|
||||
return parse_url(url)[1]
|
||||
|
||||
|
||||
def _host_validator(config: dict[str, str]) -> dict[str, str]:
|
||||
"""Validate that a host is properly configured."""
|
||||
if config[CONF_HOST].startswith("elks://"):
|
||||
@ -231,7 +236,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Elk-M1 Control from a config entry."""
|
||||
conf: MappingProxyType[str, Any] = entry.data
|
||||
|
||||
host = urlparse(entry.data[CONF_HOST]).hostname
|
||||
host = hostname_from_url(entry.data[CONF_HOST])
|
||||
|
||||
_LOGGER.debug("Setting up elkm1 %s", conf["host"])
|
||||
|
||||
|
@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from elkm1_lib.discovery import ElkSystem
|
||||
from elkm1_lib.elk import Elk
|
||||
@ -26,7 +25,7 @@ from homeassistant.helpers.typing import DiscoveryInfoType
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util.network import is_ip_address
|
||||
|
||||
from . import async_wait_for_elk_to_sync
|
||||
from . import async_wait_for_elk_to_sync, hostname_from_url
|
||||
from .const import CONF_AUTO_CONFIGURE, DISCOVER_SCAN_TIMEOUT, DOMAIN, LOGIN_TIMEOUT
|
||||
from .discovery import (
|
||||
_short_mac,
|
||||
@ -170,7 +169,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
for entry in self._async_current_entries(include_ignore=False):
|
||||
if (
|
||||
entry.unique_id == mac
|
||||
or urlparse(entry.data[CONF_HOST]).hostname == host
|
||||
or hostname_from_url(entry.data[CONF_HOST]) == host
|
||||
):
|
||||
if async_update_entry_from_discovery(self.hass, entry, device):
|
||||
self.hass.async_create_task(
|
||||
@ -214,7 +213,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
current_unique_ids = self._async_current_ids()
|
||||
current_hosts = {
|
||||
urlparse(entry.data[CONF_HOST]).hostname
|
||||
hostname_from_url(entry.data[CONF_HOST])
|
||||
for entry in self._async_current_entries(include_ignore=False)
|
||||
}
|
||||
discovered_devices = await async_discover_devices(
|
||||
@ -344,7 +343,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if self._url_already_configured(url):
|
||||
return self.async_abort(reason="address_already_configured")
|
||||
|
||||
host = urlparse(url).hostname
|
||||
host = hostname_from_url(url)
|
||||
_LOGGER.debug(
|
||||
"Importing is trying to fill unique id from discovery for %s", host
|
||||
)
|
||||
@ -367,10 +366,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
def _url_already_configured(self, url: str) -> bool:
|
||||
"""See if we already have a elkm1 matching user input configured."""
|
||||
existing_hosts = {
|
||||
urlparse(entry.data[CONF_HOST]).hostname
|
||||
hostname_from_url(entry.data[CONF_HOST])
|
||||
for entry in self._async_current_entries()
|
||||
}
|
||||
return urlparse(url).hostname in existing_hosts
|
||||
return hostname_from_url(url) in existing_hosts
|
||||
|
||||
|
||||
class InvalidAuth(exceptions.HomeAssistantError):
|
||||
|
@ -137,6 +137,7 @@ class ESPHomeClient(BaseBleakClient):
|
||||
was_connected = self._is_connected
|
||||
self.services = BleakGATTServiceCollection() # type: ignore[no-untyped-call]
|
||||
self._is_connected = False
|
||||
self._notify_cancels.clear()
|
||||
if self._disconnected_event:
|
||||
self._disconnected_event.set()
|
||||
self._disconnected_event = None
|
||||
@ -463,12 +464,20 @@ class ESPHomeClient(BaseBleakClient):
|
||||
UUID or directly by the BleakGATTCharacteristic object representing it.
|
||||
callback (function): The function to be called on notification.
|
||||
"""
|
||||
ble_handle = characteristic.handle
|
||||
if ble_handle in self._notify_cancels:
|
||||
raise BleakError(
|
||||
"Notifications are already enabled on "
|
||||
f"service:{characteristic.service_uuid} "
|
||||
f"characteristic:{characteristic.uuid} "
|
||||
f"handle:{ble_handle}"
|
||||
)
|
||||
cancel_coro = await self._client.bluetooth_gatt_start_notify(
|
||||
self._address_as_int,
|
||||
characteristic.handle,
|
||||
ble_handle,
|
||||
lambda handle, data: callback(data),
|
||||
)
|
||||
self._notify_cancels[characteristic.handle] = cancel_coro
|
||||
self._notify_cancels[ble_handle] = cancel_coro
|
||||
|
||||
@api_error_as_bleak_error
|
||||
async def stop_notify(
|
||||
@ -483,5 +492,7 @@ class ESPHomeClient(BaseBleakClient):
|
||||
directly by the BleakGATTCharacteristic object representing it.
|
||||
"""
|
||||
characteristic = self._resolve_characteristic(char_specifier)
|
||||
coro = self._notify_cancels.pop(characteristic.handle)
|
||||
await coro()
|
||||
# Do not raise KeyError if notifications are not enabled on this characteristic
|
||||
# to be consistent with the behavior of the BlueZ backend
|
||||
if coro := self._notify_cancels.pop(characteristic.handle, None):
|
||||
await coro()
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "ESPHome",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/esphome",
|
||||
"requirements": ["aioesphomeapi==11.4.2"],
|
||||
"requirements": ["aioesphomeapi==11.4.3"],
|
||||
"zeroconf": ["_esphomelib._tcp.local."],
|
||||
"dhcp": [{ "registered_devices": true }],
|
||||
"codeowners": ["@OttoWinter", "@jesserockz"],
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": ["home-assistant-frontend==20221102.1"],
|
||||
"requirements": ["home-assistant-frontend==20221108.0"],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
|
@ -3,11 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Iterable
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from gcal_sync.api import SyncEventsRequest
|
||||
from gcal_sync.api import GoogleCalendarService, ListEventsRequest, SyncEventsRequest
|
||||
from gcal_sync.exceptions import ApiException
|
||||
from gcal_sync.model import DateOrDatetime, Event
|
||||
from gcal_sync.store import ScopedCalendarStore
|
||||
@ -196,21 +197,30 @@ async def async_setup_entry(
|
||||
entity_registry.async_remove(
|
||||
entity_entry.entity_id,
|
||||
)
|
||||
request_template = SyncEventsRequest(
|
||||
calendar_id=calendar_id,
|
||||
search=data.get(CONF_SEARCH),
|
||||
start_time=dt_util.now() + SYNC_EVENT_MIN_TIME,
|
||||
)
|
||||
sync = CalendarEventSyncManager(
|
||||
calendar_service,
|
||||
store=ScopedCalendarStore(store, unique_id or entity_name),
|
||||
request_template=request_template,
|
||||
)
|
||||
coordinator = CalendarUpdateCoordinator(
|
||||
hass,
|
||||
sync,
|
||||
data[CONF_NAME],
|
||||
)
|
||||
coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator
|
||||
if search := data.get(CONF_SEARCH):
|
||||
coordinator = CalendarQueryUpdateCoordinator(
|
||||
hass,
|
||||
calendar_service,
|
||||
data[CONF_NAME],
|
||||
calendar_id,
|
||||
search,
|
||||
)
|
||||
else:
|
||||
request_template = SyncEventsRequest(
|
||||
calendar_id=calendar_id,
|
||||
start_time=dt_util.now() + SYNC_EVENT_MIN_TIME,
|
||||
)
|
||||
sync = CalendarEventSyncManager(
|
||||
calendar_service,
|
||||
store=ScopedCalendarStore(store, unique_id or entity_name),
|
||||
request_template=request_template,
|
||||
)
|
||||
coordinator = CalendarSyncUpdateCoordinator(
|
||||
hass,
|
||||
sync,
|
||||
data[CONF_NAME],
|
||||
)
|
||||
entities.append(
|
||||
GoogleCalendarEntity(
|
||||
coordinator,
|
||||
@ -242,8 +252,8 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]):
|
||||
"""Coordinator for calendar RPC calls."""
|
||||
class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]):
|
||||
"""Coordinator for calendar RPC calls that use an efficient sync."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -251,7 +261,7 @@ class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]):
|
||||
sync: CalendarEventSyncManager,
|
||||
name: str,
|
||||
) -> None:
|
||||
"""Create the Calendar event device."""
|
||||
"""Create the CalendarSyncUpdateCoordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
@ -271,6 +281,87 @@ class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]):
|
||||
dt_util.DEFAULT_TIME_ZONE
|
||||
)
|
||||
|
||||
async def async_get_events(
|
||||
self, start_date: datetime, end_date: datetime
|
||||
) -> Iterable[Event]:
|
||||
"""Get all events in a specific time frame."""
|
||||
if not self.data:
|
||||
raise HomeAssistantError(
|
||||
"Unable to get events: Sync from server has not completed"
|
||||
)
|
||||
return self.data.overlapping(
|
||||
dt_util.as_local(start_date),
|
||||
dt_util.as_local(end_date),
|
||||
)
|
||||
|
||||
@property
|
||||
def upcoming(self) -> Iterable[Event] | None:
|
||||
"""Return upcoming events if any."""
|
||||
if self.data:
|
||||
return self.data.active_after(dt_util.now())
|
||||
return None
|
||||
|
||||
|
||||
class CalendarQueryUpdateCoordinator(DataUpdateCoordinator[list[Event]]):
|
||||
"""Coordinator for calendar RPC calls.
|
||||
|
||||
This sends a polling RPC, not using sync, as a workaround
|
||||
for limitations in the calendar API for supporting search.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
calendar_service: GoogleCalendarService,
|
||||
name: str,
|
||||
calendar_id: str,
|
||||
search: str | None,
|
||||
) -> None:
|
||||
"""Create the CalendarQueryUpdateCoordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=name,
|
||||
update_interval=MIN_TIME_BETWEEN_UPDATES,
|
||||
)
|
||||
self.calendar_service = calendar_service
|
||||
self.calendar_id = calendar_id
|
||||
self._search = search
|
||||
|
||||
async def async_get_events(
|
||||
self, start_date: datetime, end_date: datetime
|
||||
) -> Iterable[Event]:
|
||||
"""Get all events in a specific time frame."""
|
||||
request = ListEventsRequest(
|
||||
calendar_id=self.calendar_id,
|
||||
start_time=start_date,
|
||||
end_time=end_date,
|
||||
search=self._search,
|
||||
)
|
||||
result_items = []
|
||||
try:
|
||||
result = await self.calendar_service.async_list_events(request)
|
||||
async for result_page in result:
|
||||
result_items.extend(result_page.items)
|
||||
except ApiException as err:
|
||||
self.async_set_update_error(err)
|
||||
raise HomeAssistantError(str(err)) from err
|
||||
return result_items
|
||||
|
||||
async def _async_update_data(self) -> list[Event]:
|
||||
"""Fetch data from API endpoint."""
|
||||
request = ListEventsRequest(calendar_id=self.calendar_id, search=self._search)
|
||||
try:
|
||||
result = await self.calendar_service.async_list_events(request)
|
||||
except ApiException as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
return result.items
|
||||
|
||||
@property
|
||||
def upcoming(self) -> Iterable[Event] | None:
|
||||
"""Return the next upcoming event if any."""
|
||||
return self.data
|
||||
|
||||
|
||||
class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
|
||||
"""A calendar event entity."""
|
||||
@ -279,7 +370,7 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: CalendarUpdateCoordinator,
|
||||
coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator,
|
||||
calendar_id: str,
|
||||
data: dict[str, Any],
|
||||
entity_id: str,
|
||||
@ -352,14 +443,7 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
) -> list[CalendarEvent]:
|
||||
"""Get all events in a specific time frame."""
|
||||
if not (timeline := self.coordinator.data):
|
||||
raise HomeAssistantError(
|
||||
"Unable to get events: Sync from server has not completed"
|
||||
)
|
||||
result_items = timeline.overlapping(
|
||||
dt_util.as_local(start_date),
|
||||
dt_util.as_local(end_date),
|
||||
)
|
||||
result_items = await self.coordinator.async_get_events(start_date, end_date)
|
||||
return [
|
||||
_get_calendar_event(event)
|
||||
for event in filter(self._event_filter, result_items)
|
||||
@ -367,14 +451,12 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
|
||||
|
||||
def _apply_coordinator_update(self) -> None:
|
||||
"""Copy state from the coordinator to this entity."""
|
||||
if (timeline := self.coordinator.data) and (
|
||||
api_event := next(
|
||||
filter(
|
||||
self._event_filter,
|
||||
timeline.active_after(dt_util.now()),
|
||||
),
|
||||
None,
|
||||
)
|
||||
if api_event := next(
|
||||
filter(
|
||||
self._event_filter,
|
||||
self.coordinator.upcoming or [],
|
||||
),
|
||||
None,
|
||||
):
|
||||
self._event = _get_calendar_event(api_event)
|
||||
(self._event.summary, self._offset_value) = extract_offset(
|
||||
|
@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/calendar.google/",
|
||||
"requirements": ["gcal-sync==2.2.3", "oauth2client==4.1.3"],
|
||||
"requirements": ["gcal-sync==4.0.0", "oauth2client==4.1.3"],
|
||||
"codeowners": ["@allenporter"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"]
|
||||
|
@ -10,8 +10,10 @@ import os
|
||||
from typing import Any, cast
|
||||
|
||||
from aiohttp import web
|
||||
from pyhap.characteristic import Characteristic
|
||||
from pyhap.const import STANDALONE_AID
|
||||
from pyhap.loader import get_loader
|
||||
from pyhap.service import Service
|
||||
import voluptuous as vol
|
||||
from zeroconf.asyncio import AsyncZeroconf
|
||||
|
||||
@ -74,13 +76,7 @@ from . import ( # noqa: F401
|
||||
type_switches,
|
||||
type_thermostats,
|
||||
)
|
||||
from .accessories import (
|
||||
HomeAccessory,
|
||||
HomeBridge,
|
||||
HomeDriver,
|
||||
HomeIIDManager,
|
||||
get_accessory,
|
||||
)
|
||||
from .accessories import HomeAccessory, HomeBridge, HomeDriver, get_accessory
|
||||
from .aidmanager import AccessoryAidStorage
|
||||
from .const import (
|
||||
ATTR_INTEGRATION,
|
||||
@ -139,7 +135,7 @@ STATUS_WAIT = 3
|
||||
PORT_CLEANUP_CHECK_INTERVAL_SECS = 1
|
||||
|
||||
_HOMEKIT_CONFIG_UPDATE_TIME = (
|
||||
5 # number of seconds to wait for homekit to see the c# change
|
||||
10 # number of seconds to wait for homekit to see the c# change
|
||||
)
|
||||
|
||||
|
||||
@ -529,6 +525,7 @@ class HomeKit:
|
||||
self.status = STATUS_READY
|
||||
self.driver: HomeDriver | None = None
|
||||
self.bridge: HomeBridge | None = None
|
||||
self._reset_lock = asyncio.Lock()
|
||||
|
||||
def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> None:
|
||||
"""Set up bridge and accessory driver."""
|
||||
@ -548,7 +545,7 @@ class HomeKit:
|
||||
async_zeroconf_instance=async_zeroconf_instance,
|
||||
zeroconf_server=f"{uuid}-hap.local.",
|
||||
loader=get_loader(),
|
||||
iid_manager=HomeIIDManager(self.iid_storage),
|
||||
iid_storage=self.iid_storage,
|
||||
)
|
||||
|
||||
# If we do not load the mac address will be wrong
|
||||
@ -558,21 +555,24 @@ class HomeKit:
|
||||
|
||||
async def async_reset_accessories(self, entity_ids: Iterable[str]) -> None:
|
||||
"""Reset the accessory to load the latest configuration."""
|
||||
if not self.bridge:
|
||||
await self.async_reset_accessories_in_accessory_mode(entity_ids)
|
||||
return
|
||||
await self.async_reset_accessories_in_bridge_mode(entity_ids)
|
||||
async with self._reset_lock:
|
||||
if not self.bridge:
|
||||
await self.async_reset_accessories_in_accessory_mode(entity_ids)
|
||||
return
|
||||
await self.async_reset_accessories_in_bridge_mode(entity_ids)
|
||||
|
||||
async def _async_shutdown_accessory(self, accessory: HomeAccessory) -> None:
|
||||
"""Shutdown an accessory."""
|
||||
assert self.driver is not None
|
||||
await accessory.stop()
|
||||
# Deallocate the IIDs for the accessory
|
||||
iid_manager = self.driver.iid_manager
|
||||
for service in accessory.services:
|
||||
iid_manager.remove_iid(iid_manager.remove_obj(service))
|
||||
for char in service.characteristics:
|
||||
iid_manager.remove_iid(iid_manager.remove_obj(char))
|
||||
iid_manager = accessory.iid_manager
|
||||
services: list[Service] = accessory.services
|
||||
for service in services:
|
||||
iid_manager.remove_obj(service)
|
||||
characteristics: list[Characteristic] = service.characteristics
|
||||
for char in characteristics:
|
||||
iid_manager.remove_obj(char)
|
||||
|
||||
async def async_reset_accessories_in_accessory_mode(
|
||||
self, entity_ids: Iterable[str]
|
||||
@ -581,7 +581,6 @@ class HomeKit:
|
||||
assert self.driver is not None
|
||||
|
||||
acc = cast(HomeAccessory, self.driver.accessory)
|
||||
await self._async_shutdown_accessory(acc)
|
||||
if acc.entity_id not in entity_ids:
|
||||
return
|
||||
if not (state := self.hass.states.get(acc.entity_id)):
|
||||
@ -589,6 +588,7 @@ class HomeKit:
|
||||
"The underlying entity %s disappeared during reset", acc.entity_id
|
||||
)
|
||||
return
|
||||
await self._async_shutdown_accessory(acc)
|
||||
if new_acc := self._async_create_single_accessory([state]):
|
||||
self.driver.accessory = new_acc
|
||||
self.hass.async_add_job(new_acc.run)
|
||||
|
@ -270,7 +270,7 @@ class HomeAccessory(Accessory): # type: ignore[misc]
|
||||
driver=driver,
|
||||
display_name=cleanup_name_for_homekit(name),
|
||||
aid=aid,
|
||||
iid_manager=driver.iid_manager,
|
||||
iid_manager=HomeIIDManager(driver.iid_storage),
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
@ -570,7 +570,7 @@ class HomeBridge(Bridge): # type: ignore[misc]
|
||||
|
||||
def __init__(self, hass: HomeAssistant, driver: HomeDriver, name: str) -> None:
|
||||
"""Initialize a Bridge object."""
|
||||
super().__init__(driver, name, iid_manager=driver.iid_manager)
|
||||
super().__init__(driver, name, iid_manager=HomeIIDManager(driver.iid_storage))
|
||||
self.set_info_service(
|
||||
firmware_revision=format_version(__version__),
|
||||
manufacturer=MANUFACTURER,
|
||||
@ -603,7 +603,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
|
||||
entry_id: str,
|
||||
bridge_name: str,
|
||||
entry_title: str,
|
||||
iid_manager: HomeIIDManager,
|
||||
iid_storage: AccessoryIIDStorage,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize a AccessoryDriver object."""
|
||||
@ -612,7 +612,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
|
||||
self._entry_id = entry_id
|
||||
self._bridge_name = bridge_name
|
||||
self._entry_title = entry_title
|
||||
self.iid_manager = iid_manager
|
||||
self.iid_storage = iid_storage
|
||||
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
def pair(
|
||||
|
@ -31,6 +31,8 @@ async def async_get_config_entry_diagnostics(
|
||||
"options": dict(entry.options),
|
||||
},
|
||||
}
|
||||
if homekit.iid_storage:
|
||||
data["iid_storage"] = homekit.iid_storage.allocations
|
||||
if not homekit.driver: # not started yet or startup failed
|
||||
return data
|
||||
driver: AccessoryDriver = homekit.driver
|
||||
|
@ -17,7 +17,7 @@ from homeassistant.helpers.storage import Store
|
||||
|
||||
from .util import get_iid_storage_filename_for_entry_id
|
||||
|
||||
IID_MANAGER_STORAGE_VERSION = 1
|
||||
IID_MANAGER_STORAGE_VERSION = 2
|
||||
IID_MANAGER_SAVE_DELAY = 2
|
||||
|
||||
ALLOCATIONS_KEY = "allocations"
|
||||
@ -26,6 +26,40 @@ IID_MIN = 1
|
||||
IID_MAX = 18446744073709551615
|
||||
|
||||
|
||||
ACCESSORY_INFORMATION_SERVICE = "3E"
|
||||
|
||||
|
||||
class IIDStorage(Store):
|
||||
"""Storage class for IIDManager."""
|
||||
|
||||
async def _async_migrate_func(
|
||||
self,
|
||||
old_major_version: int,
|
||||
old_minor_version: int,
|
||||
old_data: dict,
|
||||
):
|
||||
"""Migrate to the new version."""
|
||||
if old_major_version == 1:
|
||||
# Convert v1 to v2 format which uses a unique iid set per accessory
|
||||
# instead of per pairing since we need the ACCESSORY_INFORMATION_SERVICE
|
||||
# to always have iid 1 for each bridged accessory as well as the bridge
|
||||
old_allocations: dict[str, int] = old_data.pop(ALLOCATIONS_KEY, {})
|
||||
new_allocation: dict[str, dict[str, int]] = {}
|
||||
old_data[ALLOCATIONS_KEY] = new_allocation
|
||||
for allocation_key, iid in old_allocations.items():
|
||||
aid_str, new_allocation_key = allocation_key.split("_", 1)
|
||||
service_type, _, char_type, *_ = new_allocation_key.split("_")
|
||||
accessory_allocation = new_allocation.setdefault(aid_str, {})
|
||||
if service_type == ACCESSORY_INFORMATION_SERVICE and not char_type:
|
||||
accessory_allocation[new_allocation_key] = 1
|
||||
elif iid != 1:
|
||||
accessory_allocation[new_allocation_key] = iid
|
||||
|
||||
return old_data
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AccessoryIIDStorage:
|
||||
"""
|
||||
Provide stable allocation of IIDs for the lifetime of an accessory.
|
||||
@ -37,15 +71,15 @@ class AccessoryIIDStorage:
|
||||
def __init__(self, hass: HomeAssistant, entry_id: str) -> None:
|
||||
"""Create a new iid store."""
|
||||
self.hass = hass
|
||||
self.allocations: dict[str, int] = {}
|
||||
self.allocated_iids: list[int] = []
|
||||
self.allocations: dict[str, dict[str, int]] = {}
|
||||
self.allocated_iids: dict[str, list[int]] = {}
|
||||
self.entry_id = entry_id
|
||||
self.store: Store | None = None
|
||||
self.store: IIDStorage | None = None
|
||||
|
||||
async def async_initialize(self) -> None:
|
||||
"""Load the latest IID data."""
|
||||
iid_store = get_iid_storage_filename_for_entry_id(self.entry_id)
|
||||
self.store = Store(self.hass, IID_MANAGER_STORAGE_VERSION, iid_store)
|
||||
self.store = IIDStorage(self.hass, IID_MANAGER_STORAGE_VERSION, iid_store)
|
||||
|
||||
if not (raw_storage := await self.store.async_load()):
|
||||
# There is no data about iid allocations yet
|
||||
@ -53,7 +87,8 @@ class AccessoryIIDStorage:
|
||||
|
||||
assert isinstance(raw_storage, dict)
|
||||
self.allocations = raw_storage.get(ALLOCATIONS_KEY, {})
|
||||
self.allocated_iids = sorted(self.allocations.values())
|
||||
for aid_str, allocations in self.allocations.items():
|
||||
self.allocated_iids[aid_str] = sorted(allocations.values())
|
||||
|
||||
def get_or_allocate_iid(
|
||||
self,
|
||||
@ -68,16 +103,25 @@ class AccessoryIIDStorage:
|
||||
char_hap_type: str | None = uuid_to_hap_type(char_uuid) if char_uuid else None
|
||||
# Allocation key must be a string since we are saving it to JSON
|
||||
allocation_key = (
|
||||
f'{aid}_{service_hap_type}_{service_unique_id or ""}_'
|
||||
f'{service_hap_type}_{service_unique_id or ""}_'
|
||||
f'{char_hap_type or ""}_{char_unique_id or ""}'
|
||||
)
|
||||
if allocation_key in self.allocations:
|
||||
return self.allocations[allocation_key]
|
||||
next_iid = self.allocated_iids[-1] + 1 if self.allocated_iids else 1
|
||||
self.allocations[allocation_key] = next_iid
|
||||
self.allocated_iids.append(next_iid)
|
||||
# AID must be a string since JSON keys cannot be int
|
||||
aid_str = str(aid)
|
||||
accessory_allocation = self.allocations.setdefault(aid_str, {})
|
||||
accessory_allocated_iids = self.allocated_iids.setdefault(aid_str, [1])
|
||||
if service_hap_type == ACCESSORY_INFORMATION_SERVICE and char_uuid is None:
|
||||
return 1
|
||||
if allocation_key in accessory_allocation:
|
||||
return accessory_allocation[allocation_key]
|
||||
if accessory_allocated_iids:
|
||||
allocated_iid = accessory_allocated_iids[-1] + 1
|
||||
else:
|
||||
allocated_iid = 2
|
||||
accessory_allocation[allocation_key] = allocated_iid
|
||||
accessory_allocated_iids.append(allocated_iid)
|
||||
self._async_schedule_save()
|
||||
return next_iid
|
||||
return allocated_iid
|
||||
|
||||
@callback
|
||||
def _async_schedule_save(self) -> None:
|
||||
@ -91,6 +135,6 @@ class AccessoryIIDStorage:
|
||||
return await self.store.async_save(self._data_to_save())
|
||||
|
||||
@callback
|
||||
def _data_to_save(self) -> dict[str, dict[str, int]]:
|
||||
def _data_to_save(self) -> dict[str, dict[str, dict[str, int]]]:
|
||||
"""Return data of entity map to store in a file."""
|
||||
return {ALLOCATIONS_KEY: self.allocations}
|
||||
|
@ -306,7 +306,7 @@ class Thermostat(HomeAccessory):
|
||||
if attributes.get(ATTR_HVAC_ACTION) is not None:
|
||||
self.fan_chars.append(CHAR_CURRENT_FAN_STATE)
|
||||
serv_fan = self.add_preload_service(SERV_FANV2, self.fan_chars)
|
||||
serv_fan.add_linked_service(serv_thermostat)
|
||||
serv_thermostat.add_linked_service(serv_fan)
|
||||
self.char_active = serv_fan.configure_char(
|
||||
CHAR_ACTIVE, value=1, setter_callback=self._set_fan_active
|
||||
)
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "HomeKit Controller",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"requirements": ["aiohomekit==2.2.14"],
|
||||
"requirements": ["aiohomekit==2.2.18"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
|
||||
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }],
|
||||
"dependencies": ["bluetooth", "zeroconf"],
|
||||
|
@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/iaqualink/",
|
||||
"codeowners": ["@flz"],
|
||||
"requirements": ["iaqualink==0.5.0"],
|
||||
"requirements": ["iaqualink==0.5.0", "h2==4.1.0"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["iaqualink"]
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "lidarr",
|
||||
"name": "Lidarr",
|
||||
"documentation": "https://www.home-assistant.io/integrations/lidarr",
|
||||
"requirements": ["aiopyarr==22.10.0"],
|
||||
"requirements": ["aiopyarr==22.11.0"],
|
||||
"codeowners": ["@tkdrob"],
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling",
|
||||
|
@ -14,8 +14,11 @@ from awesomeversion import AwesomeVersion
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS_PCT,
|
||||
ATTR_COLOR_NAME,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ATTR_HS_COLOR,
|
||||
ATTR_KELVIN,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_XY_COLOR,
|
||||
)
|
||||
@ -24,7 +27,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
from .const import DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT
|
||||
from .const import _LOGGER, DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT
|
||||
|
||||
FIX_MAC_FW = AwesomeVersion("3.70")
|
||||
|
||||
@ -80,6 +83,17 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] |
|
||||
"""
|
||||
hue, saturation, brightness, kelvin = [None] * 4
|
||||
|
||||
if (color_name := kwargs.get(ATTR_COLOR_NAME)) is not None:
|
||||
try:
|
||||
hue, saturation = color_util.color_RGB_to_hs(
|
||||
*color_util.color_name_to_rgb(color_name)
|
||||
)
|
||||
except ValueError:
|
||||
_LOGGER.warning(
|
||||
"Got unknown color %s, falling back to neutral white", color_name
|
||||
)
|
||||
hue, saturation = (0, 0)
|
||||
|
||||
if ATTR_HS_COLOR in kwargs:
|
||||
hue, saturation = kwargs[ATTR_HS_COLOR]
|
||||
elif ATTR_RGB_COLOR in kwargs:
|
||||
@ -93,6 +107,13 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] |
|
||||
saturation = int(saturation / 100 * 65535)
|
||||
kelvin = 3500
|
||||
|
||||
if ATTR_KELVIN in kwargs:
|
||||
_LOGGER.warning(
|
||||
"The 'kelvin' parameter is deprecated. Please use 'color_temp_kelvin' for all service calls"
|
||||
)
|
||||
kelvin = kwargs.pop(ATTR_KELVIN)
|
||||
saturation = 0
|
||||
|
||||
if ATTR_COLOR_TEMP_KELVIN in kwargs:
|
||||
kelvin = kwargs.pop(ATTR_COLOR_TEMP_KELVIN)
|
||||
saturation = 0
|
||||
@ -100,6 +121,9 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] |
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS])
|
||||
|
||||
if ATTR_BRIGHTNESS_PCT in kwargs:
|
||||
brightness = convert_8_to_16(round(255 * kwargs[ATTR_BRIGHTNESS_PCT] / 100))
|
||||
|
||||
hsbk = [hue, saturation, brightness, kelvin]
|
||||
return None if hsbk == [None] * 4 else hsbk
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Litter-Robot",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/litterrobot",
|
||||
"requirements": ["pylitterbot==2022.10.2"],
|
||||
"requirements": ["pylitterbot==2022.11.0"],
|
||||
"codeowners": ["@natekspencer", "@tkdrob"],
|
||||
"dhcp": [{ "hostname": "litter-robot4" }],
|
||||
"iot_class": "cloud_push",
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "netatmo",
|
||||
"name": "Netatmo",
|
||||
"documentation": "https://www.home-assistant.io/integrations/netatmo",
|
||||
"requirements": ["pyatmo==7.3.0"],
|
||||
"requirements": ["pyatmo==7.4.0"],
|
||||
"after_dependencies": ["cloud", "media_source"],
|
||||
"dependencies": ["application_credentials", "webhook"],
|
||||
"codeowners": ["@cgtobi"],
|
||||
|
@ -80,6 +80,11 @@ class NexiaThermostatEntity(NexiaEntity):
|
||||
self.hass, f"{SIGNAL_THERMOSTAT_UPDATE}-{self._thermostat.thermostat_id}"
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if thermostat is available and data is available."""
|
||||
return super().available and self._thermostat.is_online
|
||||
|
||||
|
||||
class NexiaThermostatZoneEntity(NexiaThermostatEntity):
|
||||
"""Base class for nexia devices attached to a thermostat."""
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "nexia",
|
||||
"name": "Nexia/American Standard/Trane",
|
||||
"requirements": ["nexia==2.0.5"],
|
||||
"requirements": ["nexia==2.0.6"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/nexia",
|
||||
"config_flow": true,
|
||||
|
@ -8,7 +8,7 @@
|
||||
"manufacturer_id": 220
|
||||
}
|
||||
],
|
||||
"requirements": ["oralb-ble==0.10.0"],
|
||||
"requirements": ["oralb-ble==0.13.0"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"iot_class": "local_push"
|
||||
|
@ -5,6 +5,7 @@ from typing import TypedDict
|
||||
|
||||
from p1monitor import (
|
||||
P1Monitor,
|
||||
P1MonitorConnectionError,
|
||||
P1MonitorNoDataError,
|
||||
Phases,
|
||||
Settings,
|
||||
@ -101,8 +102,8 @@ class P1MonitorDataUpdateCoordinator(DataUpdateCoordinator[P1MonitorData]):
|
||||
try:
|
||||
data[SERVICE_WATERMETER] = await self.p1monitor.watermeter()
|
||||
self.has_water_meter = True
|
||||
except P1MonitorNoDataError:
|
||||
LOGGER.debug("No watermeter data received from P1 Monitor")
|
||||
except (P1MonitorNoDataError, P1MonitorConnectionError):
|
||||
LOGGER.debug("No water meter data received from P1 Monitor")
|
||||
if self.has_water_meter is None:
|
||||
self.has_water_meter = False
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "P1 Monitor",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/p1_monitor",
|
||||
"requirements": ["p1monitor==2.1.0"],
|
||||
"requirements": ["p1monitor==2.1.1"],
|
||||
"codeowners": ["@klaasnicolaas"],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "local_polling",
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "plugwise",
|
||||
"name": "Plugwise",
|
||||
"documentation": "https://www.home-assistant.io/integrations/plugwise",
|
||||
"requirements": ["plugwise==0.25.3"],
|
||||
"requirements": ["plugwise==0.25.7"],
|
||||
"codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"],
|
||||
"zeroconf": ["_plugwise._tcp.local."],
|
||||
"config_flow": true,
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "radarr",
|
||||
"name": "Radarr",
|
||||
"documentation": "https://www.home-assistant.io/integrations/radarr",
|
||||
"requirements": ["aiopyarr==22.10.0"],
|
||||
"requirements": ["aiopyarr==22.11.0"],
|
||||
"codeowners": ["@tkdrob"],
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling",
|
||||
|
@ -237,6 +237,7 @@ async def async_setup_entry(
|
||||
|
||||
# Add switches to control restrictions:
|
||||
for description in RESTRICTIONS_SWITCH_DESCRIPTIONS:
|
||||
coordinator = data.coordinators[description.api_category]
|
||||
if not key_exists(coordinator.data, description.data_key):
|
||||
continue
|
||||
entities.append(RainMachineRestrictionSwitch(entry, data, description))
|
||||
|
@ -89,6 +89,13 @@ COMBINED_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.All(cv.ensure_list, [COMBINED_SCHEMA])},
|
||||
{
|
||||
DOMAIN: vol.All(
|
||||
# convert empty dict to empty list
|
||||
lambda x: [] if x == {} else x,
|
||||
cv.ensure_list,
|
||||
[COMBINED_SCHEMA],
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
@ -7,7 +7,7 @@
|
||||
"samsungctl[websocket]==0.7.1",
|
||||
"samsungtvws[async,encrypted]==2.5.0",
|
||||
"wakeonlan==2.1.0",
|
||||
"async-upnp-client==0.32.1"
|
||||
"async-upnp-client==0.32.2"
|
||||
],
|
||||
"ssdp": [
|
||||
{
|
||||
|
@ -23,6 +23,7 @@ from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_RESOURCE,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_USERNAME,
|
||||
@ -43,7 +44,7 @@ from .coordinator import ScrapeCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=10)
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
CONF_ATTR = "attribute"
|
||||
CONF_SELECT = "select"
|
||||
@ -111,7 +112,8 @@ async def async_setup_platform(
|
||||
|
||||
rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl)
|
||||
|
||||
coordinator = ScrapeCoordinator(hass, rest, SCAN_INTERVAL)
|
||||
scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
coordinator = ScrapeCoordinator(hass, rest, scan_interval)
|
||||
await coordinator.async_refresh()
|
||||
if coordinator.data is None:
|
||||
raise PlatformNotReady
|
||||
|
@ -9,6 +9,7 @@ from aioshelly.block_device import Block
|
||||
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry, entity, entity_registry
|
||||
@ -615,6 +616,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
|
||||
"""Initialize the sleeping sensor."""
|
||||
self.sensors = sensors
|
||||
self.last_state: StateType = None
|
||||
self.last_unit: str | None = None
|
||||
self.coordinator = coordinator
|
||||
self.attribute = attribute
|
||||
self.block: Block | None = block # type: ignore[assignment]
|
||||
@ -644,6 +646,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
|
||||
|
||||
if last_state is not None:
|
||||
self.last_state = last_state.state
|
||||
self.last_unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
@callback
|
||||
def _update_callback(self) -> None:
|
||||
@ -696,6 +699,7 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity, RestoreEntity):
|
||||
) -> None:
|
||||
"""Initialize the sleeping sensor."""
|
||||
self.last_state: StateType = None
|
||||
self.last_unit: str | None = None
|
||||
self.coordinator = coordinator
|
||||
self.key = key
|
||||
self.attribute = attribute
|
||||
@ -725,3 +729,4 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity, RestoreEntity):
|
||||
|
||||
if last_state is not None:
|
||||
self.last_state = last_state.state
|
||||
self.last_unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
@ -47,12 +47,7 @@ from .entity import (
|
||||
async_setup_entry_rest,
|
||||
async_setup_entry_rpc,
|
||||
)
|
||||
from .utils import (
|
||||
get_device_entry_gen,
|
||||
get_device_uptime,
|
||||
is_rpc_device_externally_powered,
|
||||
temperature_unit,
|
||||
)
|
||||
from .utils import get_device_entry_gen, get_device_uptime
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -84,7 +79,7 @@ SENSORS: Final = {
|
||||
("device", "deviceTemp"): BlockSensorDescription(
|
||||
key="device|deviceTemp",
|
||||
name="Device Temperature",
|
||||
unit_fn=temperature_unit,
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
value=lambda value: round(value, 1),
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@ -145,7 +140,7 @@ SENSORS: Final = {
|
||||
key="emeter|powerFactor",
|
||||
name="Power Factor",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value=lambda value: abs(round(value * 100, 1)),
|
||||
value=lambda value: round(value * 100, 1),
|
||||
device_class=SensorDeviceClass.POWER_FACTOR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
@ -226,7 +221,7 @@ SENSORS: Final = {
|
||||
("sensor", "temp"): BlockSensorDescription(
|
||||
key="sensor|temp",
|
||||
name="Temperature",
|
||||
unit_fn=temperature_unit,
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
value=lambda value: round(value, 1),
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@ -235,7 +230,7 @@ SENSORS: Final = {
|
||||
("sensor", "extTemp"): BlockSensorDescription(
|
||||
key="sensor|extTemp",
|
||||
name="Temperature",
|
||||
unit_fn=temperature_unit,
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
value=lambda value: round(value, 1),
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@ -407,7 +402,6 @@ RPC_SENSORS: Final = {
|
||||
value=lambda status, _: status["percent"],
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
removal_condition=is_rpc_device_externally_powered,
|
||||
entity_registry_enabled_default=True,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
@ -505,8 +499,6 @@ class BlockSensor(ShellyBlockAttributeEntity, SensorEntity):
|
||||
super().__init__(coordinator, block, attribute, description)
|
||||
|
||||
self._attr_native_unit_of_measurement = description.native_unit_of_measurement
|
||||
if unit_fn := description.unit_fn:
|
||||
self._attr_native_unit_of_measurement = unit_fn(block.info(attribute))
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
@ -553,10 +545,6 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity):
|
||||
"""Initialize the sleeping sensor."""
|
||||
super().__init__(coordinator, block, attribute, description, entry, sensors)
|
||||
|
||||
self._attr_native_unit_of_measurement = description.native_unit_of_measurement
|
||||
if block and (unit_fn := description.unit_fn):
|
||||
self._attr_native_unit_of_measurement = unit_fn(block.info(attribute))
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return value of sensor."""
|
||||
@ -565,6 +553,14 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity):
|
||||
|
||||
return self.last_state
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement of the sensor, if any."""
|
||||
if self.block is not None:
|
||||
return self.entity_description.native_unit_of_measurement
|
||||
|
||||
return self.last_unit
|
||||
|
||||
|
||||
class RpcSleepingSensor(ShellySleepingRpcAttributeEntity, SensorEntity):
|
||||
"""Represent a RPC sleeping sensor."""
|
||||
@ -578,3 +574,11 @@ class RpcSleepingSensor(ShellySleepingRpcAttributeEntity, SensorEntity):
|
||||
return self.attribute_value
|
||||
|
||||
return self.last_state
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement of the sensor, if any."""
|
||||
if self.coordinator.device.initialized:
|
||||
return self.entity_description.native_unit_of_measurement
|
||||
|
||||
return self.last_unit
|
||||
|
@ -5,13 +5,13 @@ from datetime import datetime, timedelta
|
||||
from typing import Any, cast
|
||||
|
||||
from aiohttp.web import Request, WebSocketResponse
|
||||
from aioshelly.block_device import BLOCK_VALUE_UNIT, COAP, Block, BlockDevice
|
||||
from aioshelly.block_device import COAP, Block, BlockDevice
|
||||
from aioshelly.const import MODEL_NAMES
|
||||
from aioshelly.rpc_device import RpcDevice, WsServer
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry, entity_registry, singleton
|
||||
from homeassistant.helpers.typing import EventType
|
||||
@ -43,13 +43,6 @@ def async_remove_shelly_entity(
|
||||
entity_reg.async_remove(entity_id)
|
||||
|
||||
|
||||
def temperature_unit(block_info: dict[str, Any]) -> str:
|
||||
"""Detect temperature unit."""
|
||||
if block_info[BLOCK_VALUE_UNIT] == "F":
|
||||
return TEMP_FAHRENHEIT
|
||||
return TEMP_CELSIUS
|
||||
|
||||
|
||||
def get_block_device_name(device: BlockDevice) -> str:
|
||||
"""Naming for device."""
|
||||
return cast(str, device.settings["name"] or device.settings["device"]["hostname"])
|
||||
@ -364,13 +357,6 @@ def is_rpc_channel_type_light(config: dict[str, Any], channel: int) -> bool:
|
||||
return con_types is not None and con_types[channel].lower().startswith("light")
|
||||
|
||||
|
||||
def is_rpc_device_externally_powered(
|
||||
config: dict[str, Any], status: dict[str, Any], key: str
|
||||
) -> bool:
|
||||
"""Return true if device has external power instead of battery."""
|
||||
return cast(bool, status[key]["external"]["present"])
|
||||
|
||||
|
||||
def get_rpc_input_triggers(device: RpcDevice) -> list[tuple[str, str]]:
|
||||
"""Return list of input triggers for RPC device."""
|
||||
triggers = []
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Sonarr",
|
||||
"documentation": "https://www.home-assistant.io/integrations/sonarr",
|
||||
"codeowners": ["@ctalkington"],
|
||||
"requirements": ["aiopyarr==22.10.0"],
|
||||
"requirements": ["aiopyarr==22.11.0"],
|
||||
"config_flow": true,
|
||||
"quality_scale": "silver",
|
||||
"iot_class": "local_polling",
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "ssdp",
|
||||
"name": "Simple Service Discovery Protocol (SSDP)",
|
||||
"documentation": "https://www.home-assistant.io/integrations/ssdp",
|
||||
"requirements": ["async-upnp-client==0.32.1"],
|
||||
"requirements": ["async-upnp-client==0.32.2"],
|
||||
"dependencies": ["network"],
|
||||
"after_dependencies": ["zeroconf"],
|
||||
"codeowners": [],
|
||||
|
@ -53,6 +53,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
try:
|
||||
await tibber_connection.update_info()
|
||||
if not tibber_connection.name:
|
||||
raise ConfigEntryNotReady("Could not fetch Tibber data.")
|
||||
|
||||
except asyncio.TimeoutError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
except aiohttp.ClientError as err:
|
||||
|
@ -20,6 +20,7 @@ from .const import (
|
||||
CONFIG_ENTRY_ST,
|
||||
CONFIG_ENTRY_UDN,
|
||||
DOMAIN,
|
||||
DOMAIN_DISCOVERIES,
|
||||
LOGGER,
|
||||
ST_IGD_V1,
|
||||
ST_IGD_V2,
|
||||
@ -47,7 +48,7 @@ def _is_complete_discovery(discovery_info: ssdp.SsdpServiceInfo) -> bool:
|
||||
)
|
||||
|
||||
|
||||
async def _async_discover_igd_devices(
|
||||
async def _async_discovered_igd_devices(
|
||||
hass: HomeAssistant,
|
||||
) -> list[ssdp.SsdpServiceInfo]:
|
||||
"""Discovery IGD devices."""
|
||||
@ -79,9 +80,19 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
# - ssdp(discovery_info) --> ssdp_confirm(None) --> ssdp_confirm({}) --> create_entry()
|
||||
# - user(None): scan --> user({...}) --> create_entry()
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the UPnP/IGD config flow."""
|
||||
self._discoveries: list[SsdpServiceInfo] | None = None
|
||||
@property
|
||||
def _discoveries(self) -> dict[str, SsdpServiceInfo]:
|
||||
"""Get current discoveries."""
|
||||
domain_data: dict = self.hass.data.setdefault(DOMAIN, {})
|
||||
return domain_data.setdefault(DOMAIN_DISCOVERIES, {})
|
||||
|
||||
def _add_discovery(self, discovery: SsdpServiceInfo) -> None:
|
||||
"""Add a discovery."""
|
||||
self._discoveries[discovery.ssdp_usn] = discovery
|
||||
|
||||
def _remove_discovery(self, usn: str) -> SsdpServiceInfo:
|
||||
"""Remove a discovery by its USN/unique_id."""
|
||||
return self._discoveries.pop(usn)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: Mapping[str, Any] | None = None
|
||||
@ -95,7 +106,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
discovery = next(
|
||||
iter(
|
||||
discovery
|
||||
for discovery in self._discoveries
|
||||
for discovery in self._discoveries.values()
|
||||
if discovery.ssdp_usn == user_input["unique_id"]
|
||||
)
|
||||
)
|
||||
@ -103,21 +114,19 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return await self._async_create_entry_from_discovery(discovery)
|
||||
|
||||
# Discover devices.
|
||||
discoveries = await _async_discover_igd_devices(self.hass)
|
||||
discoveries = await _async_discovered_igd_devices(self.hass)
|
||||
|
||||
# Store discoveries which have not been configured.
|
||||
current_unique_ids = {
|
||||
entry.unique_id for entry in self._async_current_entries()
|
||||
}
|
||||
self._discoveries = [
|
||||
discovery
|
||||
for discovery in discoveries
|
||||
for discovery in discoveries:
|
||||
if (
|
||||
_is_complete_discovery(discovery)
|
||||
and _is_igd_device(discovery)
|
||||
and discovery.ssdp_usn not in current_unique_ids
|
||||
)
|
||||
]
|
||||
):
|
||||
self._add_discovery(discovery)
|
||||
|
||||
# Ensure anything to add.
|
||||
if not self._discoveries:
|
||||
@ -128,7 +137,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
vol.Required("unique_id"): vol.In(
|
||||
{
|
||||
discovery.ssdp_usn: _friendly_name_from_discovery(discovery)
|
||||
for discovery in self._discoveries
|
||||
for discovery in self._discoveries.values()
|
||||
}
|
||||
),
|
||||
}
|
||||
@ -163,12 +172,13 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
mac_address = await _async_mac_address_from_discovery(self.hass, discovery_info)
|
||||
host = discovery_info.ssdp_headers["_host"]
|
||||
self._abort_if_unique_id_configured(
|
||||
# Store mac address for older entries.
|
||||
# Store mac address and other data for older entries.
|
||||
# The location is stored in the config entry such that when the location changes, the entry is reloaded.
|
||||
updates={
|
||||
CONFIG_ENTRY_MAC_ADDRESS: mac_address,
|
||||
CONFIG_ENTRY_LOCATION: discovery_info.ssdp_location,
|
||||
CONFIG_ENTRY_HOST: host,
|
||||
CONFIG_ENTRY_ST: discovery_info.ssdp_st,
|
||||
},
|
||||
)
|
||||
|
||||
@ -204,7 +214,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="config_entry_updated")
|
||||
|
||||
# Store discovery.
|
||||
self._discoveries = [discovery_info]
|
||||
self._add_discovery(discovery_info)
|
||||
|
||||
# Ensure user recognizable.
|
||||
self.context["title_placeholders"] = {
|
||||
@ -221,10 +231,27 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="ssdp_confirm")
|
||||
|
||||
assert self._discoveries
|
||||
discovery = self._discoveries[0]
|
||||
assert self.unique_id
|
||||
discovery = self._remove_discovery(self.unique_id)
|
||||
return await self._async_create_entry_from_discovery(discovery)
|
||||
|
||||
async def async_step_ignore(self, user_input: dict[str, Any]) -> FlowResult:
|
||||
"""Ignore this config flow."""
|
||||
usn = user_input["unique_id"]
|
||||
discovery = self._remove_discovery(usn)
|
||||
mac_address = await _async_mac_address_from_discovery(self.hass, discovery)
|
||||
data = {
|
||||
CONFIG_ENTRY_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN],
|
||||
CONFIG_ENTRY_ST: discovery.ssdp_st,
|
||||
CONFIG_ENTRY_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN],
|
||||
CONFIG_ENTRY_MAC_ADDRESS: mac_address,
|
||||
CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"],
|
||||
CONFIG_ENTRY_LOCATION: discovery.ssdp_location,
|
||||
}
|
||||
|
||||
await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False)
|
||||
return self.async_create_entry(title=user_input["title"], data=data)
|
||||
|
||||
async def _async_create_entry_from_discovery(
|
||||
self,
|
||||
discovery: SsdpServiceInfo,
|
||||
@ -243,5 +270,6 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
CONFIG_ENTRY_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN],
|
||||
CONFIG_ENTRY_LOCATION: discovery.ssdp_location,
|
||||
CONFIG_ENTRY_MAC_ADDRESS: mac_address,
|
||||
CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"],
|
||||
}
|
||||
return self.async_create_entry(title=title, data=data)
|
||||
|
@ -7,6 +7,7 @@ from homeassistant.const import TIME_SECONDS
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "upnp"
|
||||
DOMAIN_DISCOVERIES = "discoveries"
|
||||
BYTES_RECEIVED = "bytes_received"
|
||||
BYTES_SENT = "bytes_sent"
|
||||
PACKETS_RECEIVED = "packets_received"
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "UPnP/IGD",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/upnp",
|
||||
"requirements": ["async-upnp-client==0.32.1", "getmac==0.8.2"],
|
||||
"requirements": ["async-upnp-client==0.32.2", "getmac==0.8.2"],
|
||||
"dependencies": ["network", "ssdp"],
|
||||
"codeowners": ["@StevenLooman"],
|
||||
"ssdp": [
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Venstar",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/venstar",
|
||||
"requirements": ["venstarcolortouch==0.18"],
|
||||
"requirements": ["venstarcolortouch==0.19"],
|
||||
"codeowners": ["@garbled1"],
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["venstarcolortouch"]
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Xiaomi Gateway (Aqara)",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara",
|
||||
"requirements": ["PyXiaomiGateway==0.14.1"],
|
||||
"requirements": ["PyXiaomiGateway==0.14.3"],
|
||||
"after_dependencies": ["discovery"],
|
||||
"codeowners": ["@danielhiversen", "@syssi"],
|
||||
"zeroconf": ["_miio._udp.local."],
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "yeelight",
|
||||
"name": "Yeelight",
|
||||
"documentation": "https://www.home-assistant.io/integrations/yeelight",
|
||||
"requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.1"],
|
||||
"requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.2"],
|
||||
"codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["network"],
|
||||
|
@ -552,12 +552,20 @@ def _first_non_link_local_address(
|
||||
"""Return the first ipv6 or non-link local ipv4 address, preferring IPv4."""
|
||||
for address in addresses:
|
||||
ip_addr = ip_address(address)
|
||||
if not ip_addr.is_link_local and ip_addr.version == 4:
|
||||
if (
|
||||
not ip_addr.is_link_local
|
||||
and not ip_addr.is_unspecified
|
||||
and ip_addr.version == 4
|
||||
):
|
||||
return str(ip_addr)
|
||||
# If we didn't find a good IPv4 address, check for IPv6 addresses.
|
||||
for address in addresses:
|
||||
ip_addr = ip_address(address)
|
||||
if not ip_addr.is_link_local and ip_addr.version == 6:
|
||||
if (
|
||||
not ip_addr.is_link_local
|
||||
and not ip_addr.is_unspecified
|
||||
and ip_addr.version == 6
|
||||
):
|
||||
return str(ip_addr)
|
||||
return None
|
||||
|
||||
|
@ -98,12 +98,26 @@ class ColorChannel(ZigbeeChannel):
|
||||
@property
|
||||
def min_mireds(self) -> int:
|
||||
"""Return the coldest color_temp that this channel supports."""
|
||||
return self.cluster.get("color_temp_physical_min", self.MIN_MIREDS)
|
||||
min_mireds = self.cluster.get("color_temp_physical_min", self.MIN_MIREDS)
|
||||
if min_mireds == 0:
|
||||
self.warning(
|
||||
"[Min mireds is 0, setting to %s] Please open an issue on the quirks repo to have this device corrected",
|
||||
self.MIN_MIREDS,
|
||||
)
|
||||
min_mireds = self.MIN_MIREDS
|
||||
return min_mireds
|
||||
|
||||
@property
|
||||
def max_mireds(self) -> int:
|
||||
"""Return the warmest color_temp that this channel supports."""
|
||||
return self.cluster.get("color_temp_physical_max", self.MAX_MIREDS)
|
||||
max_mireds = self.cluster.get("color_temp_physical_max", self.MAX_MIREDS)
|
||||
if max_mireds == 0:
|
||||
self.warning(
|
||||
"[Max mireds is 0, setting to %s] Please open an issue on the quirks repo to have this device corrected",
|
||||
self.MAX_MIREDS,
|
||||
)
|
||||
max_mireds = self.MAX_MIREDS
|
||||
return max_mireds
|
||||
|
||||
@property
|
||||
def hs_supported(self) -> bool:
|
||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from zigpy import types
|
||||
from zhaquirks.inovelli.types import AllLEDEffectType, SingleLEDEffectType
|
||||
from zigpy.exceptions import ZigbeeException
|
||||
import zigpy.zcl
|
||||
|
||||
@ -183,59 +183,47 @@ class InovelliNotificationChannel(ClientChannel):
|
||||
class InovelliConfigEntityChannel(ZigbeeChannel):
|
||||
"""Inovelli Configuration Entity channel."""
|
||||
|
||||
class LEDEffectType(types.enum8):
|
||||
"""Effect type for Inovelli Blue Series switch."""
|
||||
|
||||
Off = 0x00
|
||||
Solid = 0x01
|
||||
Fast_Blink = 0x02
|
||||
Slow_Blink = 0x03
|
||||
Pulse = 0x04
|
||||
Chase = 0x05
|
||||
Open_Close = 0x06
|
||||
Small_To_Big = 0x07
|
||||
Clear = 0xFF
|
||||
|
||||
REPORT_CONFIG = ()
|
||||
ZCL_INIT_ATTRS = {
|
||||
"dimming_speed_up_remote": False,
|
||||
"dimming_speed_up_local": False,
|
||||
"ramp_rate_off_to_on_local": False,
|
||||
"ramp_rate_off_to_on_remote": False,
|
||||
"dimming_speed_down_remote": False,
|
||||
"dimming_speed_down_local": False,
|
||||
"ramp_rate_on_to_off_local": False,
|
||||
"ramp_rate_on_to_off_remote": False,
|
||||
"minimum_level": False,
|
||||
"maximum_level": False,
|
||||
"invert_switch": False,
|
||||
"auto_off_timer": False,
|
||||
"default_level_local": False,
|
||||
"default_level_remote": False,
|
||||
"state_after_power_restored": False,
|
||||
"load_level_indicator_timeout": False,
|
||||
"active_power_reports": False,
|
||||
"periodic_power_and_energy_reports": False,
|
||||
"active_energy_reports": False,
|
||||
"dimming_speed_up_remote": True,
|
||||
"dimming_speed_up_local": True,
|
||||
"ramp_rate_off_to_on_local": True,
|
||||
"ramp_rate_off_to_on_remote": True,
|
||||
"dimming_speed_down_remote": True,
|
||||
"dimming_speed_down_local": True,
|
||||
"ramp_rate_on_to_off_local": True,
|
||||
"ramp_rate_on_to_off_remote": True,
|
||||
"minimum_level": True,
|
||||
"maximum_level": True,
|
||||
"invert_switch": True,
|
||||
"auto_off_timer": True,
|
||||
"default_level_local": True,
|
||||
"default_level_remote": True,
|
||||
"state_after_power_restored": True,
|
||||
"load_level_indicator_timeout": True,
|
||||
"active_power_reports": True,
|
||||
"periodic_power_and_energy_reports": True,
|
||||
"active_energy_reports": True,
|
||||
"power_type": False,
|
||||
"switch_type": False,
|
||||
"button_delay": False,
|
||||
"smart_bulb_mode": False,
|
||||
"double_tap_up_for_full_brightness": False,
|
||||
"led_color_when_on": False,
|
||||
"led_color_when_off": False,
|
||||
"led_intensity_when_on": False,
|
||||
"led_intensity_when_off": False,
|
||||
"double_tap_up_for_full_brightness": True,
|
||||
"led_color_when_on": True,
|
||||
"led_color_when_off": True,
|
||||
"led_intensity_when_on": True,
|
||||
"led_intensity_when_off": True,
|
||||
"local_protection": False,
|
||||
"output_mode": False,
|
||||
"on_off_led_mode": False,
|
||||
"firmware_progress_led": False,
|
||||
"relay_click_in_on_off_mode": False,
|
||||
"on_off_led_mode": True,
|
||||
"firmware_progress_led": True,
|
||||
"relay_click_in_on_off_mode": True,
|
||||
"disable_clear_notifications_double_tap": True,
|
||||
}
|
||||
|
||||
async def issue_all_led_effect(
|
||||
self,
|
||||
effect_type: LEDEffectType | int = LEDEffectType.Fast_Blink,
|
||||
effect_type: AllLEDEffectType | int = AllLEDEffectType.Fast_Blink,
|
||||
color: int = 200,
|
||||
level: int = 100,
|
||||
duration: int = 3,
|
||||
@ -251,7 +239,7 @@ class InovelliConfigEntityChannel(ZigbeeChannel):
|
||||
async def issue_individual_led_effect(
|
||||
self,
|
||||
led_number: int = 1,
|
||||
effect_type: LEDEffectType | int = LEDEffectType.Fast_Blink,
|
||||
effect_type: SingleLEDEffectType | int = SingleLEDEffectType.Fast_Blink,
|
||||
color: int = 200,
|
||||
level: int = 100,
|
||||
duration: int = 3,
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||
|
||||
from . import DOMAIN
|
||||
from .api import SERVICE_WARNING_DEVICE_SQUAWK, SERVICE_WARNING_DEVICE_WARN
|
||||
from .core.channels.manufacturerspecific import InovelliConfigEntityChannel
|
||||
from .core.channels.manufacturerspecific import AllLEDEffectType, SingleLEDEffectType
|
||||
from .core.const import CHANNEL_IAS_WD, CHANNEL_INOVELLI
|
||||
from .core.helpers import async_get_zha_device
|
||||
|
||||
@ -40,9 +40,7 @@ INOVELLI_ALL_LED_EFFECT_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): INOVELLI_ALL_LED_EFFECT,
|
||||
vol.Required(CONF_DOMAIN): DOMAIN,
|
||||
vol.Required(
|
||||
"effect_type"
|
||||
): InovelliConfigEntityChannel.LEDEffectType.__getitem__,
|
||||
vol.Required("effect_type"): AllLEDEffectType.__getitem__,
|
||||
vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)),
|
||||
vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
|
||||
vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)),
|
||||
@ -52,10 +50,16 @@ INOVELLI_ALL_LED_EFFECT_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
|
||||
INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA = INOVELLI_ALL_LED_EFFECT_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): INOVELLI_INDIVIDUAL_LED_EFFECT,
|
||||
vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(1, 7)),
|
||||
vol.Required("effect_type"): SingleLEDEffectType.__getitem__,
|
||||
vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)),
|
||||
}
|
||||
)
|
||||
|
||||
ACTION_SCHEMA_MAP = {
|
||||
INOVELLI_ALL_LED_EFFECT: INOVELLI_ALL_LED_EFFECT_SCHEMA,
|
||||
INOVELLI_INDIVIDUAL_LED_EFFECT: INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA,
|
||||
}
|
||||
|
||||
ACTION_SCHEMA = vol.Any(
|
||||
INOVELLI_ALL_LED_EFFECT_SCHEMA,
|
||||
INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA,
|
||||
@ -83,9 +87,7 @@ DEVICE_ACTION_TYPES = {
|
||||
DEVICE_ACTION_SCHEMAS = {
|
||||
INOVELLI_ALL_LED_EFFECT: vol.Schema(
|
||||
{
|
||||
vol.Required("effect_type"): vol.In(
|
||||
InovelliConfigEntityChannel.LEDEffectType.__members__.keys()
|
||||
),
|
||||
vol.Required("effect_type"): vol.In(AllLEDEffectType.__members__.keys()),
|
||||
vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)),
|
||||
vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
|
||||
vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)),
|
||||
@ -94,9 +96,7 @@ DEVICE_ACTION_SCHEMAS = {
|
||||
INOVELLI_INDIVIDUAL_LED_EFFECT: vol.Schema(
|
||||
{
|
||||
vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)),
|
||||
vol.Required("effect_type"): vol.In(
|
||||
InovelliConfigEntityChannel.LEDEffectType.__members__.keys()
|
||||
),
|
||||
vol.Required("effect_type"): vol.In(SingleLEDEffectType.__members__.keys()),
|
||||
vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)),
|
||||
vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
|
||||
vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)),
|
||||
@ -127,6 +127,15 @@ async def async_call_action_from_config(
|
||||
)
|
||||
|
||||
|
||||
async def async_validate_action_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
schema = ACTION_SCHEMA_MAP.get(config[CONF_TYPE], DEFAULT_ACTION_SCHEMA)
|
||||
config = schema(config)
|
||||
return config
|
||||
|
||||
|
||||
async def async_get_actions(
|
||||
hass: HomeAssistant, device_id: str
|
||||
) -> list[dict[str, str]]:
|
||||
|
@ -7,7 +7,7 @@
|
||||
"bellows==0.34.2",
|
||||
"pyserial==3.5",
|
||||
"pyserial-asyncio==0.6",
|
||||
"zha-quirks==0.0.84",
|
||||
"zha-quirks==0.0.85",
|
||||
"zigpy-deconz==0.19.0",
|
||||
"zigpy==0.51.5",
|
||||
"zigpy-xbee==0.16.2",
|
||||
|
@ -418,3 +418,15 @@ class InovelliRelayClickInOnOffMode(
|
||||
|
||||
_zcl_attribute: str = "relay_click_in_on_off_mode"
|
||||
_attr_name: str = "Disable relay click in on off mode"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_INOVELLI,
|
||||
)
|
||||
class InovelliDisableDoubleTapClearNotificationsMode(
|
||||
ZHASwitchConfigurationEntity, id_suffix="disable_clear_notifications_double_tap"
|
||||
):
|
||||
"""Inovelli disable clear notifications double tap control."""
|
||||
|
||||
_zcl_attribute: str = "disable_clear_notifications_double_tap"
|
||||
_attr_name: str = "Disable config 2x tap to clear notifications"
|
||||
|
@ -660,24 +660,25 @@ class ConfigEntry:
|
||||
data: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Start a reauth flow."""
|
||||
flow_context = {
|
||||
"source": SOURCE_REAUTH,
|
||||
"entry_id": self.entry_id,
|
||||
"title_placeholders": {"name": self.title},
|
||||
"unique_id": self.unique_id,
|
||||
}
|
||||
|
||||
if context:
|
||||
flow_context.update(context)
|
||||
|
||||
for flow in hass.config_entries.flow.async_progress_by_handler(self.domain):
|
||||
if flow["context"] == flow_context:
|
||||
return
|
||||
if any(
|
||||
flow
|
||||
for flow in hass.config_entries.flow.async_progress_by_handler(self.domain)
|
||||
if flow["context"].get("source") == SOURCE_REAUTH
|
||||
and flow["context"].get("entry_id") == self.entry_id
|
||||
):
|
||||
# Reauth flow already in progress for this entry
|
||||
return
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
self.domain,
|
||||
context=flow_context,
|
||||
context={
|
||||
"source": SOURCE_REAUTH,
|
||||
"entry_id": self.entry_id,
|
||||
"title_placeholders": {"name": self.title},
|
||||
"unique_id": self.unique_id,
|
||||
}
|
||||
| (context or {}),
|
||||
data=self.data | (data or {}),
|
||||
)
|
||||
)
|
||||
|
@ -8,7 +8,7 @@ from .backports.enum import StrEnum
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2022
|
||||
MINOR_VERSION: Final = 11
|
||||
PATCH_VERSION: Final = "1"
|
||||
PATCH_VERSION: Final = "2"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)
|
||||
|
@ -4,15 +4,15 @@ aiodiscover==1.4.13
|
||||
aiohttp==3.8.1
|
||||
aiohttp_cors==0.7.0
|
||||
astral==2.2
|
||||
async-upnp-client==0.32.1
|
||||
async-upnp-client==0.32.2
|
||||
async_timeout==4.0.2
|
||||
atomicwrites-homeassistant==1.4.1
|
||||
attrs==21.2.0
|
||||
awesomeversion==22.9.0
|
||||
bcrypt==3.1.7
|
||||
bleak-retry-connector==2.8.2
|
||||
bleak==0.19.1
|
||||
bluetooth-adapters==0.6.0
|
||||
bleak-retry-connector==2.8.3
|
||||
bleak==0.19.2
|
||||
bluetooth-adapters==0.7.0
|
||||
bluetooth-auto-recovery==0.3.6
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.2.0
|
||||
@ -21,7 +21,7 @@ dbus-fast==1.61.1
|
||||
fnvhash==0.1.0
|
||||
hass-nabucasa==0.56.0
|
||||
home-assistant-bluetooth==1.6.0
|
||||
home-assistant-frontend==20221102.1
|
||||
home-assistant-frontend==20221108.0
|
||||
httpx==0.23.0
|
||||
ifaddr==0.1.7
|
||||
jinja2==3.1.2
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2022.11.1"
|
||||
version = "2022.11.2"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
@ -50,7 +50,7 @@ PyTurboJPEG==1.6.7
|
||||
PyViCare==2.17.0
|
||||
|
||||
# homeassistant.components.xiaomi_aqara
|
||||
PyXiaomiGateway==0.14.1
|
||||
PyXiaomiGateway==0.14.3
|
||||
|
||||
# homeassistant.components.remember_the_milk
|
||||
RtmAPI==0.7.2
|
||||
@ -153,7 +153,7 @@ aioecowitt==2022.09.3
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==11.4.2
|
||||
aioesphomeapi==11.4.3
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
@ -171,7 +171,7 @@ aioguardian==2022.07.0
|
||||
aioharmony==0.2.9
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==2.2.14
|
||||
aiohomekit==2.2.18
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@ -237,7 +237,7 @@ aiopvpc==3.0.0
|
||||
# homeassistant.components.lidarr
|
||||
# homeassistant.components.radarr
|
||||
# homeassistant.components.sonarr
|
||||
aiopyarr==22.10.0
|
||||
aiopyarr==22.11.0
|
||||
|
||||
# homeassistant.components.qnap_qsw
|
||||
aioqsw==0.2.2
|
||||
@ -353,7 +353,7 @@ asterisk_mbox==0.5.0
|
||||
# homeassistant.components.ssdp
|
||||
# homeassistant.components.upnp
|
||||
# homeassistant.components.yeelight
|
||||
async-upnp-client==0.32.1
|
||||
async-upnp-client==0.32.2
|
||||
|
||||
# homeassistant.components.supla
|
||||
asyncpysupla==0.0.5
|
||||
@ -413,10 +413,10 @@ bimmer_connected==0.10.4
|
||||
bizkaibus==0.1.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak-retry-connector==2.8.2
|
||||
bleak-retry-connector==2.8.3
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak==0.19.1
|
||||
bleak==0.19.2
|
||||
|
||||
# homeassistant.components.blebox
|
||||
blebox_uniapi==2.1.3
|
||||
@ -438,7 +438,7 @@ bluemaestro-ble==0.2.0
|
||||
# bluepy==1.3.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-adapters==0.6.0
|
||||
bluetooth-adapters==0.7.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-auto-recovery==0.3.6
|
||||
@ -725,7 +725,7 @@ gTTS==2.2.4
|
||||
garages-amsterdam==3.0.0
|
||||
|
||||
# homeassistant.components.google
|
||||
gcal-sync==2.2.3
|
||||
gcal-sync==4.0.0
|
||||
|
||||
# homeassistant.components.geniushub
|
||||
geniushub-client==0.6.30
|
||||
@ -815,6 +815,9 @@ gstreamer-player==1.1.2
|
||||
# homeassistant.components.profiler
|
||||
guppy3==3.1.2
|
||||
|
||||
# homeassistant.components.iaqualink
|
||||
h2==4.1.0
|
||||
|
||||
# homeassistant.components.homekit
|
||||
ha-HAP-python==4.5.2
|
||||
|
||||
@ -868,7 +871,7 @@ hole==0.7.0
|
||||
holidays==0.16
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20221102.1
|
||||
home-assistant-frontend==20221108.0
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@ -1135,7 +1138,7 @@ nettigo-air-monitor==1.4.2
|
||||
neurio==0.3.1
|
||||
|
||||
# homeassistant.components.nexia
|
||||
nexia==2.0.5
|
||||
nexia==2.0.6
|
||||
|
||||
# homeassistant.components.nextcloud
|
||||
nextcloudmonitor==1.1.0
|
||||
@ -1238,7 +1241,7 @@ openwrt-luci-rpc==1.1.11
|
||||
openwrt-ubus-rpc==0.0.2
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.10.0
|
||||
oralb-ble==0.13.0
|
||||
|
||||
# homeassistant.components.oru
|
||||
oru==0.1.11
|
||||
@ -1250,7 +1253,7 @@ orvibo==1.1.1
|
||||
ovoenergy==1.2.0
|
||||
|
||||
# homeassistant.components.p1_monitor
|
||||
p1monitor==2.1.0
|
||||
p1monitor==2.1.1
|
||||
|
||||
# homeassistant.components.mqtt
|
||||
# homeassistant.components.shiftr
|
||||
@ -1312,7 +1315,7 @@ plexauth==0.0.6
|
||||
plexwebsocket==0.0.13
|
||||
|
||||
# homeassistant.components.plugwise
|
||||
plugwise==0.25.3
|
||||
plugwise==0.25.7
|
||||
|
||||
# homeassistant.components.plum_lightpad
|
||||
plumlightpad==0.0.11
|
||||
@ -1433,7 +1436,7 @@ pyaftership==21.11.0
|
||||
pyairnow==1.1.0
|
||||
|
||||
# homeassistant.components.airvisual
|
||||
pyairvisual==2022.07.0
|
||||
pyairvisual==2022.11.1
|
||||
|
||||
# homeassistant.components.almond
|
||||
pyalmond==0.0.2
|
||||
@ -1442,7 +1445,7 @@ pyalmond==0.0.2
|
||||
pyatag==0.3.5.3
|
||||
|
||||
# homeassistant.components.netatmo
|
||||
pyatmo==7.3.0
|
||||
pyatmo==7.4.0
|
||||
|
||||
# homeassistant.components.atome
|
||||
pyatome==0.1.1
|
||||
@ -1688,7 +1691,7 @@ pylibrespot-java==0.1.1
|
||||
pylitejet==0.3.0
|
||||
|
||||
# homeassistant.components.litterrobot
|
||||
pylitterbot==2022.10.2
|
||||
pylitterbot==2022.11.0
|
||||
|
||||
# homeassistant.components.lutron_caseta
|
||||
pylutron-caseta==0.17.1
|
||||
@ -2487,7 +2490,7 @@ vehicle==0.4.0
|
||||
velbus-aio==2022.10.4
|
||||
|
||||
# homeassistant.components.venstar
|
||||
venstarcolortouch==0.18
|
||||
venstarcolortouch==0.19
|
||||
|
||||
# homeassistant.components.vilfo
|
||||
vilfo-api-client==0.3.2
|
||||
@ -2607,7 +2610,7 @@ zengge==0.2
|
||||
zeroconf==0.39.4
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.84
|
||||
zha-quirks==0.0.85
|
||||
|
||||
# homeassistant.components.zhong_hong
|
||||
zhong_hong_hvac==1.0.9
|
||||
|
@ -46,7 +46,7 @@ PyTurboJPEG==1.6.7
|
||||
PyViCare==2.17.0
|
||||
|
||||
# homeassistant.components.xiaomi_aqara
|
||||
PyXiaomiGateway==0.14.1
|
||||
PyXiaomiGateway==0.14.3
|
||||
|
||||
# homeassistant.components.remember_the_milk
|
||||
RtmAPI==0.7.2
|
||||
@ -140,7 +140,7 @@ aioecowitt==2022.09.3
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==11.4.2
|
||||
aioesphomeapi==11.4.3
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
@ -155,7 +155,7 @@ aioguardian==2022.07.0
|
||||
aioharmony==0.2.9
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==2.2.14
|
||||
aiohomekit==2.2.18
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@ -212,7 +212,7 @@ aiopvpc==3.0.0
|
||||
# homeassistant.components.lidarr
|
||||
# homeassistant.components.radarr
|
||||
# homeassistant.components.sonarr
|
||||
aiopyarr==22.10.0
|
||||
aiopyarr==22.11.0
|
||||
|
||||
# homeassistant.components.qnap_qsw
|
||||
aioqsw==0.2.2
|
||||
@ -307,7 +307,7 @@ arcam-fmj==0.12.0
|
||||
# homeassistant.components.ssdp
|
||||
# homeassistant.components.upnp
|
||||
# homeassistant.components.yeelight
|
||||
async-upnp-client==0.32.1
|
||||
async-upnp-client==0.32.2
|
||||
|
||||
# homeassistant.components.sleepiq
|
||||
asyncsleepiq==1.2.3
|
||||
@ -337,10 +337,10 @@ bellows==0.34.2
|
||||
bimmer_connected==0.10.4
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak-retry-connector==2.8.2
|
||||
bleak-retry-connector==2.8.3
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak==0.19.1
|
||||
bleak==0.19.2
|
||||
|
||||
# homeassistant.components.blebox
|
||||
blebox_uniapi==2.1.3
|
||||
@ -352,7 +352,7 @@ blinkpy==0.19.2
|
||||
bluemaestro-ble==0.2.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-adapters==0.6.0
|
||||
bluetooth-adapters==0.7.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-auto-recovery==0.3.6
|
||||
@ -541,7 +541,7 @@ gTTS==2.2.4
|
||||
garages-amsterdam==3.0.0
|
||||
|
||||
# homeassistant.components.google
|
||||
gcal-sync==2.2.3
|
||||
gcal-sync==4.0.0
|
||||
|
||||
# homeassistant.components.geocaching
|
||||
geocachingapi==0.2.1
|
||||
@ -607,6 +607,9 @@ gspread==5.5.0
|
||||
# homeassistant.components.profiler
|
||||
guppy3==3.1.2
|
||||
|
||||
# homeassistant.components.iaqualink
|
||||
h2==4.1.0
|
||||
|
||||
# homeassistant.components.homekit
|
||||
ha-HAP-python==4.5.2
|
||||
|
||||
@ -648,7 +651,7 @@ hole==0.7.0
|
||||
holidays==0.16
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20221102.1
|
||||
home-assistant-frontend==20221108.0
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@ -825,7 +828,7 @@ netmap==0.7.0.2
|
||||
nettigo-air-monitor==1.4.2
|
||||
|
||||
# homeassistant.components.nexia
|
||||
nexia==2.0.5
|
||||
nexia==2.0.6
|
||||
|
||||
# homeassistant.components.discord
|
||||
nextcord==2.0.0a8
|
||||
@ -883,13 +886,13 @@ open-meteo==0.2.1
|
||||
openerz-api==0.1.0
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.10.0
|
||||
oralb-ble==0.13.0
|
||||
|
||||
# homeassistant.components.ovo_energy
|
||||
ovoenergy==1.2.0
|
||||
|
||||
# homeassistant.components.p1_monitor
|
||||
p1monitor==2.1.0
|
||||
p1monitor==2.1.1
|
||||
|
||||
# homeassistant.components.mqtt
|
||||
# homeassistant.components.shiftr
|
||||
@ -939,7 +942,7 @@ plexauth==0.0.6
|
||||
plexwebsocket==0.0.13
|
||||
|
||||
# homeassistant.components.plugwise
|
||||
plugwise==0.25.3
|
||||
plugwise==0.25.7
|
||||
|
||||
# homeassistant.components.plum_lightpad
|
||||
plumlightpad==0.0.11
|
||||
@ -1021,7 +1024,7 @@ pyaehw4a1==0.3.9
|
||||
pyairnow==1.1.0
|
||||
|
||||
# homeassistant.components.airvisual
|
||||
pyairvisual==2022.07.0
|
||||
pyairvisual==2022.11.1
|
||||
|
||||
# homeassistant.components.almond
|
||||
pyalmond==0.0.2
|
||||
@ -1030,7 +1033,7 @@ pyalmond==0.0.2
|
||||
pyatag==0.3.5.3
|
||||
|
||||
# homeassistant.components.netatmo
|
||||
pyatmo==7.3.0
|
||||
pyatmo==7.4.0
|
||||
|
||||
# homeassistant.components.apple_tv
|
||||
pyatv==0.10.3
|
||||
@ -1189,7 +1192,7 @@ pylibrespot-java==0.1.1
|
||||
pylitejet==0.3.0
|
||||
|
||||
# homeassistant.components.litterrobot
|
||||
pylitterbot==2022.10.2
|
||||
pylitterbot==2022.11.0
|
||||
|
||||
# homeassistant.components.lutron_caseta
|
||||
pylutron-caseta==0.17.1
|
||||
@ -1721,7 +1724,7 @@ vehicle==0.4.0
|
||||
velbus-aio==2022.10.4
|
||||
|
||||
# homeassistant.components.venstar
|
||||
venstarcolortouch==0.18
|
||||
venstarcolortouch==0.19
|
||||
|
||||
# homeassistant.components.vilfo
|
||||
vilfo-api-client==0.3.2
|
||||
@ -1808,7 +1811,7 @@ zamg==0.1.1
|
||||
zeroconf==0.39.4
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.84
|
||||
zha-quirks==0.0.85
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-deconz==0.19.0
|
||||
|
@ -1,14 +1,14 @@
|
||||
"""Define tests for the AirVisual config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from pyairvisual.errors import (
|
||||
AirVisualError,
|
||||
from pyairvisual.cloud_api import (
|
||||
InvalidKeyError,
|
||||
KeyExpiredError,
|
||||
NodeProError,
|
||||
NotFoundError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
from pyairvisual.errors import AirVisualError
|
||||
from pyairvisual.node import NodeProError
|
||||
import pytest
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
|
@ -2,6 +2,7 @@
|
||||
from dataclasses import asdict
|
||||
from unittest.mock import patch
|
||||
|
||||
from elkm1_lib.discovery import ElkSystem
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
@ -1317,3 +1318,285 @@ async def test_discovered_by_dhcp_no_udp_response(hass):
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_multiple_instances_with_discovery(hass):
|
||||
"""Test we can setup a secure elk."""
|
||||
|
||||
elk_discovery_1 = ElkSystem("aa:bb:cc:dd:ee:ff", "127.0.0.1", 2601)
|
||||
elk_discovery_2 = ElkSystem("aa:bb:cc:dd:ee:fe", "127.0.0.2", 2601)
|
||||
|
||||
with _patch_discovery(device=elk_discovery_1):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert not result["errors"]
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
mocked_elk = mock_elk(invalid_auth=False, sync_complete=True)
|
||||
|
||||
with _patch_elk(elk=mocked_elk):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"device": elk_discovery_1.mac_address},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with _patch_discovery(device=elk_discovery_1), _patch_elk(elk=mocked_elk), patch(
|
||||
"homeassistant.components.elkm1.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.elkm1.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == "create_entry"
|
||||
assert result3["title"] == "ElkM1 ddeeff"
|
||||
assert result3["data"] == {
|
||||
"auto_configure": True,
|
||||
"host": "elks://127.0.0.1",
|
||||
"password": "test-password",
|
||||
"prefix": "",
|
||||
"username": "test-username",
|
||||
}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
# Now try to add another instance with the different discovery info
|
||||
with _patch_discovery(device=elk_discovery_2):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert not result["errors"]
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
mocked_elk = mock_elk(invalid_auth=False, sync_complete=True)
|
||||
|
||||
with _patch_elk(elk=mocked_elk):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"device": elk_discovery_2.mac_address},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with _patch_discovery(device=elk_discovery_2), _patch_elk(elk=mocked_elk), patch(
|
||||
"homeassistant.components.elkm1.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == "create_entry"
|
||||
assert result3["title"] == "ElkM1 ddeefe"
|
||||
assert result3["data"] == {
|
||||
"auto_configure": True,
|
||||
"host": "elks://127.0.0.2",
|
||||
"password": "test-password",
|
||||
"prefix": "ddeefe",
|
||||
"username": "test-username",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
# Finally, try to add another instance manually with no discovery info
|
||||
|
||||
with _patch_discovery(no_device=True):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {}
|
||||
assert result["step_id"] == "manual_connection"
|
||||
|
||||
mocked_elk = mock_elk(invalid_auth=None, sync_complete=True)
|
||||
|
||||
with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch(
|
||||
"homeassistant.components.elkm1.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"protocol": "non-secure",
|
||||
"address": "1.2.3.4",
|
||||
"prefix": "guest_house",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "guest_house"
|
||||
assert result2["data"] == {
|
||||
"auto_configure": True,
|
||||
"host": "elk://1.2.3.4",
|
||||
"prefix": "guest_house",
|
||||
"username": "",
|
||||
"password": "",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_multiple_instances_with_tls_v12(hass):
|
||||
"""Test we can setup a secure elk with tls v1_2."""
|
||||
|
||||
elk_discovery_1 = ElkSystem("aa:bb:cc:dd:ee:ff", "127.0.0.1", 2601)
|
||||
elk_discovery_2 = ElkSystem("aa:bb:cc:dd:ee:fe", "127.0.0.2", 2601)
|
||||
|
||||
with _patch_discovery(device=elk_discovery_1):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert not result["errors"]
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
mocked_elk = mock_elk(invalid_auth=False, sync_complete=True)
|
||||
|
||||
with _patch_elk(elk=mocked_elk):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"device": elk_discovery_1.mac_address},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert not result["errors"]
|
||||
assert result2["step_id"] == "discovered_connection"
|
||||
with _patch_discovery(device=elk_discovery_1), _patch_elk(elk=mocked_elk), patch(
|
||||
"homeassistant.components.elkm1.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.elkm1.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
"protocol": "TLS 1.2",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == "create_entry"
|
||||
assert result3["title"] == "ElkM1 ddeeff"
|
||||
assert result3["data"] == {
|
||||
"auto_configure": True,
|
||||
"host": "elksv1_2://127.0.0.1",
|
||||
"password": "test-password",
|
||||
"prefix": "",
|
||||
"username": "test-username",
|
||||
}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
# Now try to add another instance with the different discovery info
|
||||
with _patch_discovery(device=elk_discovery_2):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert not result["errors"]
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
mocked_elk = mock_elk(invalid_auth=False, sync_complete=True)
|
||||
|
||||
with _patch_elk(elk=mocked_elk):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"device": elk_discovery_2.mac_address},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with _patch_discovery(device=elk_discovery_2), _patch_elk(elk=mocked_elk), patch(
|
||||
"homeassistant.components.elkm1.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
"protocol": "TLS 1.2",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == "create_entry"
|
||||
assert result3["title"] == "ElkM1 ddeefe"
|
||||
assert result3["data"] == {
|
||||
"auto_configure": True,
|
||||
"host": "elksv1_2://127.0.0.2",
|
||||
"password": "test-password",
|
||||
"prefix": "ddeefe",
|
||||
"username": "test-username",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
# Finally, try to add another instance manually with no discovery info
|
||||
|
||||
with _patch_discovery(no_device=True):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {}
|
||||
assert result["step_id"] == "manual_connection"
|
||||
|
||||
mocked_elk = mock_elk(invalid_auth=False, sync_complete=True)
|
||||
|
||||
with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch(
|
||||
"homeassistant.components.elkm1.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"protocol": "TLS 1.2",
|
||||
"address": "1.2.3.4",
|
||||
"prefix": "guest_house",
|
||||
"password": "test-password",
|
||||
"username": "test-username",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
import pprint
|
||||
|
||||
pprint.pprint(result2)
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "guest_house"
|
||||
assert result2["data"] == {
|
||||
"auto_configure": True,
|
||||
"host": "elksv1_2://1.2.3.4",
|
||||
"prefix": "guest_house",
|
||||
"password": "test-password",
|
||||
"username": "test-username",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
@ -104,7 +104,7 @@ async def primary_calendar(
|
||||
"""Fixture to return the primary calendar."""
|
||||
mock_calendar_get(
|
||||
"primary",
|
||||
{"id": primary_calendar_email, "summary": "Personal"},
|
||||
{"id": primary_calendar_email, "summary": "Personal", "accessRole": "owner"},
|
||||
exc=primary_calendar_error,
|
||||
)
|
||||
|
||||
|
@ -768,7 +768,7 @@ async def test_assign_unique_id(
|
||||
|
||||
mock_calendar_get(
|
||||
"primary",
|
||||
{"id": EMAIL_ADDRESS, "summary": "Personal"},
|
||||
{"id": EMAIL_ADDRESS, "summary": "Personal", "accessRole": "reader"},
|
||||
)
|
||||
|
||||
mock_calendars_list({"items": [test_api_calendar]})
|
||||
|
@ -6,7 +6,7 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.device_tracker.legacy import YAML_DEVICES
|
||||
from homeassistant.components.homekit.accessories import HomeDriver, HomeIIDManager
|
||||
from homeassistant.components.homekit.accessories import HomeDriver
|
||||
from homeassistant.components.homekit.const import BRIDGE_NAME, EVENT_HOMEKIT_CHANGED
|
||||
from homeassistant.components.homekit.iidmanager import AccessoryIIDStorage
|
||||
|
||||
@ -39,7 +39,7 @@ def run_driver(hass, loop, iid_storage):
|
||||
entry_id="",
|
||||
entry_title="mock entry",
|
||||
bridge_name=BRIDGE_NAME,
|
||||
iid_manager=HomeIIDManager(iid_storage),
|
||||
iid_storage=iid_storage,
|
||||
address="127.0.0.1",
|
||||
loop=loop,
|
||||
)
|
||||
@ -63,7 +63,7 @@ def hk_driver(hass, loop, iid_storage):
|
||||
entry_id="",
|
||||
entry_title="mock entry",
|
||||
bridge_name=BRIDGE_NAME,
|
||||
iid_manager=HomeIIDManager(iid_storage),
|
||||
iid_storage=iid_storage,
|
||||
address="127.0.0.1",
|
||||
loop=loop,
|
||||
)
|
||||
@ -91,7 +91,7 @@ def mock_hap(hass, loop, iid_storage, mock_zeroconf):
|
||||
entry_id="",
|
||||
entry_title="mock entry",
|
||||
bridge_name=BRIDGE_NAME,
|
||||
iid_manager=HomeIIDManager(iid_storage),
|
||||
iid_storage=iid_storage,
|
||||
address="127.0.0.1",
|
||||
loop=loop,
|
||||
)
|
||||
|
249
tests/components/homekit/fixtures/iids_v1
Normal file
249
tests/components/homekit/fixtures/iids_v1
Normal file
@ -0,0 +1,249 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 1,
|
||||
"key": "homekit.v1.iids",
|
||||
"data": {
|
||||
"allocations": {
|
||||
"1_3E___": 1,
|
||||
"1_3E__14_": 2,
|
||||
"1_3E__20_": 3,
|
||||
"1_3E__21_": 4,
|
||||
"1_3E__23_": 5,
|
||||
"1_3E__30_": 6,
|
||||
"1_3E__52_": 7,
|
||||
"1_A2___": 8,
|
||||
"1_A2__37_": 9,
|
||||
"935391877_3E___": 10,
|
||||
"935391877_3E__14_": 11,
|
||||
"935391877_3E__20_": 12,
|
||||
"935391877_3E__21_": 13,
|
||||
"935391877_3E__23_": 14,
|
||||
"935391877_3E__30_": 15,
|
||||
"935391877_3E__52_": 16,
|
||||
"935391877_4A___": 17,
|
||||
"935391877_4A__F_": 18,
|
||||
"935391877_4A__33_": 19,
|
||||
"935391877_4A__11_": 20,
|
||||
"935391877_4A__35_": 21,
|
||||
"935391877_4A__36_": 22,
|
||||
"935391877_4A__D_": 23,
|
||||
"935391877_4A__12_": 24,
|
||||
"935391877_4A__34_": 25,
|
||||
"935391877_4A__10_": 26,
|
||||
"935391877_B7___": 27,
|
||||
"935391877_B7__B0_": 28,
|
||||
"935391877_B7__BF_": 29,
|
||||
"935391877_B7__AF_": 30,
|
||||
"985724734_3E___": 31,
|
||||
"985724734_3E__14_": 32,
|
||||
"985724734_3E__20_": 33,
|
||||
"985724734_3E__21_": 34,
|
||||
"985724734_3E__23_": 35,
|
||||
"985724734_3E__30_": 36,
|
||||
"985724734_3E__52_": 37,
|
||||
"985724734_4A___": 38,
|
||||
"985724734_4A__F_": 39,
|
||||
"985724734_4A__33_": 40,
|
||||
"985724734_4A__11_": 41,
|
||||
"985724734_4A__35_": 42,
|
||||
"985724734_4A__36_": 43,
|
||||
"985724734_4A__D_": 44,
|
||||
"985724734_4A__12_": 45,
|
||||
"985724734_4A__34_": 46,
|
||||
"985724734_4A__10_": 47,
|
||||
"985724734_B7___": 48,
|
||||
"985724734_B7__B0_": 49,
|
||||
"985724734_B7__BF_": 50,
|
||||
"985724734_B7__AF_": 51,
|
||||
"3083074204_3E___": 52,
|
||||
"3083074204_3E__14_": 53,
|
||||
"3083074204_3E__20_": 54,
|
||||
"3083074204_3E__21_": 55,
|
||||
"3083074204_3E__23_": 56,
|
||||
"3083074204_3E__30_": 57,
|
||||
"3083074204_3E__52_": 58,
|
||||
"3083074204_4A___": 59,
|
||||
"3083074204_4A__F_": 60,
|
||||
"3083074204_4A__33_": 61,
|
||||
"3083074204_4A__11_": 62,
|
||||
"3083074204_4A__35_": 63,
|
||||
"3083074204_4A__36_": 64,
|
||||
"3083074204_4A__D_": 65,
|
||||
"3083074204_4A__12_": 66,
|
||||
"3083074204_4A__34_": 67,
|
||||
"3083074204_4A__10_": 68,
|
||||
"3083074204_B7___": 69,
|
||||
"3083074204_B7__B0_": 70,
|
||||
"3083074204_B7__BF_": 71,
|
||||
"3083074204_B7__AF_": 72,
|
||||
"3032741347_3E___": 73,
|
||||
"3032741347_3E__14_": 74,
|
||||
"3032741347_3E__20_": 75,
|
||||
"3032741347_3E__21_": 76,
|
||||
"3032741347_3E__23_": 77,
|
||||
"3032741347_3E__30_": 78,
|
||||
"3032741347_3E__52_": 79,
|
||||
"3032741347_4A___": 80,
|
||||
"3032741347_4A__F_": 81,
|
||||
"3032741347_4A__33_": 82,
|
||||
"3032741347_4A__11_": 83,
|
||||
"3032741347_4A__35_": 84,
|
||||
"3032741347_4A__36_": 85,
|
||||
"3032741347_4A__D_": 86,
|
||||
"3032741347_4A__12_": 87,
|
||||
"3032741347_4A__34_": 88,
|
||||
"3032741347_4A__10_": 89,
|
||||
"3032741347_B7___": 90,
|
||||
"3032741347_B7__B0_": 91,
|
||||
"3032741347_B7__BF_": 92,
|
||||
"3032741347_B7__AF_": 93,
|
||||
"3681509609_3E___": 94,
|
||||
"3681509609_3E__14_": 95,
|
||||
"3681509609_3E__20_": 96,
|
||||
"3681509609_3E__21_": 97,
|
||||
"3681509609_3E__23_": 98,
|
||||
"3681509609_3E__30_": 99,
|
||||
"3681509609_3E__52_": 100,
|
||||
"3681509609_4A___": 101,
|
||||
"3681509609_4A__F_": 102,
|
||||
"3681509609_4A__33_": 103,
|
||||
"3681509609_4A__11_": 104,
|
||||
"3681509609_4A__35_": 105,
|
||||
"3681509609_4A__36_": 106,
|
||||
"3681509609_4A__D_": 107,
|
||||
"3681509609_4A__12_": 108,
|
||||
"3681509609_4A__34_": 109,
|
||||
"3681509609_4A__10_": 110,
|
||||
"3681509609_B7___": 111,
|
||||
"3681509609_B7__B0_": 112,
|
||||
"3681509609_B7__BF_": 113,
|
||||
"3681509609_B7__AF_": 114,
|
||||
"3866063418_3E___": 115,
|
||||
"3866063418_3E__14_": 116,
|
||||
"3866063418_3E__20_": 117,
|
||||
"3866063418_3E__21_": 118,
|
||||
"3866063418_3E__23_": 119,
|
||||
"3866063418_3E__30_": 120,
|
||||
"3866063418_3E__52_": 121,
|
||||
"3866063418_4A___": 122,
|
||||
"3866063418_4A__F_": 123,
|
||||
"3866063418_4A__33_": 124,
|
||||
"3866063418_4A__11_": 125,
|
||||
"3866063418_4A__35_": 126,
|
||||
"3866063418_4A__36_": 127,
|
||||
"3866063418_4A__D_": 128,
|
||||
"3866063418_4A__12_": 129,
|
||||
"3866063418_4A__34_": 130,
|
||||
"3866063418_4A__10_": 131,
|
||||
"3866063418_B7___": 132,
|
||||
"3866063418_B7__B0_": 133,
|
||||
"3866063418_B7__BF_": 134,
|
||||
"3866063418_B7__AF_": 135,
|
||||
"3239498961_3E___": 136,
|
||||
"3239498961_3E__14_": 137,
|
||||
"3239498961_3E__20_": 138,
|
||||
"3239498961_3E__21_": 139,
|
||||
"3239498961_3E__23_": 140,
|
||||
"3239498961_3E__30_": 141,
|
||||
"3239498961_3E__52_": 142,
|
||||
"3239498961_4A___": 143,
|
||||
"3239498961_4A__F_": 144,
|
||||
"3239498961_4A__33_": 145,
|
||||
"3239498961_4A__11_": 146,
|
||||
"3239498961_4A__35_": 147,
|
||||
"3239498961_4A__36_": 148,
|
||||
"3239498961_4A__D_": 149,
|
||||
"3239498961_4A__12_": 150,
|
||||
"3239498961_4A__34_": 151,
|
||||
"3239498961_4A__10_": 152,
|
||||
"3239498961_B7___": 153,
|
||||
"3239498961_B7__B0_": 154,
|
||||
"3239498961_B7__BF_": 155,
|
||||
"3239498961_B7__AF_": 156,
|
||||
"3289831818_3E___": 157,
|
||||
"3289831818_3E__14_": 158,
|
||||
"3289831818_3E__20_": 159,
|
||||
"3289831818_3E__21_": 160,
|
||||
"3289831818_3E__23_": 161,
|
||||
"3289831818_3E__30_": 162,
|
||||
"3289831818_3E__52_": 163,
|
||||
"3289831818_4A___": 164,
|
||||
"3289831818_4A__F_": 165,
|
||||
"3289831818_4A__33_": 166,
|
||||
"3289831818_4A__11_": 167,
|
||||
"3289831818_4A__35_": 168,
|
||||
"3289831818_4A__36_": 169,
|
||||
"3289831818_4A__D_": 170,
|
||||
"3289831818_4A__12_": 171,
|
||||
"3289831818_4A__34_": 172,
|
||||
"3289831818_4A__10_": 173,
|
||||
"3289831818_B7___": 174,
|
||||
"3289831818_B7__B0_": 175,
|
||||
"3289831818_B7__BF_": 176,
|
||||
"3289831818_B7__AF_": 177,
|
||||
"3071722771_3E___": 178,
|
||||
"3071722771_3E__14_": 179,
|
||||
"3071722771_3E__20_": 180,
|
||||
"3071722771_3E__21_": 181,
|
||||
"3071722771_3E__23_": 182,
|
||||
"3071722771_3E__30_": 183,
|
||||
"3071722771_3E__52_": 184,
|
||||
"3071722771_4A___": 185,
|
||||
"3071722771_4A__F_": 186,
|
||||
"3071722771_4A__33_": 187,
|
||||
"3071722771_4A__11_": 188,
|
||||
"3071722771_4A__35_": 189,
|
||||
"3071722771_4A__36_": 190,
|
||||
"3071722771_4A__D_": 191,
|
||||
"3071722771_4A__12_": 192,
|
||||
"3071722771_4A__34_": 193,
|
||||
"3071722771_4A__10_": 194,
|
||||
"3071722771_B7___": 195,
|
||||
"3071722771_B7__B0_": 196,
|
||||
"3071722771_B7__BF_": 197,
|
||||
"3071722771_B7__AF_": 198,
|
||||
"3391630365_3E___": 199,
|
||||
"3391630365_3E__14_": 200,
|
||||
"3391630365_3E__20_": 201,
|
||||
"3391630365_3E__21_": 202,
|
||||
"3391630365_3E__23_": 203,
|
||||
"3391630365_3E__30_": 204,
|
||||
"3391630365_3E__52_": 205,
|
||||
"3391630365_4A___": 206,
|
||||
"3391630365_4A__F_": 207,
|
||||
"3391630365_4A__33_": 208,
|
||||
"3391630365_4A__11_": 209,
|
||||
"3391630365_4A__35_": 210,
|
||||
"3391630365_4A__36_": 211,
|
||||
"3391630365_4A__D_": 212,
|
||||
"3391630365_4A__12_": 213,
|
||||
"3391630365_4A__34_": 214,
|
||||
"3391630365_4A__10_": 215,
|
||||
"3391630365_B7___": 216,
|
||||
"3391630365_B7__B0_": 217,
|
||||
"3391630365_B7__BF_": 218,
|
||||
"3391630365_B7__AF_": 219,
|
||||
"3274187032_3E___": 220,
|
||||
"3274187032_3E__14_": 221,
|
||||
"3274187032_3E__20_": 222,
|
||||
"3274187032_3E__21_": 223,
|
||||
"3274187032_3E__23_": 224,
|
||||
"3274187032_3E__30_": 225,
|
||||
"3274187032_3E__52_": 226,
|
||||
"3274187032_4A___": 227,
|
||||
"3274187032_4A__F_": 228,
|
||||
"3274187032_4A__33_": 229,
|
||||
"3274187032_4A__11_": 230,
|
||||
"3274187032_4A__35_": 231,
|
||||
"3274187032_4A__36_": 232,
|
||||
"3274187032_4A__D_": 233,
|
||||
"3274187032_4A__12_": 234,
|
||||
"3274187032_4A__34_": 235,
|
||||
"3274187032_4A__10_": 236,
|
||||
"3274187032_B7___": 237,
|
||||
"3274187032_B7__B0_": 238,
|
||||
"3274187032_B7__BF_": 239,
|
||||
"3274187032_B7__AF_": 240
|
||||
}
|
||||
}
|
||||
}
|
50
tests/components/homekit/fixtures/iids_v1_with_underscore
Normal file
50
tests/components/homekit/fixtures/iids_v1_with_underscore
Normal file
@ -0,0 +1,50 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 1,
|
||||
"key": "homekit.8a47205bd97c07d7a908f10166ebe636.iids",
|
||||
"data": {
|
||||
"allocations": {
|
||||
"1_3E___": 1,
|
||||
"1_3E__14_": 2,
|
||||
"1_3E__20_": 3,
|
||||
"1_3E__21_": 4,
|
||||
"1_3E__23_": 5,
|
||||
"1_3E__30_": 6,
|
||||
"1_3E__52_": 7,
|
||||
"1_A2___": 8,
|
||||
"1_A2__37_": 9,
|
||||
"1973560704_3E___": 10,
|
||||
"1973560704_3E__14_": 11,
|
||||
"1973560704_3E__20_": 12,
|
||||
"1973560704_3E__21_": 13,
|
||||
"1973560704_3E__23_": 14,
|
||||
"1973560704_3E__30_": 15,
|
||||
"1973560704_3E__52_": 16,
|
||||
"1973560704_3E__53_": 17,
|
||||
"1973560704_89_pressed-__": 18,
|
||||
"1973560704_89_pressed-_73_": 19,
|
||||
"1973560704_89_pressed-_23_": 20,
|
||||
"1973560704_89_pressed-_CB_": 21,
|
||||
"1973560704_CC_pressed-__": 22,
|
||||
"1973560704_CC_pressed-_CD_": 23,
|
||||
"1973560704_89_changed_states-__": 24,
|
||||
"1973560704_89_changed_states-_73_": 25,
|
||||
"1973560704_89_changed_states-_23_": 26,
|
||||
"1973560704_89_changed_states-_CB_": 27,
|
||||
"1973560704_CC_changed_states-__": 28,
|
||||
"1973560704_CC_changed_states-_CD_": 29,
|
||||
"1973560704_89_turned_off-__": 30,
|
||||
"1973560704_89_turned_off-_73_": 31,
|
||||
"1973560704_89_turned_off-_23_": 32,
|
||||
"1973560704_89_turned_off-_CB_": 33,
|
||||
"1973560704_CC_turned_off-__": 34,
|
||||
"1973560704_CC_turned_off-_CD_": 35,
|
||||
"1973560704_89_turned_on-__": 36,
|
||||
"1973560704_89_turned_on-_73_": 37,
|
||||
"1973560704_89_turned_on-_23_": 38,
|
||||
"1973560704_89_turned_on-_CB_": 39,
|
||||
"1973560704_CC_turned_on-__": 40,
|
||||
"1973560704_CC_turned_on-_CD_": 41
|
||||
}
|
||||
}
|
||||
}
|
273
tests/components/homekit/fixtures/iids_v2
Normal file
273
tests/components/homekit/fixtures/iids_v2
Normal file
@ -0,0 +1,273 @@
|
||||
{
|
||||
"version": 2,
|
||||
"minor_version": 1,
|
||||
"key": "homekit.v2.iids",
|
||||
"data": {
|
||||
"allocations": {
|
||||
"1": {
|
||||
"3E___": 1,
|
||||
"3E__14_": 2,
|
||||
"3E__20_": 3,
|
||||
"3E__21_": 4,
|
||||
"3E__23_": 5,
|
||||
"3E__30_": 6,
|
||||
"3E__52_": 7,
|
||||
"A2___": 8,
|
||||
"A2__37_": 9
|
||||
},
|
||||
"935391877": {
|
||||
"3E___": 1,
|
||||
"3E__14_": 11,
|
||||
"3E__20_": 12,
|
||||
"3E__21_": 13,
|
||||
"3E__23_": 14,
|
||||
"3E__30_": 15,
|
||||
"3E__52_": 16,
|
||||
"4A___": 17,
|
||||
"4A__F_": 18,
|
||||
"4A__33_": 19,
|
||||
"4A__11_": 20,
|
||||
"4A__35_": 21,
|
||||
"4A__36_": 22,
|
||||
"4A__D_": 23,
|
||||
"4A__12_": 24,
|
||||
"4A__34_": 25,
|
||||
"4A__10_": 26,
|
||||
"B7___": 27,
|
||||
"B7__B0_": 28,
|
||||
"B7__BF_": 29,
|
||||
"B7__AF_": 30
|
||||
},
|
||||
"985724734": {
|
||||
"3E___": 1,
|
||||
"3E__14_": 32,
|
||||
"3E__20_": 33,
|
||||
"3E__21_": 34,
|
||||
"3E__23_": 35,
|
||||
"3E__30_": 36,
|
||||
"3E__52_": 37,
|
||||
"4A___": 38,
|
||||
"4A__F_": 39,
|
||||
"4A__33_": 40,
|
||||
"4A__11_": 41,
|
||||
"4A__35_": 42,
|
||||
"4A__36_": 43,
|
||||
"4A__D_": 44,
|
||||
"4A__12_": 45,
|
||||
"4A__34_": 46,
|
||||
"4A__10_": 47,
|
||||
"B7___": 48,
|
||||
"B7__B0_": 49,
|
||||
"B7__BF_": 50,
|
||||
"B7__AF_": 51
|
||||
},
|
||||
"3083074204": {
|
||||
"3E___": 1,
|
||||
"3E__14_": 53,
|
||||
"3E__20_": 54,
|
||||
"3E__21_": 55,
|
||||
"3E__23_": 56,
|
||||
"3E__30_": 57,
|
||||
"3E__52_": 58,
|
||||
"4A___": 59,
|
||||
"4A__F_": 60,
|
||||
"4A__33_": 61,
|
||||
"4A__11_": 62,
|
||||
"4A__35_": 63,
|
||||
"4A__36_": 64,
|
||||
"4A__D_": 65,
|
||||
"4A__12_": 66,
|
||||
"4A__34_": 67,
|
||||
"4A__10_": 68,
|
||||
"B7___": 69,
|
||||
"B7__B0_": 70,
|
||||
"B7__BF_": 71,
|
||||
"B7__AF_": 72
|
||||
},
|
||||
"3032741347": {
|
||||
"3E___": 1,
|
||||
"3E__14_": 74,
|
||||
"3E__20_": 75,
|
||||
"3E__21_": 76,
|
||||
"3E__23_": 77,
|
||||
"3E__30_": 78,
|
||||
"3E__52_": 79,
|
||||
"4A___": 80,
|
||||
"4A__F_": 81,
|
||||
"4A__33_": 82,
|
||||
"4A__11_": 83,
|
||||
"4A__35_": 84,
|
||||
"4A__36_": 85,
|
||||
"4A__D_": 86,
|
||||
"4A__12_": 87,
|
||||
"4A__34_": 88,
|
||||
"4A__10_": 89,
|
||||
"B7___": 90,
|
||||
"B7__B0_": 91,
|
||||
"B7__BF_": 92,
|
||||
"B7__AF_": 93
|
||||
},
|
||||
"3681509609": {
|
||||
"3E___": 1,
|
||||
"3E__14_": 95,
|
||||
"3E__20_": 96,
|
||||
"3E__21_": 97,
|
||||
"3E__23_": 98,
|
||||
"3E__30_": 99,
|
||||
"3E__52_": 100,
|
||||
"4A___": 101,
|
||||
"4A__F_": 102,
|
||||
"4A__33_": 103,
|
||||
"4A__11_": 104,
|
||||
"4A__35_": 105,
|
||||
"4A__36_": 106,
|
||||
"4A__D_": 107,
|
||||
"4A__12_": 108,
|
||||
"4A__34_": 109,
|
||||
"4A__10_": 110,
|
||||
"B7___": 111,
|
||||
"B7__B0_": 112,
|
||||
"B7__BF_": 113,
|
||||
"B7__AF_": 114
|
||||
},
|
||||
"3866063418": {
|
||||
"3E___": 1,
|
||||
"3E__14_": 116,
|
||||
"3E__20_": 117,
|
||||
"3E__21_": 118,
|
||||
"3E__23_": 119,
|
||||
"3E__30_": 120,
|
||||
"3E__52_": 121,
|
||||
"4A___": 122,
|
||||
"4A__F_": 123,
|
||||
"4A__33_": 124,
|
||||
"4A__11_": 125,
|
||||
"4A__35_": 126,
|
||||
"4A__36_": 127,
|
||||
"4A__D_": 128,
|
||||
"4A__12_": 129,
|
||||
"4A__34_": 130,
|
||||
"4A__10_": 131,
|
||||
"B7___": 132,
|
||||
"B7__B0_": 133,
|
||||
"B7__BF_": 134,
|
||||
"B7__AF_": 135
|
||||
},
|
||||
"3239498961": {
|
||||
"3E___": 1,
|
||||
"3E__14_": 137,
|
||||
"3E__20_": 138,
|
||||
"3E__21_": 139,
|
||||
"3E__23_": 140,
|
||||
"3E__30_": 141,
|
||||
"3E__52_": 142,
|
||||
"4A___": 143,
|
||||
"4A__F_": 144,
|
||||
"4A__33_": 145,
|
||||
"4A__11_": 146,
|
||||
"4A__35_": 147,
|
||||
"4A__36_": 148,
|
||||
"4A__D_": 149,
|
||||
"4A__12_": 150,
|
||||
"4A__34_": 151,
|
||||
"4A__10_": 152,
|
||||
"B7___": 153,
|
||||
"B7__B0_": 154,
|
||||
"B7__BF_": 155,
|
||||
"B7__AF_": 156
|
||||
},
|
||||
"3289831818": {
|
||||
"3E___": 1,
|
||||
"3E__14_": 158,
|
||||
"3E__20_": 159,
|
||||
"3E__21_": 160,
|
||||
"3E__23_": 161,
|
||||
"3E__30_": 162,
|
||||
"3E__52_": 163,
|
||||
"4A___": 164,
|
||||
"4A__F_": 165,
|
||||
"4A__33_": 166,
|
||||
"4A__11_": 167,
|
||||
"4A__35_": 168,
|
||||
"4A__36_": 169,
|
||||
"4A__D_": 170,
|
||||
"4A__12_": 171,
|
||||
"4A__34_": 172,
|
||||
"4A__10_": 173,
|
||||
"B7___": 174,
|
||||
"B7__B0_": 175,
|
||||
"B7__BF_": 176,
|
||||
"B7__AF_": 177
|
||||
},
|
||||
"3071722771": {
|
||||
"3E___": 1,
|
||||
"3E__14_": 179,
|
||||
"3E__20_": 180,
|
||||
"3E__21_": 181,
|
||||
"3E__23_": 182,
|
||||
"3E__30_": 183,
|
||||
"3E__52_": 184,
|
||||
"4A___": 185,
|
||||
"4A__F_": 186,
|
||||
"4A__33_": 187,
|
||||
"4A__11_": 188,
|
||||
"4A__35_": 189,
|
||||
"4A__36_": 190,
|
||||
"4A__D_": 191,
|
||||
"4A__12_": 192,
|
||||
"4A__34_": 193,
|
||||
"4A__10_": 194,
|
||||
"B7___": 195,
|
||||
"B7__B0_": 196,
|
||||
"B7__BF_": 197,
|
||||
"B7__AF_": 198
|
||||
},
|
||||
"3391630365": {
|
||||
"3E___": 1,
|
||||
"3E__14_": 200,
|
||||
"3E__20_": 201,
|
||||
"3E__21_": 202,
|
||||
"3E__23_": 203,
|
||||
"3E__30_": 204,
|
||||
"3E__52_": 205,
|
||||
"4A___": 206,
|
||||
"4A__F_": 207,
|
||||
"4A__33_": 208,
|
||||
"4A__11_": 209,
|
||||
"4A__35_": 210,
|
||||
"4A__36_": 211,
|
||||
"4A__D_": 212,
|
||||
"4A__12_": 213,
|
||||
"4A__34_": 214,
|
||||
"4A__10_": 215,
|
||||
"B7___": 216,
|
||||
"B7__B0_": 217,
|
||||
"B7__BF_": 218,
|
||||
"B7__AF_": 219
|
||||
},
|
||||
"3274187032": {
|
||||
"3E___": 1,
|
||||
"3E__14_": 221,
|
||||
"3E__20_": 222,
|
||||
"3E__21_": 223,
|
||||
"3E__23_": 224,
|
||||
"3E__30_": 225,
|
||||
"3E__52_": 226,
|
||||
"4A___": 227,
|
||||
"4A__F_": 228,
|
||||
"4A__33_": 229,
|
||||
"4A__11_": 230,
|
||||
"4A__35_": 231,
|
||||
"4A__36_": 232,
|
||||
"4A__D_": 233,
|
||||
"4A__12_": 234,
|
||||
"4A__34_": 235,
|
||||
"4A__10_": 236,
|
||||
"B7___": 237,
|
||||
"B7__B0_": 238,
|
||||
"B7__BF_": 239,
|
||||
"B7__AF_": 240
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
tests/components/homekit/fixtures/iids_v2_with_underscore
Normal file
54
tests/components/homekit/fixtures/iids_v2_with_underscore
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"version": 2,
|
||||
"minor_version": 1,
|
||||
"key": "homekit.8a47205bd97c07d7a908f10166ebe636.iids",
|
||||
"data": {
|
||||
"allocations": {
|
||||
"1": {
|
||||
"3E___": 1,
|
||||
"3E__14_": 2,
|
||||
"3E__20_": 3,
|
||||
"3E__21_": 4,
|
||||
"3E__23_": 5,
|
||||
"3E__30_": 6,
|
||||
"3E__52_": 7,
|
||||
"A2___": 8,
|
||||
"A2__37_": 9
|
||||
},
|
||||
"1973560704": {
|
||||
"3E___": 1,
|
||||
"3E__14_": 11,
|
||||
"3E__20_": 12,
|
||||
"3E__21_": 13,
|
||||
"3E__23_": 14,
|
||||
"3E__30_": 15,
|
||||
"3E__52_": 16,
|
||||
"3E__53_": 17,
|
||||
"89_pressed-__": 18,
|
||||
"89_pressed-_73_": 19,
|
||||
"89_pressed-_23_": 20,
|
||||
"89_pressed-_CB_": 21,
|
||||
"CC_pressed-__": 22,
|
||||
"CC_pressed-_CD_": 23,
|
||||
"89_changed_states-__": 24,
|
||||
"89_changed_states-_73_": 25,
|
||||
"89_changed_states-_23_": 26,
|
||||
"89_changed_states-_CB_": 27,
|
||||
"CC_changed_states-__": 28,
|
||||
"CC_changed_states-_CD_": 29,
|
||||
"89_turned_off-__": 30,
|
||||
"89_turned_off-_73_": 31,
|
||||
"89_turned_off-_23_": 32,
|
||||
"89_turned_off-_CB_": 33,
|
||||
"CC_turned_off-__": 34,
|
||||
"CC_turned_off-_CD_": 35,
|
||||
"89_turned_on-__": 36,
|
||||
"89_turned_on-_73_": 37,
|
||||
"89_turned_on-_23_": 38,
|
||||
"89_turned_on-_CB_": 39,
|
||||
"CC_turned_on-__": 40,
|
||||
"CC_turned_on-_CD_": 41
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -10,7 +10,6 @@ from homeassistant.components.homekit.accessories import (
|
||||
HomeAccessory,
|
||||
HomeBridge,
|
||||
HomeDriver,
|
||||
HomeIIDManager,
|
||||
)
|
||||
from homeassistant.components.homekit.const import (
|
||||
ATTR_DISPLAY_NAME,
|
||||
@ -724,7 +723,7 @@ def test_home_driver(iid_storage):
|
||||
"entry_id",
|
||||
"name",
|
||||
"title",
|
||||
iid_manager=HomeIIDManager(iid_storage),
|
||||
iid_storage=iid_storage,
|
||||
address=ip_address,
|
||||
port=port,
|
||||
persist_file=path,
|
||||
@ -752,22 +751,3 @@ def test_home_driver(iid_storage):
|
||||
|
||||
mock_unpair.assert_called_with("client_uuid")
|
||||
mock_show_msg.assert_called_with("hass", "entry_id", "title (any)", pin, "X-HM://0")
|
||||
|
||||
|
||||
async def test_iid_collision_raises(hass, hk_driver):
|
||||
"""Test iid collision raises.
|
||||
|
||||
If we try to allocate the same IID to the an accessory twice, we should
|
||||
raise an exception.
|
||||
"""
|
||||
|
||||
entity_id = "light.accessory"
|
||||
entity_id2 = "light.accessory2"
|
||||
|
||||
hass.states.async_set(entity_id, STATE_OFF)
|
||||
hass.states.async_set(entity_id2, STATE_OFF)
|
||||
|
||||
HomeAccessory(hass, hk_driver, "Home Accessory", entity_id, 2, {})
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
HomeAccessory(hass, hk_driver, "Home Accessory", entity_id2, 2, {})
|
||||
|
@ -43,6 +43,18 @@ async def test_config_entry_running(hass, hass_client, hk_driver, mock_async_zer
|
||||
diag = await get_diagnostics_for_config_entry(hass, hass_client, entry)
|
||||
assert diag == {
|
||||
"bridge": {},
|
||||
"iid_storage": {
|
||||
"1": {
|
||||
"3E__14_": 2,
|
||||
"3E__20_": 3,
|
||||
"3E__21_": 4,
|
||||
"3E__23_": 5,
|
||||
"3E__30_": 6,
|
||||
"3E__52_": 7,
|
||||
"A2__37_": 9,
|
||||
"A2___": 8,
|
||||
}
|
||||
},
|
||||
"accessories": [
|
||||
{
|
||||
"aid": 1,
|
||||
@ -257,6 +269,20 @@ async def test_config_entry_accessory(
|
||||
},
|
||||
"config_version": 2,
|
||||
"pairing_id": ANY,
|
||||
"iid_storage": {
|
||||
"1": {
|
||||
"3E__14_": 2,
|
||||
"3E__20_": 3,
|
||||
"3E__21_": 4,
|
||||
"3E__23_": 5,
|
||||
"3E__30_": 6,
|
||||
"3E__52_": 7,
|
||||
"43__25_": 11,
|
||||
"43___": 10,
|
||||
"A2__37_": 9,
|
||||
"A2___": 8,
|
||||
}
|
||||
},
|
||||
"status": 1,
|
||||
}
|
||||
with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch(
|
||||
|
@ -262,7 +262,7 @@ async def test_homekit_setup(hass, hk_driver, mock_async_zeroconf):
|
||||
async_zeroconf_instance=zeroconf_mock,
|
||||
zeroconf_server=f"{uuid}-hap.local.",
|
||||
loader=ANY,
|
||||
iid_manager=ANY,
|
||||
iid_storage=ANY,
|
||||
)
|
||||
assert homekit.driver.safe_mode is False
|
||||
|
||||
@ -306,7 +306,7 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_async_zeroconf):
|
||||
async_zeroconf_instance=mock_async_zeroconf,
|
||||
zeroconf_server=f"{uuid}-hap.local.",
|
||||
loader=ANY,
|
||||
iid_manager=ANY,
|
||||
iid_storage=ANY,
|
||||
)
|
||||
|
||||
|
||||
@ -350,7 +350,7 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_async_zeroconf):
|
||||
async_zeroconf_instance=async_zeroconf_instance,
|
||||
zeroconf_server=f"{uuid}-hap.local.",
|
||||
loader=ANY,
|
||||
iid_manager=ANY,
|
||||
iid_storage=ANY,
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
"""Tests for the HomeKit IID manager."""
|
||||
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from homeassistant.components.homekit.const import DOMAIN
|
||||
@ -8,9 +7,10 @@ from homeassistant.components.homekit.iidmanager import (
|
||||
AccessoryIIDStorage,
|
||||
get_iid_storage_filename_for_entry_id,
|
||||
)
|
||||
from homeassistant.helpers.json import json_loads
|
||||
from homeassistant.util.uuid import random_uuid_hex
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
|
||||
async def test_iid_generation_and_restore(hass, iid_storage, hass_storage):
|
||||
@ -77,9 +77,6 @@ async def test_iid_generation_and_restore(hass, iid_storage, hass_storage):
|
||||
unique_service_unique_char_new_aid_iid1
|
||||
== unique_service_unique_char_new_aid_iid2
|
||||
)
|
||||
assert unique_service_unique_char_new_aid_iid1 != iid1
|
||||
assert unique_service_unique_char_new_aid_iid1 != unique_service_unique_char_iid1
|
||||
|
||||
await iid_storage.async_save()
|
||||
|
||||
iid_storage2 = AccessoryIIDStorage(hass, entry.entry_id)
|
||||
@ -99,3 +96,85 @@ async def test_iid_storage_filename(hass, iid_storage, hass_storage):
|
||||
assert iid_storage.store.path.endswith(
|
||||
get_iid_storage_filename_for_entry_id(entry.entry_id)
|
||||
)
|
||||
|
||||
|
||||
async def test_iid_migration_to_v2(hass, iid_storage, hass_storage):
|
||||
"""Test iid storage migration."""
|
||||
v1_iids = json_loads(load_fixture("iids_v1", DOMAIN))
|
||||
v2_iids = json_loads(load_fixture("iids_v2", DOMAIN))
|
||||
hass_storage["homekit.v1.iids"] = v1_iids
|
||||
hass_storage["homekit.v2.iids"] = v2_iids
|
||||
|
||||
iid_storage_v2 = AccessoryIIDStorage(hass, "v1")
|
||||
await iid_storage_v2.async_initialize()
|
||||
|
||||
iid_storage_v1 = AccessoryIIDStorage(hass, "v2")
|
||||
await iid_storage_v1.async_initialize()
|
||||
|
||||
assert iid_storage_v1.allocations == iid_storage_v2.allocations
|
||||
assert iid_storage_v1.allocated_iids == iid_storage_v2.allocated_iids
|
||||
|
||||
assert len(iid_storage_v2.allocations) == 12
|
||||
|
||||
for allocations in iid_storage_v2.allocations.values():
|
||||
assert allocations["3E___"] == 1
|
||||
|
||||
|
||||
async def test_iid_migration_to_v2_with_underscore(hass, iid_storage, hass_storage):
|
||||
"""Test iid storage migration with underscore."""
|
||||
v1_iids = json_loads(load_fixture("iids_v1_with_underscore", DOMAIN))
|
||||
v2_iids = json_loads(load_fixture("iids_v2_with_underscore", DOMAIN))
|
||||
hass_storage["homekit.v1_with_underscore.iids"] = v1_iids
|
||||
hass_storage["homekit.v2_with_underscore.iids"] = v2_iids
|
||||
|
||||
iid_storage_v2 = AccessoryIIDStorage(hass, "v1_with_underscore")
|
||||
await iid_storage_v2.async_initialize()
|
||||
|
||||
iid_storage_v1 = AccessoryIIDStorage(hass, "v2_with_underscore")
|
||||
await iid_storage_v1.async_initialize()
|
||||
|
||||
assert iid_storage_v1.allocations == iid_storage_v2.allocations
|
||||
assert iid_storage_v1.allocated_iids == iid_storage_v2.allocated_iids
|
||||
|
||||
assert len(iid_storage_v2.allocations) == 2
|
||||
|
||||
for allocations in iid_storage_v2.allocations.values():
|
||||
assert allocations["3E___"] == 1
|
||||
|
||||
|
||||
async def test_iid_generation_and_restore_v2(hass, iid_storage, hass_storage):
|
||||
"""Test generating iids and restoring them from storage."""
|
||||
entry = MockConfigEntry(domain=DOMAIN)
|
||||
|
||||
iid_storage = AccessoryIIDStorage(hass, entry.entry_id)
|
||||
await iid_storage.async_initialize()
|
||||
not_accessory_info_service_iid = iid_storage.get_or_allocate_iid(
|
||||
1, "000000AA-0000-1000-8000-0026BB765291", None, None, None
|
||||
)
|
||||
assert not_accessory_info_service_iid == 2
|
||||
assert iid_storage.allocated_iids == {"1": [1, 2]}
|
||||
not_accessory_info_service_iid_2 = iid_storage.get_or_allocate_iid(
|
||||
1, "000000BB-0000-1000-8000-0026BB765291", None, None, None
|
||||
)
|
||||
assert not_accessory_info_service_iid_2 == 3
|
||||
assert iid_storage.allocated_iids == {"1": [1, 2, 3]}
|
||||
not_accessory_info_service_iid_2 = iid_storage.get_or_allocate_iid(
|
||||
1, "000000BB-0000-1000-8000-0026BB765291", None, None, None
|
||||
)
|
||||
assert not_accessory_info_service_iid_2 == 3
|
||||
assert iid_storage.allocated_iids == {"1": [1, 2, 3]}
|
||||
accessory_info_service_iid = iid_storage.get_or_allocate_iid(
|
||||
1, "0000003E-0000-1000-8000-0026BB765291", None, None, None
|
||||
)
|
||||
assert accessory_info_service_iid == 1
|
||||
assert iid_storage.allocated_iids == {"1": [1, 2, 3]}
|
||||
accessory_info_service_iid = iid_storage.get_or_allocate_iid(
|
||||
1, "0000003E-0000-1000-8000-0026BB765291", None, None, None
|
||||
)
|
||||
assert accessory_info_service_iid == 1
|
||||
assert iid_storage.allocated_iids == {"1": [1, 2, 3]}
|
||||
accessory_info_service_iid = iid_storage.get_or_allocate_iid(
|
||||
2, "0000003E-0000-1000-8000-0026BB765291", None, None, None
|
||||
)
|
||||
assert accessory_info_service_iid == 1
|
||||
assert iid_storage.allocated_iids == {"1": [1, 2, 3], "2": [1]}
|
||||
|
@ -21,11 +21,14 @@ from homeassistant.components.lifx.manager import (
|
||||
)
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS_PCT,
|
||||
ATTR_COLOR_MODE,
|
||||
ATTR_COLOR_NAME,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ATTR_EFFECT,
|
||||
ATTR_HS_COLOR,
|
||||
ATTR_KELVIN,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_SUPPORTED_COLOR_MODES,
|
||||
ATTR_TRANSITION,
|
||||
@ -1397,6 +1400,131 @@ async def test_transitions_color_bulb(hass: HomeAssistant) -> None:
|
||||
bulb.set_color.reset_mock()
|
||||
|
||||
|
||||
async def test_lifx_set_state_color(hass: HomeAssistant) -> None:
|
||||
"""Test lifx.set_state works with color names and RGB."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_bulb_new_firmware()
|
||||
bulb.power_level = 65535
|
||||
bulb.color = [32000, None, 32000, 2700]
|
||||
with _patch_discovery(device=bulb), _patch_config_flow_try_connect(
|
||||
device=bulb
|
||||
), _patch_device(device=bulb):
|
||||
await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
|
||||
# brightness should convert from 8 to 16 bits
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_state",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255},
|
||||
blocking=True,
|
||||
)
|
||||
assert bulb.set_color.calls[0][0][0] == [32000, None, 65535, 2700]
|
||||
bulb.set_color.reset_mock()
|
||||
|
||||
# brightness_pct should convert into 16 bit
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_state",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 90},
|
||||
blocking=True,
|
||||
)
|
||||
assert bulb.set_color.calls[0][0][0] == [32000, None, 59110, 2700]
|
||||
bulb.set_color.reset_mock()
|
||||
|
||||
# color name should turn into hue, saturation
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_state",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_NAME: "red", ATTR_BRIGHTNESS_PCT: 100},
|
||||
blocking=True,
|
||||
)
|
||||
assert bulb.set_color.calls[0][0][0] == [0, 65535, 65535, 3500]
|
||||
bulb.set_color.reset_mock()
|
||||
|
||||
# unknown color name should reset back to neutral white, i.e. 3500K
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_state",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_NAME: "deepblack"},
|
||||
blocking=True,
|
||||
)
|
||||
assert bulb.set_color.calls[0][0][0] == [0, 0, 32000, 3500]
|
||||
bulb.set_color.reset_mock()
|
||||
|
||||
# RGB should convert to hue, saturation
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_state",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (0, 255, 0)},
|
||||
blocking=True,
|
||||
)
|
||||
assert bulb.set_color.calls[0][0][0] == [21845, 65535, 32000, 3500]
|
||||
bulb.set_color.reset_mock()
|
||||
|
||||
# XY should convert to hue, saturation
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_state",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_XY_COLOR: (0.34, 0.339)},
|
||||
blocking=True,
|
||||
)
|
||||
assert bulb.set_color.calls[0][0][0] == [5461, 5139, 32000, 3500]
|
||||
bulb.set_color.reset_mock()
|
||||
|
||||
|
||||
async def test_lifx_set_state_kelvin(hass: HomeAssistant) -> None:
|
||||
"""Test set_state works with old and new kelvin parameter names."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_bulb_new_firmware()
|
||||
bulb.power_level = 65535
|
||||
bulb.color = [32000, None, 32000, 6000]
|
||||
with _patch_discovery(device=bulb), _patch_config_flow_try_connect(
|
||||
device=bulb
|
||||
), _patch_device(device=bulb):
|
||||
await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "on"
|
||||
attributes = state.attributes
|
||||
assert attributes[ATTR_BRIGHTNESS] == 125
|
||||
assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
assert bulb.set_power.calls[0][0][0] is False
|
||||
bulb.set_power.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_state",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255, ATTR_KELVIN: 3500},
|
||||
blocking=True,
|
||||
)
|
||||
assert bulb.set_color.calls[0][0][0] == [32000, 0, 65535, 3500]
|
||||
bulb.set_color.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_state",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100, ATTR_COLOR_TEMP_KELVIN: 2700},
|
||||
blocking=True,
|
||||
)
|
||||
assert bulb.set_color.calls[0][0][0] == [32000, 0, 25700, 2700]
|
||||
bulb.set_color.reset_mock()
|
||||
|
||||
|
||||
async def test_infrared_color_bulb(hass: HomeAssistant) -> None:
|
||||
"""Test setting infrared with a color bulb."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
|
@ -24,6 +24,7 @@ from homeassistant.const import (
|
||||
CONF_ENTITIES,
|
||||
CONF_EXCLUDE,
|
||||
CONF_INCLUDE,
|
||||
EVENT_HOMEASSISTANT_FINAL_WRITE,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
@ -52,6 +53,15 @@ def set_utc(hass):
|
||||
hass.config.set_time_zone("UTC")
|
||||
|
||||
|
||||
def listeners_without_writes(listeners: dict[str, int]) -> dict[str, int]:
|
||||
"""Return listeners without final write listeners since we are not testing for these."""
|
||||
return {
|
||||
key: value
|
||||
for key, value in listeners.items()
|
||||
if key != EVENT_HOMEASSISTANT_FINAL_WRITE
|
||||
}
|
||||
|
||||
|
||||
async def _async_mock_logbook_platform(hass: HomeAssistant) -> None:
|
||||
class MockLogbookPlatform:
|
||||
"""Mock a logbook platform."""
|
||||
@ -684,7 +694,9 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities(
|
||||
assert msg["success"]
|
||||
|
||||
# Check our listener got unsubscribed
|
||||
assert hass.bus.async_listeners() == init_listeners
|
||||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) == listeners_without_writes(init_listeners)
|
||||
|
||||
|
||||
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
|
||||
@ -892,7 +904,9 @@ async def test_subscribe_unsubscribe_logbook_stream_included_entities(
|
||||
assert msg["success"]
|
||||
|
||||
# Check our listener got unsubscribed
|
||||
assert hass.bus.async_listeners() == init_listeners
|
||||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) == listeners_without_writes(init_listeners)
|
||||
|
||||
|
||||
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
|
||||
@ -1083,7 +1097,9 @@ async def test_logbook_stream_excluded_entities_inherits_filters_from_recorder(
|
||||
assert msg["success"]
|
||||
|
||||
# Check our listener got unsubscribed
|
||||
assert hass.bus.async_listeners() == init_listeners
|
||||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) == listeners_without_writes(init_listeners)
|
||||
|
||||
|
||||
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
|
||||
@ -1386,7 +1402,9 @@ async def test_subscribe_unsubscribe_logbook_stream(
|
||||
assert msg["success"]
|
||||
|
||||
# Check our listener got unsubscribed
|
||||
assert hass.bus.async_listeners() == init_listeners
|
||||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) == listeners_without_writes(init_listeners)
|
||||
|
||||
|
||||
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
|
||||
@ -1484,7 +1502,9 @@ async def test_subscribe_unsubscribe_logbook_stream_entities(
|
||||
assert msg["success"]
|
||||
|
||||
# Check our listener got unsubscribed
|
||||
assert hass.bus.async_listeners() == init_listeners
|
||||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) == listeners_without_writes(init_listeners)
|
||||
|
||||
|
||||
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
|
||||
@ -1586,12 +1606,9 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_with_end_time(
|
||||
assert msg["success"]
|
||||
|
||||
# Check our listener got unsubscribed
|
||||
listeners = hass.bus.async_listeners()
|
||||
# The async_fire_time_changed above triggers unsubscribe from
|
||||
# homeassistant_final_write, don't worry about those
|
||||
init_listeners.pop("homeassistant_final_write")
|
||||
listeners.pop("homeassistant_final_write")
|
||||
assert listeners == init_listeners
|
||||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) == listeners_without_writes(init_listeners)
|
||||
|
||||
|
||||
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
|
||||
@ -1659,7 +1676,9 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_past_only(
|
||||
assert msg["success"]
|
||||
|
||||
# Check our listener got unsubscribed
|
||||
assert hass.bus.async_listeners() == init_listeners
|
||||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) == listeners_without_writes(init_listeners)
|
||||
|
||||
|
||||
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
|
||||
@ -1759,7 +1778,9 @@ async def test_subscribe_unsubscribe_logbook_stream_big_query(
|
||||
assert msg["success"]
|
||||
|
||||
# Check our listener got unsubscribed
|
||||
assert hass.bus.async_listeners() == init_listeners
|
||||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) == listeners_without_writes(init_listeners)
|
||||
|
||||
|
||||
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
|
||||
@ -1853,7 +1874,9 @@ async def test_subscribe_unsubscribe_logbook_stream_device(
|
||||
assert msg["success"]
|
||||
|
||||
# Check our listener got unsubscribed
|
||||
assert hass.bus.async_listeners() == init_listeners
|
||||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) == listeners_without_writes(init_listeners)
|
||||
|
||||
|
||||
async def test_event_stream_bad_start_time(recorder_mock, hass, hass_ws_client):
|
||||
@ -1968,7 +1991,9 @@ async def test_logbook_stream_match_multiple_entities(
|
||||
assert msg["success"]
|
||||
|
||||
# Check our listener got unsubscribed
|
||||
assert hass.bus.async_listeners() == init_listeners
|
||||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) == listeners_without_writes(init_listeners)
|
||||
|
||||
|
||||
async def test_event_stream_bad_end_time(recorder_mock, hass, hass_ws_client):
|
||||
@ -2091,7 +2116,9 @@ async def test_live_stream_with_one_second_commit_interval(
|
||||
assert msg["success"]
|
||||
|
||||
# Check our listener got unsubscribed
|
||||
assert hass.bus.async_listeners() == init_listeners
|
||||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) == listeners_without_writes(init_listeners)
|
||||
|
||||
|
||||
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
|
||||
@ -2146,7 +2173,9 @@ async def test_subscribe_disconnected(recorder_mock, hass, hass_ws_client):
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check our listener got unsubscribed
|
||||
assert hass.bus.async_listeners() == init_listeners
|
||||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) == listeners_without_writes(init_listeners)
|
||||
|
||||
|
||||
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
|
||||
@ -2189,7 +2218,9 @@ async def test_stream_consumer_stop_processing(recorder_mock, hass, hass_ws_clie
|
||||
assert msg["type"] == TYPE_RESULT
|
||||
assert msg["success"]
|
||||
|
||||
assert hass.bus.async_listeners() != init_listeners
|
||||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) != listeners_without_writes(init_listeners)
|
||||
for _ in range(5):
|
||||
hass.states.async_set("binary_sensor.is_light", STATE_ON)
|
||||
hass.states.async_set("binary_sensor.is_light", STATE_OFF)
|
||||
@ -2197,9 +2228,13 @@ async def test_stream_consumer_stop_processing(recorder_mock, hass, hass_ws_clie
|
||||
|
||||
# Check our listener got unsubscribed because
|
||||
# the queue got full and the overload safety tripped
|
||||
assert hass.bus.async_listeners() == after_ws_created_listeners
|
||||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) == listeners_without_writes(after_ws_created_listeners)
|
||||
await websocket_client.close()
|
||||
assert hass.bus.async_listeners() == init_listeners
|
||||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) == listeners_without_writes(init_listeners)
|
||||
|
||||
|
||||
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
|
||||
@ -2332,7 +2367,9 @@ async def test_subscribe_all_entities_are_continuous(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check our listener got unsubscribed
|
||||
assert hass.bus.async_listeners() == init_listeners
|
||||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) == listeners_without_writes(init_listeners)
|
||||
|
||||
|
||||
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
|
||||
@ -2494,7 +2531,9 @@ async def test_subscribe_entities_some_have_uom_multiple(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check our listener got unsubscribed
|
||||
assert hass.bus.async_listeners() == init_listeners
|
||||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) == listeners_without_writes(init_listeners)
|
||||
|
||||
|
||||
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
|
||||
@ -2608,7 +2647,9 @@ async def test_logbook_stream_ignores_forced_updates(
|
||||
assert msg["success"]
|
||||
|
||||
# Check our listener got unsubscribed
|
||||
assert hass.bus.async_listeners() == init_listeners
|
||||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) == listeners_without_writes(init_listeners)
|
||||
|
||||
|
||||
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
|
||||
@ -2703,4 +2744,6 @@ async def test_subscribe_all_entities_are_continuous_with_device(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check our listener got unsubscribed
|
||||
assert hass.bus.async_listeners() == init_listeners
|
||||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) == listeners_without_writes(init_listeners)
|
||||
|
@ -15,10 +15,15 @@ from homeassistant.const import (
|
||||
SERVICE_RELOAD,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from tests.common import async_fire_time_changed, get_fixture_path
|
||||
from tests.common import (
|
||||
assert_setup_component,
|
||||
async_fire_time_changed,
|
||||
get_fixture_path,
|
||||
)
|
||||
|
||||
|
||||
@respx.mock
|
||||
@ -399,3 +404,17 @@ async def test_multiple_rest_endpoints(hass):
|
||||
assert hass.states.get("sensor.json_date_time").state == "07:11:08 PM"
|
||||
assert hass.states.get("sensor.json_time").state == "07:11:39 PM"
|
||||
assert hass.states.get("binary_sensor.binary_sensor").state == "on"
|
||||
|
||||
|
||||
async def test_empty_config(hass: HomeAssistant) -> None:
|
||||
"""Test setup with empty configuration.
|
||||
|
||||
For example (with rest.yaml an empty file):
|
||||
rest: !include rest.yaml
|
||||
"""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
{DOMAIN: {}},
|
||||
)
|
||||
assert_setup_component(0, DOMAIN)
|
||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.scrape.sensor import SCAN_INTERVAL
|
||||
from homeassistant.components.scrape.sensor import DEFAULT_SCAN_INTERVAL
|
||||
from homeassistant.components.sensor import (
|
||||
CONF_STATE_CLASS,
|
||||
SensorDeviceClass,
|
||||
@ -189,7 +189,7 @@ async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None:
|
||||
assert state.state == "Current Version: 2021.12.10"
|
||||
|
||||
mocker.payload = "test_scrape_sensor_no_data"
|
||||
async_fire_time_changed(hass, datetime.utcnow() + SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass, datetime.utcnow() + DEFAULT_SCAN_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.ha_version")
|
||||
|
@ -63,6 +63,42 @@ async def test_flow_ssdp(hass: HomeAssistant):
|
||||
CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN,
|
||||
CONFIG_ENTRY_LOCATION: TEST_LOCATION,
|
||||
CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS,
|
||||
CONFIG_ENTRY_HOST: TEST_HOST,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"ssdp_instant_discovery",
|
||||
"mock_setup_entry",
|
||||
"mock_get_source_ip",
|
||||
"mock_mac_address_from_host",
|
||||
)
|
||||
async def test_flow_ssdp_ignore(hass: HomeAssistant):
|
||||
"""Test config flow: discovered + ignore through ssdp."""
|
||||
# Discovered via step ssdp.
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_SSDP},
|
||||
data=TEST_DISCOVERY,
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "ssdp_confirm"
|
||||
|
||||
# Ignore entry.
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IGNORE},
|
||||
data={"unique_id": TEST_USN, "title": TEST_FRIENDLY_NAME},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == TEST_FRIENDLY_NAME
|
||||
assert result["data"] == {
|
||||
CONFIG_ENTRY_ST: TEST_ST,
|
||||
CONFIG_ENTRY_UDN: TEST_UDN,
|
||||
CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN,
|
||||
CONFIG_ENTRY_LOCATION: TEST_LOCATION,
|
||||
CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS,
|
||||
CONFIG_ENTRY_HOST: TEST_HOST,
|
||||
}
|
||||
|
||||
|
||||
@ -138,6 +174,7 @@ async def test_flow_ssdp_no_mac_address(hass: HomeAssistant):
|
||||
CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN,
|
||||
CONFIG_ENTRY_LOCATION: TEST_LOCATION,
|
||||
CONFIG_ENTRY_MAC_ADDRESS: None,
|
||||
CONFIG_ENTRY_HOST: TEST_HOST,
|
||||
}
|
||||
|
||||
|
||||
@ -382,6 +419,7 @@ async def test_flow_user(hass: HomeAssistant):
|
||||
CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN,
|
||||
CONFIG_ENTRY_LOCATION: TEST_LOCATION,
|
||||
CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS,
|
||||
CONFIG_ENTRY_HOST: TEST_HOST,
|
||||
}
|
||||
|
||||
|
||||
|
@ -819,6 +819,24 @@ async def test_info_from_service_with_link_local_address_first(hass):
|
||||
assert info.host == "192.168.66.12"
|
||||
|
||||
|
||||
async def test_info_from_service_with_unspecified_address_first(hass):
|
||||
"""Test that the unspecified address is ignored."""
|
||||
service_type = "_test._tcp.local."
|
||||
service_info = get_service_info_mock(service_type, f"test.{service_type}")
|
||||
service_info.addresses = ["0.0.0.0", "192.168.66.12"]
|
||||
info = zeroconf.info_from_service(service_info)
|
||||
assert info.host == "192.168.66.12"
|
||||
|
||||
|
||||
async def test_info_from_service_with_unspecified_address_only(hass):
|
||||
"""Test that the unspecified address is ignored."""
|
||||
service_type = "_test._tcp.local."
|
||||
service_info = get_service_info_mock(service_type, f"test.{service_type}")
|
||||
service_info.addresses = ["0.0.0.0"]
|
||||
info = zeroconf.info_from_service(service_info)
|
||||
assert info is None
|
||||
|
||||
|
||||
async def test_info_from_service_with_link_local_address_second(hass):
|
||||
"""Test that the link local address is ignored."""
|
||||
service_type = "_test._tcp.local."
|
||||
|
@ -290,7 +290,7 @@ async def test_action(hass, device_ias, device_inovelli):
|
||||
"domain": DOMAIN,
|
||||
"device_id": inovelli_reg_device.id,
|
||||
"type": "issue_individual_led_effect",
|
||||
"effect_type": "Open_Close",
|
||||
"effect_type": "Falling",
|
||||
"led_number": 1,
|
||||
"duration": 5,
|
||||
"level": 10,
|
||||
|
@ -242,7 +242,9 @@ async def eWeLink_light(hass, zigpy_device_mock, zha_device_joined):
|
||||
color_cluster = zigpy_device.endpoints[1].light_color
|
||||
color_cluster.PLUGGED_ATTR_READS = {
|
||||
"color_capabilities": lighting.Color.ColorCapabilities.Color_temperature
|
||||
| lighting.Color.ColorCapabilities.XY_attributes
|
||||
| lighting.Color.ColorCapabilities.XY_attributes,
|
||||
"color_temp_physical_min": 0,
|
||||
"color_temp_physical_max": 0,
|
||||
}
|
||||
zha_device = await zha_device_joined(zigpy_device)
|
||||
zha_device.available = True
|
||||
@ -1192,6 +1194,8 @@ async def test_transitions(
|
||||
assert eWeLink_state.state == STATE_ON
|
||||
assert eWeLink_state.attributes["color_temp"] == 235
|
||||
assert eWeLink_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
|
||||
assert eWeLink_state.attributes["min_mireds"] == 153
|
||||
assert eWeLink_state.attributes["max_mireds"] == 500
|
||||
|
||||
|
||||
async def async_test_on_off_from_light(hass, cluster, entity_id):
|
||||
|
@ -3287,6 +3287,7 @@ async def test_disallow_entry_reload_with_setup_in_progresss(hass, manager):
|
||||
async def test_reauth(hass):
|
||||
"""Test the async_reauth_helper."""
|
||||
entry = MockConfigEntry(title="test_title", domain="test")
|
||||
entry2 = MockConfigEntry(title="test_title", domain="test")
|
||||
|
||||
mock_setup_entry = AsyncMock(return_value=True)
|
||||
mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry))
|
||||
@ -3313,7 +3314,19 @@ async def test_reauth(hass):
|
||||
|
||||
assert mock_init.call_args.kwargs["data"]["extra_data"] == 1234
|
||||
|
||||
assert entry.entry_id != entry2.entry_id
|
||||
|
||||
# Check we can't start duplicate flows
|
||||
entry.async_start_reauth(hass, {"extra_context": "some_extra_context"})
|
||||
await hass.async_block_till_done()
|
||||
assert len(flows) == 1
|
||||
assert len(hass.config_entries.flow.async_progress()) == 1
|
||||
|
||||
# Check we can't start duplicate when the context context is different
|
||||
entry.async_start_reauth(hass, {"diff": "diff"})
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.config_entries.flow.async_progress()) == 1
|
||||
|
||||
# Check we can start a reauth for a different entry
|
||||
entry2.async_start_reauth(hass, {"extra_context": "some_extra_context"})
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.config_entries.flow.async_progress()) == 2
|
||||
|
Loading…
x
Reference in New Issue
Block a user