This commit is contained in:
Franck Nijhof 2022-11-08 17:41:05 +01:00 committed by GitHub
commit c757c9b99f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
85 changed files with 1891 additions and 366 deletions

View File

@ -7,13 +7,9 @@ from math import ceil
from typing import Any from typing import Any
from pyairvisual import CloudAPI, NodeSamba from pyairvisual import CloudAPI, NodeSamba
from pyairvisual.errors import ( from pyairvisual.cloud_api import InvalidKeyError, KeyExpiredError, UnauthorizedError
AirVisualError, from pyairvisual.errors import AirVisualError
InvalidKeyError, from pyairvisual.node import NodeProError
KeyExpiredError,
NodeProError,
UnauthorizedError,
)
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (

View File

@ -6,14 +6,14 @@ from collections.abc import Mapping
from typing import Any from typing import Any
from pyairvisual import CloudAPI, NodeSamba from pyairvisual import CloudAPI, NodeSamba
from pyairvisual.errors import ( from pyairvisual.cloud_api import (
AirVisualError,
InvalidKeyError, InvalidKeyError,
KeyExpiredError, KeyExpiredError,
NodeProError,
NotFoundError, NotFoundError,
UnauthorizedError, UnauthorizedError,
) )
from pyairvisual.errors import AirVisualError
from pyairvisual.node import NodeProError
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries

View File

@ -3,7 +3,7 @@
"name": "AirVisual", "name": "AirVisual",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airvisual", "documentation": "https://www.home-assistant.io/integrations/airvisual",
"requirements": ["pyairvisual==2022.07.0"], "requirements": ["pyairvisual==2022.11.1"],
"codeowners": ["@bachya"], "codeowners": ["@bachya"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pyairvisual", "pysmb"], "loggers": ["pyairvisual", "pysmb"],

View File

@ -6,9 +6,9 @@
"after_dependencies": ["hassio"], "after_dependencies": ["hassio"],
"quality_scale": "internal", "quality_scale": "internal",
"requirements": [ "requirements": [
"bleak==0.19.1", "bleak==0.19.2",
"bleak-retry-connector==2.8.2", "bleak-retry-connector==2.8.3",
"bluetooth-adapters==0.6.0", "bluetooth-adapters==0.7.0",
"bluetooth-auto-recovery==0.3.6", "bluetooth-auto-recovery==0.3.6",
"dbus-fast==1.61.1" "dbus-fast==1.61.1"
], ],

View File

@ -262,7 +262,11 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow):
self.config_entry.entry_id 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() sources = coordinator.source_map.values()
self.source_list = [item["title"] for item in sources] self.source_list = [item["title"] for item in sources]
return await self.async_step_user() return await self.async_step_user()

View File

@ -48,6 +48,9 @@
"ignored_sources": "List of ignored sources" "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."
} }
} }
} }

View File

@ -41,6 +41,9 @@
} }
}, },
"options": { "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": { "step": {
"user": { "user": {
"data": { "data": {

View File

@ -89,6 +89,7 @@ T = TypeVar(
class DeconzSensorDescriptionMixin(Generic[T]): class DeconzSensorDescriptionMixin(Generic[T]):
"""Required values when describing secondary sensor attributes.""" """Required values when describing secondary sensor attributes."""
supported_fn: Callable[[T], bool]
update_key: str update_key: str
value_fn: Callable[[T], datetime | StateType] value_fn: Callable[[T], datetime | StateType]
@ -105,6 +106,7 @@ class DeconzSensorDescription(SensorEntityDescription, DeconzSensorDescriptionMi
ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
DeconzSensorDescription[AirQuality]( DeconzSensorDescription[AirQuality](
key="air_quality", key="air_quality",
supported_fn=lambda device: device.air_quality is not None,
update_key="airquality", update_key="airquality",
value_fn=lambda device: device.air_quality, value_fn=lambda device: device.air_quality,
instance_check=AirQuality, instance_check=AirQuality,
@ -112,6 +114,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
), ),
DeconzSensorDescription[AirQuality]( DeconzSensorDescription[AirQuality](
key="air_quality_ppb", key="air_quality_ppb",
supported_fn=lambda device: device.air_quality_ppb is not None,
update_key="airqualityppb", update_key="airqualityppb",
value_fn=lambda device: device.air_quality_ppb, value_fn=lambda device: device.air_quality_ppb,
instance_check=AirQuality, instance_check=AirQuality,
@ -122,6 +125,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
), ),
DeconzSensorDescription[Consumption]( DeconzSensorDescription[Consumption](
key="consumption", key="consumption",
supported_fn=lambda device: device.consumption is not None,
update_key="consumption", update_key="consumption",
value_fn=lambda device: device.scaled_consumption, value_fn=lambda device: device.scaled_consumption,
instance_check=Consumption, instance_check=Consumption,
@ -131,6 +135,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
), ),
DeconzSensorDescription[Daylight]( DeconzSensorDescription[Daylight](
key="daylight_status", key="daylight_status",
supported_fn=lambda device: True,
update_key="status", update_key="status",
value_fn=lambda device: DAYLIGHT_STATUS[device.daylight_status], value_fn=lambda device: DAYLIGHT_STATUS[device.daylight_status],
instance_check=Daylight, instance_check=Daylight,
@ -139,12 +144,14 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
), ),
DeconzSensorDescription[GenericStatus]( DeconzSensorDescription[GenericStatus](
key="status", key="status",
supported_fn=lambda device: device.status is not None,
update_key="status", update_key="status",
value_fn=lambda device: device.status, value_fn=lambda device: device.status,
instance_check=GenericStatus, instance_check=GenericStatus,
), ),
DeconzSensorDescription[Humidity]( DeconzSensorDescription[Humidity](
key="humidity", key="humidity",
supported_fn=lambda device: device.humidity is not None,
update_key="humidity", update_key="humidity",
value_fn=lambda device: device.scaled_humidity, value_fn=lambda device: device.scaled_humidity,
instance_check=Humidity, instance_check=Humidity,
@ -154,6 +161,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
), ),
DeconzSensorDescription[LightLevel]( DeconzSensorDescription[LightLevel](
key="light_level", key="light_level",
supported_fn=lambda device: device.light_level is not None,
update_key="lightlevel", update_key="lightlevel",
value_fn=lambda device: device.scaled_light_level, value_fn=lambda device: device.scaled_light_level,
instance_check=LightLevel, instance_check=LightLevel,
@ -163,6 +171,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
), ),
DeconzSensorDescription[Power]( DeconzSensorDescription[Power](
key="power", key="power",
supported_fn=lambda device: device.power is not None,
update_key="power", update_key="power",
value_fn=lambda device: device.power, value_fn=lambda device: device.power,
instance_check=Power, instance_check=Power,
@ -172,6 +181,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
), ),
DeconzSensorDescription[Pressure]( DeconzSensorDescription[Pressure](
key="pressure", key="pressure",
supported_fn=lambda device: device.pressure is not None,
update_key="pressure", update_key="pressure",
value_fn=lambda device: device.pressure, value_fn=lambda device: device.pressure,
instance_check=Pressure, instance_check=Pressure,
@ -181,6 +191,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
), ),
DeconzSensorDescription[Temperature]( DeconzSensorDescription[Temperature](
key="temperature", key="temperature",
supported_fn=lambda device: device.temperature is not None,
update_key="temperature", update_key="temperature",
value_fn=lambda device: device.scaled_temperature, value_fn=lambda device: device.scaled_temperature,
instance_check=Temperature, instance_check=Temperature,
@ -190,6 +201,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
), ),
DeconzSensorDescription[Time]( DeconzSensorDescription[Time](
key="last_set", key="last_set",
supported_fn=lambda device: device.last_set is not None,
update_key="lastset", update_key="lastset",
value_fn=lambda device: dt_util.parse_datetime(device.last_set), value_fn=lambda device: dt_util.parse_datetime(device.last_set),
instance_check=Time, instance_check=Time,
@ -197,6 +209,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
), ),
DeconzSensorDescription[SensorResources]( DeconzSensorDescription[SensorResources](
key="battery", key="battery",
supported_fn=lambda device: device.battery is not None,
update_key="battery", update_key="battery",
value_fn=lambda device: device.battery, value_fn=lambda device: device.battery,
name_suffix="Battery", name_suffix="Battery",
@ -208,6 +221,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
), ),
DeconzSensorDescription[SensorResources]( DeconzSensorDescription[SensorResources](
key="internal_temperature", key="internal_temperature",
supported_fn=lambda device: device.internal_temperature is not None,
update_key="temperature", update_key="temperature",
value_fn=lambda device: device.internal_temperature, value_fn=lambda device: device.internal_temperature,
name_suffix="Temperature", name_suffix="Temperature",
@ -268,7 +282,7 @@ async def async_setup_entry(
continue continue
no_sensor_data = False no_sensor_data = False
if description.value_fn(sensor) is None: if not description.supported_fn(sensor):
no_sensor_data = True no_sensor_data = True
if description.instance_check is None: if description.instance_check is None:

View File

@ -3,7 +3,7 @@
"name": "DLNA Digital Media Renderer", "name": "DLNA Digital Media Renderer",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "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"], "dependencies": ["ssdp"],
"after_dependencies": ["media_source"], "after_dependencies": ["media_source"],
"ssdp": [ "ssdp": [

View File

@ -3,7 +3,7 @@
"name": "DLNA Digital Media Server", "name": "DLNA Digital Media Server",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dlna_dms", "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"], "dependencies": ["ssdp"],
"after_dependencies": ["media_source"], "after_dependencies": ["media_source"],
"ssdp": [ "ssdp": [

View File

@ -7,11 +7,11 @@ import logging
import re import re
from types import MappingProxyType from types import MappingProxyType
from typing import Any, cast from typing import Any, cast
from urllib.parse import urlparse
import async_timeout import async_timeout
from elkm1_lib.elements import Element from elkm1_lib.elements import Element
from elkm1_lib.elk import Elk from elkm1_lib.elk import Elk
from elkm1_lib.util import parse_url
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry 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]: def _host_validator(config: dict[str, str]) -> dict[str, str]:
"""Validate that a host is properly configured.""" """Validate that a host is properly configured."""
if config[CONF_HOST].startswith("elks://"): 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.""" """Set up Elk-M1 Control from a config entry."""
conf: MappingProxyType[str, Any] = entry.data 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"]) _LOGGER.debug("Setting up elkm1 %s", conf["host"])

View File

@ -4,7 +4,6 @@ from __future__ import annotations
import asyncio import asyncio
import logging import logging
from typing import Any from typing import Any
from urllib.parse import urlparse
from elkm1_lib.discovery import ElkSystem from elkm1_lib.discovery import ElkSystem
from elkm1_lib.elk import Elk from elkm1_lib.elk import Elk
@ -26,7 +25,7 @@ from homeassistant.helpers.typing import DiscoveryInfoType
from homeassistant.util import slugify from homeassistant.util import slugify
from homeassistant.util.network import is_ip_address 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 .const import CONF_AUTO_CONFIGURE, DISCOVER_SCAN_TIMEOUT, DOMAIN, LOGIN_TIMEOUT
from .discovery import ( from .discovery import (
_short_mac, _short_mac,
@ -170,7 +169,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
for entry in self._async_current_entries(include_ignore=False): for entry in self._async_current_entries(include_ignore=False):
if ( if (
entry.unique_id == mac 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): if async_update_entry_from_discovery(self.hass, entry, device):
self.hass.async_create_task( self.hass.async_create_task(
@ -214,7 +213,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
current_unique_ids = self._async_current_ids() current_unique_ids = self._async_current_ids()
current_hosts = { 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) for entry in self._async_current_entries(include_ignore=False)
} }
discovered_devices = await async_discover_devices( discovered_devices = await async_discover_devices(
@ -344,7 +343,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if self._url_already_configured(url): if self._url_already_configured(url):
return self.async_abort(reason="address_already_configured") return self.async_abort(reason="address_already_configured")
host = urlparse(url).hostname host = hostname_from_url(url)
_LOGGER.debug( _LOGGER.debug(
"Importing is trying to fill unique id from discovery for %s", host "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: def _url_already_configured(self, url: str) -> bool:
"""See if we already have a elkm1 matching user input configured.""" """See if we already have a elkm1 matching user input configured."""
existing_hosts = { existing_hosts = {
urlparse(entry.data[CONF_HOST]).hostname hostname_from_url(entry.data[CONF_HOST])
for entry in self._async_current_entries() 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): class InvalidAuth(exceptions.HomeAssistantError):

View File

@ -137,6 +137,7 @@ class ESPHomeClient(BaseBleakClient):
was_connected = self._is_connected was_connected = self._is_connected
self.services = BleakGATTServiceCollection() # type: ignore[no-untyped-call] self.services = BleakGATTServiceCollection() # type: ignore[no-untyped-call]
self._is_connected = False self._is_connected = False
self._notify_cancels.clear()
if self._disconnected_event: if self._disconnected_event:
self._disconnected_event.set() self._disconnected_event.set()
self._disconnected_event = None self._disconnected_event = None
@ -463,12 +464,20 @@ class ESPHomeClient(BaseBleakClient):
UUID or directly by the BleakGATTCharacteristic object representing it. UUID or directly by the BleakGATTCharacteristic object representing it.
callback (function): The function to be called on notification. 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( cancel_coro = await self._client.bluetooth_gatt_start_notify(
self._address_as_int, self._address_as_int,
characteristic.handle, ble_handle,
lambda handle, data: callback(data), lambda handle, data: callback(data),
) )
self._notify_cancels[characteristic.handle] = cancel_coro self._notify_cancels[ble_handle] = cancel_coro
@api_error_as_bleak_error @api_error_as_bleak_error
async def stop_notify( async def stop_notify(
@ -483,5 +492,7 @@ class ESPHomeClient(BaseBleakClient):
directly by the BleakGATTCharacteristic object representing it. directly by the BleakGATTCharacteristic object representing it.
""" """
characteristic = self._resolve_characteristic(char_specifier) characteristic = self._resolve_characteristic(char_specifier)
coro = self._notify_cancels.pop(characteristic.handle) # Do not raise KeyError if notifications are not enabled on this characteristic
await coro() # to be consistent with the behavior of the BlueZ backend
if coro := self._notify_cancels.pop(characteristic.handle, None):
await coro()

View File

@ -3,7 +3,7 @@
"name": "ESPHome", "name": "ESPHome",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/esphome", "documentation": "https://www.home-assistant.io/integrations/esphome",
"requirements": ["aioesphomeapi==11.4.2"], "requirements": ["aioesphomeapi==11.4.3"],
"zeroconf": ["_esphomelib._tcp.local."], "zeroconf": ["_esphomelib._tcp.local."],
"dhcp": [{ "registered_devices": true }], "dhcp": [{ "registered_devices": true }],
"codeowners": ["@OttoWinter", "@jesserockz"], "codeowners": ["@OttoWinter", "@jesserockz"],

View File

@ -2,7 +2,7 @@
"domain": "frontend", "domain": "frontend",
"name": "Home Assistant Frontend", "name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend", "documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": ["home-assistant-frontend==20221102.1"], "requirements": ["home-assistant-frontend==20221108.0"],
"dependencies": [ "dependencies": [
"api", "api",
"auth", "auth",

View File

@ -3,11 +3,12 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import Iterable
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from typing import Any 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.exceptions import ApiException
from gcal_sync.model import DateOrDatetime, Event from gcal_sync.model import DateOrDatetime, Event
from gcal_sync.store import ScopedCalendarStore from gcal_sync.store import ScopedCalendarStore
@ -196,21 +197,30 @@ async def async_setup_entry(
entity_registry.async_remove( entity_registry.async_remove(
entity_entry.entity_id, entity_entry.entity_id,
) )
request_template = SyncEventsRequest( coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator
calendar_id=calendar_id, if search := data.get(CONF_SEARCH):
search=data.get(CONF_SEARCH), coordinator = CalendarQueryUpdateCoordinator(
start_time=dt_util.now() + SYNC_EVENT_MIN_TIME, hass,
) calendar_service,
sync = CalendarEventSyncManager( data[CONF_NAME],
calendar_service, calendar_id,
store=ScopedCalendarStore(store, unique_id or entity_name), search,
request_template=request_template, )
) else:
coordinator = CalendarUpdateCoordinator( request_template = SyncEventsRequest(
hass, calendar_id=calendar_id,
sync, start_time=dt_util.now() + SYNC_EVENT_MIN_TIME,
data[CONF_NAME], )
) 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( entities.append(
GoogleCalendarEntity( GoogleCalendarEntity(
coordinator, coordinator,
@ -242,8 +252,8 @@ async def async_setup_entry(
) )
class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]): class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]):
"""Coordinator for calendar RPC calls.""" """Coordinator for calendar RPC calls that use an efficient sync."""
def __init__( def __init__(
self, self,
@ -251,7 +261,7 @@ class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]):
sync: CalendarEventSyncManager, sync: CalendarEventSyncManager,
name: str, name: str,
) -> None: ) -> None:
"""Create the Calendar event device.""" """Create the CalendarSyncUpdateCoordinator."""
super().__init__( super().__init__(
hass, hass,
_LOGGER, _LOGGER,
@ -271,6 +281,87 @@ class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]):
dt_util.DEFAULT_TIME_ZONE 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): class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
"""A calendar event entity.""" """A calendar event entity."""
@ -279,7 +370,7 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
def __init__( def __init__(
self, self,
coordinator: CalendarUpdateCoordinator, coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator,
calendar_id: str, calendar_id: str,
data: dict[str, Any], data: dict[str, Any],
entity_id: str, entity_id: str,
@ -352,14 +443,7 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
self, hass: HomeAssistant, start_date: datetime, end_date: datetime self, hass: HomeAssistant, start_date: datetime, end_date: datetime
) -> list[CalendarEvent]: ) -> list[CalendarEvent]:
"""Get all events in a specific time frame.""" """Get all events in a specific time frame."""
if not (timeline := self.coordinator.data): result_items = await self.coordinator.async_get_events(start_date, end_date)
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),
)
return [ return [
_get_calendar_event(event) _get_calendar_event(event)
for event in filter(self._event_filter, result_items) for event in filter(self._event_filter, result_items)
@ -367,14 +451,12 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
def _apply_coordinator_update(self) -> None: def _apply_coordinator_update(self) -> None:
"""Copy state from the coordinator to this entity.""" """Copy state from the coordinator to this entity."""
if (timeline := self.coordinator.data) and ( if api_event := next(
api_event := next( filter(
filter( self._event_filter,
self._event_filter, self.coordinator.upcoming or [],
timeline.active_after(dt_util.now()), ),
), None,
None,
)
): ):
self._event = _get_calendar_event(api_event) self._event = _get_calendar_event(api_event)
(self._event.summary, self._offset_value) = extract_offset( (self._event.summary, self._offset_value) = extract_offset(

View File

@ -4,7 +4,7 @@
"config_flow": true, "config_flow": true,
"dependencies": ["application_credentials"], "dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/calendar.google/", "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"], "codeowners": ["@allenporter"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["googleapiclient"] "loggers": ["googleapiclient"]

View File

@ -10,8 +10,10 @@ import os
from typing import Any, cast from typing import Any, cast
from aiohttp import web from aiohttp import web
from pyhap.characteristic import Characteristic
from pyhap.const import STANDALONE_AID from pyhap.const import STANDALONE_AID
from pyhap.loader import get_loader from pyhap.loader import get_loader
from pyhap.service import Service
import voluptuous as vol import voluptuous as vol
from zeroconf.asyncio import AsyncZeroconf from zeroconf.asyncio import AsyncZeroconf
@ -74,13 +76,7 @@ from . import ( # noqa: F401
type_switches, type_switches,
type_thermostats, type_thermostats,
) )
from .accessories import ( from .accessories import HomeAccessory, HomeBridge, HomeDriver, get_accessory
HomeAccessory,
HomeBridge,
HomeDriver,
HomeIIDManager,
get_accessory,
)
from .aidmanager import AccessoryAidStorage from .aidmanager import AccessoryAidStorage
from .const import ( from .const import (
ATTR_INTEGRATION, ATTR_INTEGRATION,
@ -139,7 +135,7 @@ STATUS_WAIT = 3
PORT_CLEANUP_CHECK_INTERVAL_SECS = 1 PORT_CLEANUP_CHECK_INTERVAL_SECS = 1
_HOMEKIT_CONFIG_UPDATE_TIME = ( _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.status = STATUS_READY
self.driver: HomeDriver | None = None self.driver: HomeDriver | None = None
self.bridge: HomeBridge | None = None self.bridge: HomeBridge | None = None
self._reset_lock = asyncio.Lock()
def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> None: def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> None:
"""Set up bridge and accessory driver.""" """Set up bridge and accessory driver."""
@ -548,7 +545,7 @@ class HomeKit:
async_zeroconf_instance=async_zeroconf_instance, async_zeroconf_instance=async_zeroconf_instance,
zeroconf_server=f"{uuid}-hap.local.", zeroconf_server=f"{uuid}-hap.local.",
loader=get_loader(), 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 # 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: async def async_reset_accessories(self, entity_ids: Iterable[str]) -> None:
"""Reset the accessory to load the latest configuration.""" """Reset the accessory to load the latest configuration."""
if not self.bridge: async with self._reset_lock:
await self.async_reset_accessories_in_accessory_mode(entity_ids) if not self.bridge:
return await self.async_reset_accessories_in_accessory_mode(entity_ids)
await self.async_reset_accessories_in_bridge_mode(entity_ids) return
await self.async_reset_accessories_in_bridge_mode(entity_ids)
async def _async_shutdown_accessory(self, accessory: HomeAccessory) -> None: async def _async_shutdown_accessory(self, accessory: HomeAccessory) -> None:
"""Shutdown an accessory.""" """Shutdown an accessory."""
assert self.driver is not None assert self.driver is not None
await accessory.stop() await accessory.stop()
# Deallocate the IIDs for the accessory # Deallocate the IIDs for the accessory
iid_manager = self.driver.iid_manager iid_manager = accessory.iid_manager
for service in accessory.services: services: list[Service] = accessory.services
iid_manager.remove_iid(iid_manager.remove_obj(service)) for service in services:
for char in service.characteristics: iid_manager.remove_obj(service)
iid_manager.remove_iid(iid_manager.remove_obj(char)) characteristics: list[Characteristic] = service.characteristics
for char in characteristics:
iid_manager.remove_obj(char)
async def async_reset_accessories_in_accessory_mode( async def async_reset_accessories_in_accessory_mode(
self, entity_ids: Iterable[str] self, entity_ids: Iterable[str]
@ -581,7 +581,6 @@ class HomeKit:
assert self.driver is not None assert self.driver is not None
acc = cast(HomeAccessory, self.driver.accessory) acc = cast(HomeAccessory, self.driver.accessory)
await self._async_shutdown_accessory(acc)
if acc.entity_id not in entity_ids: if acc.entity_id not in entity_ids:
return return
if not (state := self.hass.states.get(acc.entity_id)): 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 "The underlying entity %s disappeared during reset", acc.entity_id
) )
return return
await self._async_shutdown_accessory(acc)
if new_acc := self._async_create_single_accessory([state]): if new_acc := self._async_create_single_accessory([state]):
self.driver.accessory = new_acc self.driver.accessory = new_acc
self.hass.async_add_job(new_acc.run) self.hass.async_add_job(new_acc.run)

View File

@ -270,7 +270,7 @@ class HomeAccessory(Accessory): # type: ignore[misc]
driver=driver, driver=driver,
display_name=cleanup_name_for_homekit(name), display_name=cleanup_name_for_homekit(name),
aid=aid, aid=aid,
iid_manager=driver.iid_manager, iid_manager=HomeIIDManager(driver.iid_storage),
*args, *args,
**kwargs, **kwargs,
) )
@ -570,7 +570,7 @@ class HomeBridge(Bridge): # type: ignore[misc]
def __init__(self, hass: HomeAssistant, driver: HomeDriver, name: str) -> None: def __init__(self, hass: HomeAssistant, driver: HomeDriver, name: str) -> None:
"""Initialize a Bridge object.""" """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( self.set_info_service(
firmware_revision=format_version(__version__), firmware_revision=format_version(__version__),
manufacturer=MANUFACTURER, manufacturer=MANUFACTURER,
@ -603,7 +603,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
entry_id: str, entry_id: str,
bridge_name: str, bridge_name: str,
entry_title: str, entry_title: str,
iid_manager: HomeIIDManager, iid_storage: AccessoryIIDStorage,
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
"""Initialize a AccessoryDriver object.""" """Initialize a AccessoryDriver object."""
@ -612,7 +612,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
self._entry_id = entry_id self._entry_id = entry_id
self._bridge_name = bridge_name self._bridge_name = bridge_name
self._entry_title = entry_title self._entry_title = entry_title
self.iid_manager = iid_manager self.iid_storage = iid_storage
@pyhap_callback # type: ignore[misc] @pyhap_callback # type: ignore[misc]
def pair( def pair(

View File

@ -31,6 +31,8 @@ async def async_get_config_entry_diagnostics(
"options": dict(entry.options), "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 if not homekit.driver: # not started yet or startup failed
return data return data
driver: AccessoryDriver = homekit.driver driver: AccessoryDriver = homekit.driver

View File

@ -17,7 +17,7 @@ from homeassistant.helpers.storage import Store
from .util import get_iid_storage_filename_for_entry_id 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 IID_MANAGER_SAVE_DELAY = 2
ALLOCATIONS_KEY = "allocations" ALLOCATIONS_KEY = "allocations"
@ -26,6 +26,40 @@ IID_MIN = 1
IID_MAX = 18446744073709551615 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: class AccessoryIIDStorage:
""" """
Provide stable allocation of IIDs for the lifetime of an accessory. 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: def __init__(self, hass: HomeAssistant, entry_id: str) -> None:
"""Create a new iid store.""" """Create a new iid store."""
self.hass = hass self.hass = hass
self.allocations: dict[str, int] = {} self.allocations: dict[str, dict[str, int]] = {}
self.allocated_iids: list[int] = [] self.allocated_iids: dict[str, list[int]] = {}
self.entry_id = entry_id self.entry_id = entry_id
self.store: Store | None = None self.store: IIDStorage | None = None
async def async_initialize(self) -> None: async def async_initialize(self) -> None:
"""Load the latest IID data.""" """Load the latest IID data."""
iid_store = get_iid_storage_filename_for_entry_id(self.entry_id) 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()): if not (raw_storage := await self.store.async_load()):
# There is no data about iid allocations yet # There is no data about iid allocations yet
@ -53,7 +87,8 @@ class AccessoryIIDStorage:
assert isinstance(raw_storage, dict) assert isinstance(raw_storage, dict)
self.allocations = raw_storage.get(ALLOCATIONS_KEY, {}) 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( def get_or_allocate_iid(
self, self,
@ -68,16 +103,25 @@ class AccessoryIIDStorage:
char_hap_type: str | None = uuid_to_hap_type(char_uuid) if char_uuid else None 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 must be a string since we are saving it to JSON
allocation_key = ( 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 ""}' f'{char_hap_type or ""}_{char_unique_id or ""}'
) )
if allocation_key in self.allocations: # AID must be a string since JSON keys cannot be int
return self.allocations[allocation_key] aid_str = str(aid)
next_iid = self.allocated_iids[-1] + 1 if self.allocated_iids else 1 accessory_allocation = self.allocations.setdefault(aid_str, {})
self.allocations[allocation_key] = next_iid accessory_allocated_iids = self.allocated_iids.setdefault(aid_str, [1])
self.allocated_iids.append(next_iid) 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() self._async_schedule_save()
return next_iid return allocated_iid
@callback @callback
def _async_schedule_save(self) -> None: def _async_schedule_save(self) -> None:
@ -91,6 +135,6 @@ class AccessoryIIDStorage:
return await self.store.async_save(self._data_to_save()) return await self.store.async_save(self._data_to_save())
@callback @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 data of entity map to store in a file."""
return {ALLOCATIONS_KEY: self.allocations} return {ALLOCATIONS_KEY: self.allocations}

View File

@ -306,7 +306,7 @@ class Thermostat(HomeAccessory):
if attributes.get(ATTR_HVAC_ACTION) is not None: if attributes.get(ATTR_HVAC_ACTION) is not None:
self.fan_chars.append(CHAR_CURRENT_FAN_STATE) self.fan_chars.append(CHAR_CURRENT_FAN_STATE)
serv_fan = self.add_preload_service(SERV_FANV2, self.fan_chars) 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( self.char_active = serv_fan.configure_char(
CHAR_ACTIVE, value=1, setter_callback=self._set_fan_active CHAR_ACTIVE, value=1, setter_callback=self._set_fan_active
) )

View File

@ -3,7 +3,7 @@
"name": "HomeKit Controller", "name": "HomeKit Controller",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller", "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."], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }],
"dependencies": ["bluetooth", "zeroconf"], "dependencies": ["bluetooth", "zeroconf"],

View File

@ -4,7 +4,7 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/iaqualink/", "documentation": "https://www.home-assistant.io/integrations/iaqualink/",
"codeowners": ["@flz"], "codeowners": ["@flz"],
"requirements": ["iaqualink==0.5.0"], "requirements": ["iaqualink==0.5.0", "h2==4.1.0"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["iaqualink"] "loggers": ["iaqualink"]
} }

View File

@ -2,7 +2,7 @@
"domain": "lidarr", "domain": "lidarr",
"name": "Lidarr", "name": "Lidarr",
"documentation": "https://www.home-assistant.io/integrations/lidarr", "documentation": "https://www.home-assistant.io/integrations/lidarr",
"requirements": ["aiopyarr==22.10.0"], "requirements": ["aiopyarr==22.11.0"],
"codeowners": ["@tkdrob"], "codeowners": ["@tkdrob"],
"config_flow": true, "config_flow": true,
"iot_class": "local_polling", "iot_class": "local_polling",

View File

@ -14,8 +14,11 @@ from awesomeversion import AwesomeVersion
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT,
ATTR_COLOR_NAME,
ATTR_COLOR_TEMP_KELVIN, ATTR_COLOR_TEMP_KELVIN,
ATTR_HS_COLOR, ATTR_HS_COLOR,
ATTR_KELVIN,
ATTR_RGB_COLOR, ATTR_RGB_COLOR,
ATTR_XY_COLOR, ATTR_XY_COLOR,
) )
@ -24,7 +27,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
import homeassistant.util.color as color_util 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") 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 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: if ATTR_HS_COLOR in kwargs:
hue, saturation = kwargs[ATTR_HS_COLOR] hue, saturation = kwargs[ATTR_HS_COLOR]
elif ATTR_RGB_COLOR in kwargs: 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) saturation = int(saturation / 100 * 65535)
kelvin = 3500 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: if ATTR_COLOR_TEMP_KELVIN in kwargs:
kelvin = kwargs.pop(ATTR_COLOR_TEMP_KELVIN) kelvin = kwargs.pop(ATTR_COLOR_TEMP_KELVIN)
saturation = 0 saturation = 0
@ -100,6 +121,9 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] |
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) 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] hsbk = [hue, saturation, brightness, kelvin]
return None if hsbk == [None] * 4 else hsbk return None if hsbk == [None] * 4 else hsbk

View File

@ -3,7 +3,7 @@
"name": "Litter-Robot", "name": "Litter-Robot",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/litterrobot", "documentation": "https://www.home-assistant.io/integrations/litterrobot",
"requirements": ["pylitterbot==2022.10.2"], "requirements": ["pylitterbot==2022.11.0"],
"codeowners": ["@natekspencer", "@tkdrob"], "codeowners": ["@natekspencer", "@tkdrob"],
"dhcp": [{ "hostname": "litter-robot4" }], "dhcp": [{ "hostname": "litter-robot4" }],
"iot_class": "cloud_push", "iot_class": "cloud_push",

View File

@ -2,7 +2,7 @@
"domain": "netatmo", "domain": "netatmo",
"name": "Netatmo", "name": "Netatmo",
"documentation": "https://www.home-assistant.io/integrations/netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo",
"requirements": ["pyatmo==7.3.0"], "requirements": ["pyatmo==7.4.0"],
"after_dependencies": ["cloud", "media_source"], "after_dependencies": ["cloud", "media_source"],
"dependencies": ["application_credentials", "webhook"], "dependencies": ["application_credentials", "webhook"],
"codeowners": ["@cgtobi"], "codeowners": ["@cgtobi"],

View File

@ -80,6 +80,11 @@ class NexiaThermostatEntity(NexiaEntity):
self.hass, f"{SIGNAL_THERMOSTAT_UPDATE}-{self._thermostat.thermostat_id}" 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): class NexiaThermostatZoneEntity(NexiaThermostatEntity):
"""Base class for nexia devices attached to a thermostat.""" """Base class for nexia devices attached to a thermostat."""

View File

@ -1,7 +1,7 @@
{ {
"domain": "nexia", "domain": "nexia",
"name": "Nexia/American Standard/Trane", "name": "Nexia/American Standard/Trane",
"requirements": ["nexia==2.0.5"], "requirements": ["nexia==2.0.6"],
"codeowners": ["@bdraco"], "codeowners": ["@bdraco"],
"documentation": "https://www.home-assistant.io/integrations/nexia", "documentation": "https://www.home-assistant.io/integrations/nexia",
"config_flow": true, "config_flow": true,

View File

@ -8,7 +8,7 @@
"manufacturer_id": 220 "manufacturer_id": 220
} }
], ],
"requirements": ["oralb-ble==0.10.0"], "requirements": ["oralb-ble==0.13.0"],
"dependencies": ["bluetooth"], "dependencies": ["bluetooth"],
"codeowners": ["@bdraco"], "codeowners": ["@bdraco"],
"iot_class": "local_push" "iot_class": "local_push"

View File

@ -5,6 +5,7 @@ from typing import TypedDict
from p1monitor import ( from p1monitor import (
P1Monitor, P1Monitor,
P1MonitorConnectionError,
P1MonitorNoDataError, P1MonitorNoDataError,
Phases, Phases,
Settings, Settings,
@ -101,8 +102,8 @@ class P1MonitorDataUpdateCoordinator(DataUpdateCoordinator[P1MonitorData]):
try: try:
data[SERVICE_WATERMETER] = await self.p1monitor.watermeter() data[SERVICE_WATERMETER] = await self.p1monitor.watermeter()
self.has_water_meter = True self.has_water_meter = True
except P1MonitorNoDataError: except (P1MonitorNoDataError, P1MonitorConnectionError):
LOGGER.debug("No watermeter data received from P1 Monitor") LOGGER.debug("No water meter data received from P1 Monitor")
if self.has_water_meter is None: if self.has_water_meter is None:
self.has_water_meter = False self.has_water_meter = False

View File

@ -3,7 +3,7 @@
"name": "P1 Monitor", "name": "P1 Monitor",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/p1_monitor", "documentation": "https://www.home-assistant.io/integrations/p1_monitor",
"requirements": ["p1monitor==2.1.0"], "requirements": ["p1monitor==2.1.1"],
"codeowners": ["@klaasnicolaas"], "codeowners": ["@klaasnicolaas"],
"quality_scale": "platinum", "quality_scale": "platinum",
"iot_class": "local_polling", "iot_class": "local_polling",

View File

@ -2,7 +2,7 @@
"domain": "plugwise", "domain": "plugwise",
"name": "Plugwise", "name": "Plugwise",
"documentation": "https://www.home-assistant.io/integrations/plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise",
"requirements": ["plugwise==0.25.3"], "requirements": ["plugwise==0.25.7"],
"codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"],
"zeroconf": ["_plugwise._tcp.local."], "zeroconf": ["_plugwise._tcp.local."],
"config_flow": true, "config_flow": true,

View File

@ -2,7 +2,7 @@
"domain": "radarr", "domain": "radarr",
"name": "Radarr", "name": "Radarr",
"documentation": "https://www.home-assistant.io/integrations/radarr", "documentation": "https://www.home-assistant.io/integrations/radarr",
"requirements": ["aiopyarr==22.10.0"], "requirements": ["aiopyarr==22.11.0"],
"codeowners": ["@tkdrob"], "codeowners": ["@tkdrob"],
"config_flow": true, "config_flow": true,
"iot_class": "local_polling", "iot_class": "local_polling",

View File

@ -237,6 +237,7 @@ async def async_setup_entry(
# Add switches to control restrictions: # Add switches to control restrictions:
for description in RESTRICTIONS_SWITCH_DESCRIPTIONS: for description in RESTRICTIONS_SWITCH_DESCRIPTIONS:
coordinator = data.coordinators[description.api_category]
if not key_exists(coordinator.data, description.data_key): if not key_exists(coordinator.data, description.data_key):
continue continue
entities.append(RainMachineRestrictionSwitch(entry, data, description)) entities.append(RainMachineRestrictionSwitch(entry, data, description))

View File

@ -89,6 +89,13 @@ COMBINED_SCHEMA = vol.Schema(
) )
CONFIG_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, extra=vol.ALLOW_EXTRA,
) )

View File

@ -7,7 +7,7 @@
"samsungctl[websocket]==0.7.1", "samsungctl[websocket]==0.7.1",
"samsungtvws[async,encrypted]==2.5.0", "samsungtvws[async,encrypted]==2.5.0",
"wakeonlan==2.1.0", "wakeonlan==2.1.0",
"async-upnp-client==0.32.1" "async-upnp-client==0.32.2"
], ],
"ssdp": [ "ssdp": [
{ {

View File

@ -23,6 +23,7 @@ from homeassistant.const import (
CONF_NAME, CONF_NAME,
CONF_PASSWORD, CONF_PASSWORD,
CONF_RESOURCE, CONF_RESOURCE,
CONF_SCAN_INTERVAL,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_OF_MEASUREMENT,
CONF_USERNAME, CONF_USERNAME,
@ -43,7 +44,7 @@ from .coordinator import ScrapeCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=10) DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
CONF_ATTR = "attribute" CONF_ATTR = "attribute"
CONF_SELECT = "select" CONF_SELECT = "select"
@ -111,7 +112,8 @@ async def async_setup_platform(
rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl) 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() await coordinator.async_refresh()
if coordinator.data is None: if coordinator.data is None:
raise PlatformNotReady raise PlatformNotReady

View File

@ -9,6 +9,7 @@ from aioshelly.block_device import Block
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry, entity, entity_registry from homeassistant.helpers import device_registry, entity, entity_registry
@ -615,6 +616,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
"""Initialize the sleeping sensor.""" """Initialize the sleeping sensor."""
self.sensors = sensors self.sensors = sensors
self.last_state: StateType = None self.last_state: StateType = None
self.last_unit: str | None = None
self.coordinator = coordinator self.coordinator = coordinator
self.attribute = attribute self.attribute = attribute
self.block: Block | None = block # type: ignore[assignment] self.block: Block | None = block # type: ignore[assignment]
@ -644,6 +646,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
if last_state is not None: if last_state is not None:
self.last_state = last_state.state self.last_state = last_state.state
self.last_unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
@callback @callback
def _update_callback(self) -> None: def _update_callback(self) -> None:
@ -696,6 +699,7 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity, RestoreEntity):
) -> None: ) -> None:
"""Initialize the sleeping sensor.""" """Initialize the sleeping sensor."""
self.last_state: StateType = None self.last_state: StateType = None
self.last_unit: str | None = None
self.coordinator = coordinator self.coordinator = coordinator
self.key = key self.key = key
self.attribute = attribute self.attribute = attribute
@ -725,3 +729,4 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity, RestoreEntity):
if last_state is not None: if last_state is not None:
self.last_state = last_state.state self.last_state = last_state.state
self.last_unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)

View File

@ -47,12 +47,7 @@ from .entity import (
async_setup_entry_rest, async_setup_entry_rest,
async_setup_entry_rpc, async_setup_entry_rpc,
) )
from .utils import ( from .utils import get_device_entry_gen, get_device_uptime
get_device_entry_gen,
get_device_uptime,
is_rpc_device_externally_powered,
temperature_unit,
)
@dataclass @dataclass
@ -84,7 +79,7 @@ SENSORS: Final = {
("device", "deviceTemp"): BlockSensorDescription( ("device", "deviceTemp"): BlockSensorDescription(
key="device|deviceTemp", key="device|deviceTemp",
name="Device Temperature", name="Device Temperature",
unit_fn=temperature_unit, native_unit_of_measurement=TEMP_CELSIUS,
value=lambda value: round(value, 1), value=lambda value: round(value, 1),
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
@ -145,7 +140,7 @@ SENSORS: Final = {
key="emeter|powerFactor", key="emeter|powerFactor",
name="Power Factor", name="Power Factor",
native_unit_of_measurement=PERCENTAGE, 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, device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
@ -226,7 +221,7 @@ SENSORS: Final = {
("sensor", "temp"): BlockSensorDescription( ("sensor", "temp"): BlockSensorDescription(
key="sensor|temp", key="sensor|temp",
name="Temperature", name="Temperature",
unit_fn=temperature_unit, native_unit_of_measurement=TEMP_CELSIUS,
value=lambda value: round(value, 1), value=lambda value: round(value, 1),
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
@ -235,7 +230,7 @@ SENSORS: Final = {
("sensor", "extTemp"): BlockSensorDescription( ("sensor", "extTemp"): BlockSensorDescription(
key="sensor|extTemp", key="sensor|extTemp",
name="Temperature", name="Temperature",
unit_fn=temperature_unit, native_unit_of_measurement=TEMP_CELSIUS,
value=lambda value: round(value, 1), value=lambda value: round(value, 1),
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
@ -407,7 +402,6 @@ RPC_SENSORS: Final = {
value=lambda status, _: status["percent"], value=lambda status, _: status["percent"],
device_class=SensorDeviceClass.BATTERY, device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
removal_condition=is_rpc_device_externally_powered,
entity_registry_enabled_default=True, entity_registry_enabled_default=True,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
), ),
@ -505,8 +499,6 @@ class BlockSensor(ShellyBlockAttributeEntity, SensorEntity):
super().__init__(coordinator, block, attribute, description) super().__init__(coordinator, block, attribute, description)
self._attr_native_unit_of_measurement = description.native_unit_of_measurement 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 @property
def native_value(self) -> StateType: def native_value(self) -> StateType:
@ -553,10 +545,6 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity):
"""Initialize the sleeping sensor.""" """Initialize the sleeping sensor."""
super().__init__(coordinator, block, attribute, description, entry, sensors) 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 @property
def native_value(self) -> StateType: def native_value(self) -> StateType:
"""Return value of sensor.""" """Return value of sensor."""
@ -565,6 +553,14 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity):
return self.last_state 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): class RpcSleepingSensor(ShellySleepingRpcAttributeEntity, SensorEntity):
"""Represent a RPC sleeping sensor.""" """Represent a RPC sleeping sensor."""
@ -578,3 +574,11 @@ class RpcSleepingSensor(ShellySleepingRpcAttributeEntity, SensorEntity):
return self.attribute_value return self.attribute_value
return self.last_state 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

View File

@ -5,13 +5,13 @@ from datetime import datetime, timedelta
from typing import Any, cast from typing import Any, cast
from aiohttp.web import Request, WebSocketResponse 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.const import MODEL_NAMES
from aioshelly.rpc_device import RpcDevice, WsServer from aioshelly.rpc_device import RpcDevice, WsServer
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.config_entries import ConfigEntry 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.core import HomeAssistant, callback
from homeassistant.helpers import device_registry, entity_registry, singleton from homeassistant.helpers import device_registry, entity_registry, singleton
from homeassistant.helpers.typing import EventType from homeassistant.helpers.typing import EventType
@ -43,13 +43,6 @@ def async_remove_shelly_entity(
entity_reg.async_remove(entity_id) 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: def get_block_device_name(device: BlockDevice) -> str:
"""Naming for device.""" """Naming for device."""
return cast(str, device.settings["name"] or device.settings["device"]["hostname"]) 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") 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]]: def get_rpc_input_triggers(device: RpcDevice) -> list[tuple[str, str]]:
"""Return list of input triggers for RPC device.""" """Return list of input triggers for RPC device."""
triggers = [] triggers = []

View File

@ -3,7 +3,7 @@
"name": "Sonarr", "name": "Sonarr",
"documentation": "https://www.home-assistant.io/integrations/sonarr", "documentation": "https://www.home-assistant.io/integrations/sonarr",
"codeowners": ["@ctalkington"], "codeowners": ["@ctalkington"],
"requirements": ["aiopyarr==22.10.0"], "requirements": ["aiopyarr==22.11.0"],
"config_flow": true, "config_flow": true,
"quality_scale": "silver", "quality_scale": "silver",
"iot_class": "local_polling", "iot_class": "local_polling",

View File

@ -2,7 +2,7 @@
"domain": "ssdp", "domain": "ssdp",
"name": "Simple Service Discovery Protocol (SSDP)", "name": "Simple Service Discovery Protocol (SSDP)",
"documentation": "https://www.home-assistant.io/integrations/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"], "dependencies": ["network"],
"after_dependencies": ["zeroconf"], "after_dependencies": ["zeroconf"],
"codeowners": [], "codeowners": [],

View File

@ -53,6 +53,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try: try:
await tibber_connection.update_info() await tibber_connection.update_info()
if not tibber_connection.name:
raise ConfigEntryNotReady("Could not fetch Tibber data.")
except asyncio.TimeoutError as err: except asyncio.TimeoutError as err:
raise ConfigEntryNotReady from err raise ConfigEntryNotReady from err
except aiohttp.ClientError as err: except aiohttp.ClientError as err:

View File

@ -20,6 +20,7 @@ from .const import (
CONFIG_ENTRY_ST, CONFIG_ENTRY_ST,
CONFIG_ENTRY_UDN, CONFIG_ENTRY_UDN,
DOMAIN, DOMAIN,
DOMAIN_DISCOVERIES,
LOGGER, LOGGER,
ST_IGD_V1, ST_IGD_V1,
ST_IGD_V2, 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, hass: HomeAssistant,
) -> list[ssdp.SsdpServiceInfo]: ) -> list[ssdp.SsdpServiceInfo]:
"""Discovery IGD devices.""" """Discovery IGD devices."""
@ -79,9 +80,19 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
# - ssdp(discovery_info) --> ssdp_confirm(None) --> ssdp_confirm({}) --> create_entry() # - ssdp(discovery_info) --> ssdp_confirm(None) --> ssdp_confirm({}) --> create_entry()
# - user(None): scan --> user({...}) --> create_entry() # - user(None): scan --> user({...}) --> create_entry()
def __init__(self) -> None: @property
"""Initialize the UPnP/IGD config flow.""" def _discoveries(self) -> dict[str, SsdpServiceInfo]:
self._discoveries: list[SsdpServiceInfo] | None = None """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( async def async_step_user(
self, user_input: Mapping[str, Any] | None = None self, user_input: Mapping[str, Any] | None = None
@ -95,7 +106,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
discovery = next( discovery = next(
iter( iter(
discovery discovery
for discovery in self._discoveries for discovery in self._discoveries.values()
if discovery.ssdp_usn == user_input["unique_id"] 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) return await self._async_create_entry_from_discovery(discovery)
# Discover devices. # 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. # Store discoveries which have not been configured.
current_unique_ids = { current_unique_ids = {
entry.unique_id for entry in self._async_current_entries() entry.unique_id for entry in self._async_current_entries()
} }
self._discoveries = [ for discovery in discoveries:
discovery
for discovery in discoveries
if ( if (
_is_complete_discovery(discovery) _is_complete_discovery(discovery)
and _is_igd_device(discovery) and _is_igd_device(discovery)
and discovery.ssdp_usn not in current_unique_ids and discovery.ssdp_usn not in current_unique_ids
) ):
] self._add_discovery(discovery)
# Ensure anything to add. # Ensure anything to add.
if not self._discoveries: if not self._discoveries:
@ -128,7 +137,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
vol.Required("unique_id"): vol.In( vol.Required("unique_id"): vol.In(
{ {
discovery.ssdp_usn: _friendly_name_from_discovery(discovery) 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) mac_address = await _async_mac_address_from_discovery(self.hass, discovery_info)
host = discovery_info.ssdp_headers["_host"] host = discovery_info.ssdp_headers["_host"]
self._abort_if_unique_id_configured( 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. # The location is stored in the config entry such that when the location changes, the entry is reloaded.
updates={ updates={
CONFIG_ENTRY_MAC_ADDRESS: mac_address, CONFIG_ENTRY_MAC_ADDRESS: mac_address,
CONFIG_ENTRY_LOCATION: discovery_info.ssdp_location, CONFIG_ENTRY_LOCATION: discovery_info.ssdp_location,
CONFIG_ENTRY_HOST: host, 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") return self.async_abort(reason="config_entry_updated")
# Store discovery. # Store discovery.
self._discoveries = [discovery_info] self._add_discovery(discovery_info)
# Ensure user recognizable. # Ensure user recognizable.
self.context["title_placeholders"] = { self.context["title_placeholders"] = {
@ -221,10 +231,27 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is None: if user_input is None:
return self.async_show_form(step_id="ssdp_confirm") return self.async_show_form(step_id="ssdp_confirm")
assert self._discoveries assert self.unique_id
discovery = self._discoveries[0] discovery = self._remove_discovery(self.unique_id)
return await self._async_create_entry_from_discovery(discovery) 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( async def _async_create_entry_from_discovery(
self, self,
discovery: SsdpServiceInfo, 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_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN],
CONFIG_ENTRY_LOCATION: discovery.ssdp_location, CONFIG_ENTRY_LOCATION: discovery.ssdp_location,
CONFIG_ENTRY_MAC_ADDRESS: mac_address, CONFIG_ENTRY_MAC_ADDRESS: mac_address,
CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"],
} }
return self.async_create_entry(title=title, data=data) return self.async_create_entry(title=title, data=data)

View File

@ -7,6 +7,7 @@ from homeassistant.const import TIME_SECONDS
LOGGER = logging.getLogger(__package__) LOGGER = logging.getLogger(__package__)
DOMAIN = "upnp" DOMAIN = "upnp"
DOMAIN_DISCOVERIES = "discoveries"
BYTES_RECEIVED = "bytes_received" BYTES_RECEIVED = "bytes_received"
BYTES_SENT = "bytes_sent" BYTES_SENT = "bytes_sent"
PACKETS_RECEIVED = "packets_received" PACKETS_RECEIVED = "packets_received"

View File

@ -3,7 +3,7 @@
"name": "UPnP/IGD", "name": "UPnP/IGD",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/upnp", "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"], "dependencies": ["network", "ssdp"],
"codeowners": ["@StevenLooman"], "codeowners": ["@StevenLooman"],
"ssdp": [ "ssdp": [

View File

@ -3,7 +3,7 @@
"name": "Venstar", "name": "Venstar",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/venstar", "documentation": "https://www.home-assistant.io/integrations/venstar",
"requirements": ["venstarcolortouch==0.18"], "requirements": ["venstarcolortouch==0.19"],
"codeowners": ["@garbled1"], "codeowners": ["@garbled1"],
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["venstarcolortouch"] "loggers": ["venstarcolortouch"]

View File

@ -3,7 +3,7 @@
"name": "Xiaomi Gateway (Aqara)", "name": "Xiaomi Gateway (Aqara)",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara", "documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara",
"requirements": ["PyXiaomiGateway==0.14.1"], "requirements": ["PyXiaomiGateway==0.14.3"],
"after_dependencies": ["discovery"], "after_dependencies": ["discovery"],
"codeowners": ["@danielhiversen", "@syssi"], "codeowners": ["@danielhiversen", "@syssi"],
"zeroconf": ["_miio._udp.local."], "zeroconf": ["_miio._udp.local."],

View File

@ -2,7 +2,7 @@
"domain": "yeelight", "domain": "yeelight",
"name": "Yeelight", "name": "Yeelight",
"documentation": "https://www.home-assistant.io/integrations/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"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"],
"config_flow": true, "config_flow": true,
"dependencies": ["network"], "dependencies": ["network"],

View File

@ -552,12 +552,20 @@ def _first_non_link_local_address(
"""Return the first ipv6 or non-link local ipv4 address, preferring IPv4.""" """Return the first ipv6 or non-link local ipv4 address, preferring IPv4."""
for address in addresses: for address in addresses:
ip_addr = ip_address(address) 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) return str(ip_addr)
# If we didn't find a good IPv4 address, check for IPv6 addresses. # If we didn't find a good IPv4 address, check for IPv6 addresses.
for address in addresses: for address in addresses:
ip_addr = ip_address(address) 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 str(ip_addr)
return None return None

View File

@ -98,12 +98,26 @@ class ColorChannel(ZigbeeChannel):
@property @property
def min_mireds(self) -> int: def min_mireds(self) -> int:
"""Return the coldest color_temp that this channel supports.""" """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 @property
def max_mireds(self) -> int: def max_mireds(self) -> int:
"""Return the warmest color_temp that this channel supports.""" """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 @property
def hs_supported(self) -> bool: def hs_supported(self) -> bool:

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from zigpy import types from zhaquirks.inovelli.types import AllLEDEffectType, SingleLEDEffectType
from zigpy.exceptions import ZigbeeException from zigpy.exceptions import ZigbeeException
import zigpy.zcl import zigpy.zcl
@ -183,59 +183,47 @@ class InovelliNotificationChannel(ClientChannel):
class InovelliConfigEntityChannel(ZigbeeChannel): class InovelliConfigEntityChannel(ZigbeeChannel):
"""Inovelli Configuration Entity channel.""" """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 = () REPORT_CONFIG = ()
ZCL_INIT_ATTRS = { ZCL_INIT_ATTRS = {
"dimming_speed_up_remote": False, "dimming_speed_up_remote": True,
"dimming_speed_up_local": False, "dimming_speed_up_local": True,
"ramp_rate_off_to_on_local": False, "ramp_rate_off_to_on_local": True,
"ramp_rate_off_to_on_remote": False, "ramp_rate_off_to_on_remote": True,
"dimming_speed_down_remote": False, "dimming_speed_down_remote": True,
"dimming_speed_down_local": False, "dimming_speed_down_local": True,
"ramp_rate_on_to_off_local": False, "ramp_rate_on_to_off_local": True,
"ramp_rate_on_to_off_remote": False, "ramp_rate_on_to_off_remote": True,
"minimum_level": False, "minimum_level": True,
"maximum_level": False, "maximum_level": True,
"invert_switch": False, "invert_switch": True,
"auto_off_timer": False, "auto_off_timer": True,
"default_level_local": False, "default_level_local": True,
"default_level_remote": False, "default_level_remote": True,
"state_after_power_restored": False, "state_after_power_restored": True,
"load_level_indicator_timeout": False, "load_level_indicator_timeout": True,
"active_power_reports": False, "active_power_reports": True,
"periodic_power_and_energy_reports": False, "periodic_power_and_energy_reports": True,
"active_energy_reports": False, "active_energy_reports": True,
"power_type": False, "power_type": False,
"switch_type": False, "switch_type": False,
"button_delay": False, "button_delay": False,
"smart_bulb_mode": False, "smart_bulb_mode": False,
"double_tap_up_for_full_brightness": False, "double_tap_up_for_full_brightness": True,
"led_color_when_on": False, "led_color_when_on": True,
"led_color_when_off": False, "led_color_when_off": True,
"led_intensity_when_on": False, "led_intensity_when_on": True,
"led_intensity_when_off": False, "led_intensity_when_off": True,
"local_protection": False, "local_protection": False,
"output_mode": False, "output_mode": False,
"on_off_led_mode": False, "on_off_led_mode": True,
"firmware_progress_led": False, "firmware_progress_led": True,
"relay_click_in_on_off_mode": False, "relay_click_in_on_off_mode": True,
"disable_clear_notifications_double_tap": True,
} }
async def issue_all_led_effect( async def issue_all_led_effect(
self, self,
effect_type: LEDEffectType | int = LEDEffectType.Fast_Blink, effect_type: AllLEDEffectType | int = AllLEDEffectType.Fast_Blink,
color: int = 200, color: int = 200,
level: int = 100, level: int = 100,
duration: int = 3, duration: int = 3,
@ -251,7 +239,7 @@ class InovelliConfigEntityChannel(ZigbeeChannel):
async def issue_individual_led_effect( async def issue_individual_led_effect(
self, self,
led_number: int = 1, led_number: int = 1,
effect_type: LEDEffectType | int = LEDEffectType.Fast_Blink, effect_type: SingleLEDEffectType | int = SingleLEDEffectType.Fast_Blink,
color: int = 200, color: int = 200,
level: int = 100, level: int = 100,
duration: int = 3, duration: int = 3,

View File

@ -13,7 +13,7 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import DOMAIN from . import DOMAIN
from .api import SERVICE_WARNING_DEVICE_SQUAWK, SERVICE_WARNING_DEVICE_WARN 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.const import CHANNEL_IAS_WD, CHANNEL_INOVELLI
from .core.helpers import async_get_zha_device 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_TYPE): INOVELLI_ALL_LED_EFFECT,
vol.Required(CONF_DOMAIN): DOMAIN, vol.Required(CONF_DOMAIN): DOMAIN,
vol.Required( vol.Required("effect_type"): AllLEDEffectType.__getitem__,
"effect_type"
): InovelliConfigEntityChannel.LEDEffectType.__getitem__,
vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)), 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("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)), 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( INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA = INOVELLI_ALL_LED_EFFECT_SCHEMA.extend(
{ {
vol.Required(CONF_TYPE): INOVELLI_INDIVIDUAL_LED_EFFECT, 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( ACTION_SCHEMA = vol.Any(
INOVELLI_ALL_LED_EFFECT_SCHEMA, INOVELLI_ALL_LED_EFFECT_SCHEMA,
INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA, INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA,
@ -83,9 +87,7 @@ DEVICE_ACTION_TYPES = {
DEVICE_ACTION_SCHEMAS = { DEVICE_ACTION_SCHEMAS = {
INOVELLI_ALL_LED_EFFECT: vol.Schema( INOVELLI_ALL_LED_EFFECT: vol.Schema(
{ {
vol.Required("effect_type"): vol.In( vol.Required("effect_type"): vol.In(AllLEDEffectType.__members__.keys()),
InovelliConfigEntityChannel.LEDEffectType.__members__.keys()
),
vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)), 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("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)), 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( INOVELLI_INDIVIDUAL_LED_EFFECT: vol.Schema(
{ {
vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)), vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)),
vol.Required("effect_type"): vol.In( vol.Required("effect_type"): vol.In(SingleLEDEffectType.__members__.keys()),
InovelliConfigEntityChannel.LEDEffectType.__members__.keys()
),
vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)), 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("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)), 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( async def async_get_actions(
hass: HomeAssistant, device_id: str hass: HomeAssistant, device_id: str
) -> list[dict[str, str]]: ) -> list[dict[str, str]]:

View File

@ -7,7 +7,7 @@
"bellows==0.34.2", "bellows==0.34.2",
"pyserial==3.5", "pyserial==3.5",
"pyserial-asyncio==0.6", "pyserial-asyncio==0.6",
"zha-quirks==0.0.84", "zha-quirks==0.0.85",
"zigpy-deconz==0.19.0", "zigpy-deconz==0.19.0",
"zigpy==0.51.5", "zigpy==0.51.5",
"zigpy-xbee==0.16.2", "zigpy-xbee==0.16.2",

View File

@ -418,3 +418,15 @@ class InovelliRelayClickInOnOffMode(
_zcl_attribute: str = "relay_click_in_on_off_mode" _zcl_attribute: str = "relay_click_in_on_off_mode"
_attr_name: str = "Disable 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"

View File

@ -660,24 +660,25 @@ class ConfigEntry:
data: dict[str, Any] | None = None, data: dict[str, Any] | None = None,
) -> None: ) -> None:
"""Start a reauth flow.""" """Start a reauth flow."""
flow_context = { if any(
"source": SOURCE_REAUTH, flow
"entry_id": self.entry_id, for flow in hass.config_entries.flow.async_progress_by_handler(self.domain)
"title_placeholders": {"name": self.title}, if flow["context"].get("source") == SOURCE_REAUTH
"unique_id": self.unique_id, and flow["context"].get("entry_id") == self.entry_id
} ):
# Reauth flow already in progress for this entry
if context: return
flow_context.update(context)
for flow in hass.config_entries.flow.async_progress_by_handler(self.domain):
if flow["context"] == flow_context:
return
hass.async_create_task( hass.async_create_task(
hass.config_entries.flow.async_init( hass.config_entries.flow.async_init(
self.domain, 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 {}), data=self.data | (data or {}),
) )
) )

View File

@ -8,7 +8,7 @@ from .backports.enum import StrEnum
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2022 MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 11 MINOR_VERSION: Final = 11
PATCH_VERSION: Final = "1" PATCH_VERSION: Final = "2"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@ -4,15 +4,15 @@ aiodiscover==1.4.13
aiohttp==3.8.1 aiohttp==3.8.1
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
astral==2.2 astral==2.2
async-upnp-client==0.32.1 async-upnp-client==0.32.2
async_timeout==4.0.2 async_timeout==4.0.2
atomicwrites-homeassistant==1.4.1 atomicwrites-homeassistant==1.4.1
attrs==21.2.0 attrs==21.2.0
awesomeversion==22.9.0 awesomeversion==22.9.0
bcrypt==3.1.7 bcrypt==3.1.7
bleak-retry-connector==2.8.2 bleak-retry-connector==2.8.3
bleak==0.19.1 bleak==0.19.2
bluetooth-adapters==0.6.0 bluetooth-adapters==0.7.0
bluetooth-auto-recovery==0.3.6 bluetooth-auto-recovery==0.3.6
certifi>=2021.5.30 certifi>=2021.5.30
ciso8601==2.2.0 ciso8601==2.2.0
@ -21,7 +21,7 @@ dbus-fast==1.61.1
fnvhash==0.1.0 fnvhash==0.1.0
hass-nabucasa==0.56.0 hass-nabucasa==0.56.0
home-assistant-bluetooth==1.6.0 home-assistant-bluetooth==1.6.0
home-assistant-frontend==20221102.1 home-assistant-frontend==20221108.0
httpx==0.23.0 httpx==0.23.0
ifaddr==0.1.7 ifaddr==0.1.7
jinja2==3.1.2 jinja2==3.1.2

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2022.11.1" version = "2022.11.2"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"

View File

@ -50,7 +50,7 @@ PyTurboJPEG==1.6.7
PyViCare==2.17.0 PyViCare==2.17.0
# homeassistant.components.xiaomi_aqara # homeassistant.components.xiaomi_aqara
PyXiaomiGateway==0.14.1 PyXiaomiGateway==0.14.3
# homeassistant.components.remember_the_milk # homeassistant.components.remember_the_milk
RtmAPI==0.7.2 RtmAPI==0.7.2
@ -153,7 +153,7 @@ aioecowitt==2022.09.3
aioemonitor==1.0.5 aioemonitor==1.0.5
# homeassistant.components.esphome # homeassistant.components.esphome
aioesphomeapi==11.4.2 aioesphomeapi==11.4.3
# homeassistant.components.flo # homeassistant.components.flo
aioflo==2021.11.0 aioflo==2021.11.0
@ -171,7 +171,7 @@ aioguardian==2022.07.0
aioharmony==0.2.9 aioharmony==0.2.9
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==2.2.14 aiohomekit==2.2.18
# homeassistant.components.emulated_hue # homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http
@ -237,7 +237,7 @@ aiopvpc==3.0.0
# homeassistant.components.lidarr # homeassistant.components.lidarr
# homeassistant.components.radarr # homeassistant.components.radarr
# homeassistant.components.sonarr # homeassistant.components.sonarr
aiopyarr==22.10.0 aiopyarr==22.11.0
# homeassistant.components.qnap_qsw # homeassistant.components.qnap_qsw
aioqsw==0.2.2 aioqsw==0.2.2
@ -353,7 +353,7 @@ asterisk_mbox==0.5.0
# homeassistant.components.ssdp # homeassistant.components.ssdp
# homeassistant.components.upnp # homeassistant.components.upnp
# homeassistant.components.yeelight # homeassistant.components.yeelight
async-upnp-client==0.32.1 async-upnp-client==0.32.2
# homeassistant.components.supla # homeassistant.components.supla
asyncpysupla==0.0.5 asyncpysupla==0.0.5
@ -413,10 +413,10 @@ bimmer_connected==0.10.4
bizkaibus==0.1.1 bizkaibus==0.1.1
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bleak-retry-connector==2.8.2 bleak-retry-connector==2.8.3
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bleak==0.19.1 bleak==0.19.2
# homeassistant.components.blebox # homeassistant.components.blebox
blebox_uniapi==2.1.3 blebox_uniapi==2.1.3
@ -438,7 +438,7 @@ bluemaestro-ble==0.2.0
# bluepy==1.3.0 # bluepy==1.3.0
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-adapters==0.6.0 bluetooth-adapters==0.7.0
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-auto-recovery==0.3.6 bluetooth-auto-recovery==0.3.6
@ -725,7 +725,7 @@ gTTS==2.2.4
garages-amsterdam==3.0.0 garages-amsterdam==3.0.0
# homeassistant.components.google # homeassistant.components.google
gcal-sync==2.2.3 gcal-sync==4.0.0
# homeassistant.components.geniushub # homeassistant.components.geniushub
geniushub-client==0.6.30 geniushub-client==0.6.30
@ -815,6 +815,9 @@ gstreamer-player==1.1.2
# homeassistant.components.profiler # homeassistant.components.profiler
guppy3==3.1.2 guppy3==3.1.2
# homeassistant.components.iaqualink
h2==4.1.0
# homeassistant.components.homekit # homeassistant.components.homekit
ha-HAP-python==4.5.2 ha-HAP-python==4.5.2
@ -868,7 +871,7 @@ hole==0.7.0
holidays==0.16 holidays==0.16
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20221102.1 home-assistant-frontend==20221108.0
# homeassistant.components.home_connect # homeassistant.components.home_connect
homeconnect==0.7.2 homeconnect==0.7.2
@ -1135,7 +1138,7 @@ nettigo-air-monitor==1.4.2
neurio==0.3.1 neurio==0.3.1
# homeassistant.components.nexia # homeassistant.components.nexia
nexia==2.0.5 nexia==2.0.6
# homeassistant.components.nextcloud # homeassistant.components.nextcloud
nextcloudmonitor==1.1.0 nextcloudmonitor==1.1.0
@ -1238,7 +1241,7 @@ openwrt-luci-rpc==1.1.11
openwrt-ubus-rpc==0.0.2 openwrt-ubus-rpc==0.0.2
# homeassistant.components.oralb # homeassistant.components.oralb
oralb-ble==0.10.0 oralb-ble==0.13.0
# homeassistant.components.oru # homeassistant.components.oru
oru==0.1.11 oru==0.1.11
@ -1250,7 +1253,7 @@ orvibo==1.1.1
ovoenergy==1.2.0 ovoenergy==1.2.0
# homeassistant.components.p1_monitor # homeassistant.components.p1_monitor
p1monitor==2.1.0 p1monitor==2.1.1
# homeassistant.components.mqtt # homeassistant.components.mqtt
# homeassistant.components.shiftr # homeassistant.components.shiftr
@ -1312,7 +1315,7 @@ plexauth==0.0.6
plexwebsocket==0.0.13 plexwebsocket==0.0.13
# homeassistant.components.plugwise # homeassistant.components.plugwise
plugwise==0.25.3 plugwise==0.25.7
# homeassistant.components.plum_lightpad # homeassistant.components.plum_lightpad
plumlightpad==0.0.11 plumlightpad==0.0.11
@ -1433,7 +1436,7 @@ pyaftership==21.11.0
pyairnow==1.1.0 pyairnow==1.1.0
# homeassistant.components.airvisual # homeassistant.components.airvisual
pyairvisual==2022.07.0 pyairvisual==2022.11.1
# homeassistant.components.almond # homeassistant.components.almond
pyalmond==0.0.2 pyalmond==0.0.2
@ -1442,7 +1445,7 @@ pyalmond==0.0.2
pyatag==0.3.5.3 pyatag==0.3.5.3
# homeassistant.components.netatmo # homeassistant.components.netatmo
pyatmo==7.3.0 pyatmo==7.4.0
# homeassistant.components.atome # homeassistant.components.atome
pyatome==0.1.1 pyatome==0.1.1
@ -1688,7 +1691,7 @@ pylibrespot-java==0.1.1
pylitejet==0.3.0 pylitejet==0.3.0
# homeassistant.components.litterrobot # homeassistant.components.litterrobot
pylitterbot==2022.10.2 pylitterbot==2022.11.0
# homeassistant.components.lutron_caseta # homeassistant.components.lutron_caseta
pylutron-caseta==0.17.1 pylutron-caseta==0.17.1
@ -2487,7 +2490,7 @@ vehicle==0.4.0
velbus-aio==2022.10.4 velbus-aio==2022.10.4
# homeassistant.components.venstar # homeassistant.components.venstar
venstarcolortouch==0.18 venstarcolortouch==0.19
# homeassistant.components.vilfo # homeassistant.components.vilfo
vilfo-api-client==0.3.2 vilfo-api-client==0.3.2
@ -2607,7 +2610,7 @@ zengge==0.2
zeroconf==0.39.4 zeroconf==0.39.4
# homeassistant.components.zha # homeassistant.components.zha
zha-quirks==0.0.84 zha-quirks==0.0.85
# homeassistant.components.zhong_hong # homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9 zhong_hong_hvac==1.0.9

View File

@ -46,7 +46,7 @@ PyTurboJPEG==1.6.7
PyViCare==2.17.0 PyViCare==2.17.0
# homeassistant.components.xiaomi_aqara # homeassistant.components.xiaomi_aqara
PyXiaomiGateway==0.14.1 PyXiaomiGateway==0.14.3
# homeassistant.components.remember_the_milk # homeassistant.components.remember_the_milk
RtmAPI==0.7.2 RtmAPI==0.7.2
@ -140,7 +140,7 @@ aioecowitt==2022.09.3
aioemonitor==1.0.5 aioemonitor==1.0.5
# homeassistant.components.esphome # homeassistant.components.esphome
aioesphomeapi==11.4.2 aioesphomeapi==11.4.3
# homeassistant.components.flo # homeassistant.components.flo
aioflo==2021.11.0 aioflo==2021.11.0
@ -155,7 +155,7 @@ aioguardian==2022.07.0
aioharmony==0.2.9 aioharmony==0.2.9
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==2.2.14 aiohomekit==2.2.18
# homeassistant.components.emulated_hue # homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http
@ -212,7 +212,7 @@ aiopvpc==3.0.0
# homeassistant.components.lidarr # homeassistant.components.lidarr
# homeassistant.components.radarr # homeassistant.components.radarr
# homeassistant.components.sonarr # homeassistant.components.sonarr
aiopyarr==22.10.0 aiopyarr==22.11.0
# homeassistant.components.qnap_qsw # homeassistant.components.qnap_qsw
aioqsw==0.2.2 aioqsw==0.2.2
@ -307,7 +307,7 @@ arcam-fmj==0.12.0
# homeassistant.components.ssdp # homeassistant.components.ssdp
# homeassistant.components.upnp # homeassistant.components.upnp
# homeassistant.components.yeelight # homeassistant.components.yeelight
async-upnp-client==0.32.1 async-upnp-client==0.32.2
# homeassistant.components.sleepiq # homeassistant.components.sleepiq
asyncsleepiq==1.2.3 asyncsleepiq==1.2.3
@ -337,10 +337,10 @@ bellows==0.34.2
bimmer_connected==0.10.4 bimmer_connected==0.10.4
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bleak-retry-connector==2.8.2 bleak-retry-connector==2.8.3
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bleak==0.19.1 bleak==0.19.2
# homeassistant.components.blebox # homeassistant.components.blebox
blebox_uniapi==2.1.3 blebox_uniapi==2.1.3
@ -352,7 +352,7 @@ blinkpy==0.19.2
bluemaestro-ble==0.2.0 bluemaestro-ble==0.2.0
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-adapters==0.6.0 bluetooth-adapters==0.7.0
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-auto-recovery==0.3.6 bluetooth-auto-recovery==0.3.6
@ -541,7 +541,7 @@ gTTS==2.2.4
garages-amsterdam==3.0.0 garages-amsterdam==3.0.0
# homeassistant.components.google # homeassistant.components.google
gcal-sync==2.2.3 gcal-sync==4.0.0
# homeassistant.components.geocaching # homeassistant.components.geocaching
geocachingapi==0.2.1 geocachingapi==0.2.1
@ -607,6 +607,9 @@ gspread==5.5.0
# homeassistant.components.profiler # homeassistant.components.profiler
guppy3==3.1.2 guppy3==3.1.2
# homeassistant.components.iaqualink
h2==4.1.0
# homeassistant.components.homekit # homeassistant.components.homekit
ha-HAP-python==4.5.2 ha-HAP-python==4.5.2
@ -648,7 +651,7 @@ hole==0.7.0
holidays==0.16 holidays==0.16
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20221102.1 home-assistant-frontend==20221108.0
# homeassistant.components.home_connect # homeassistant.components.home_connect
homeconnect==0.7.2 homeconnect==0.7.2
@ -825,7 +828,7 @@ netmap==0.7.0.2
nettigo-air-monitor==1.4.2 nettigo-air-monitor==1.4.2
# homeassistant.components.nexia # homeassistant.components.nexia
nexia==2.0.5 nexia==2.0.6
# homeassistant.components.discord # homeassistant.components.discord
nextcord==2.0.0a8 nextcord==2.0.0a8
@ -883,13 +886,13 @@ open-meteo==0.2.1
openerz-api==0.1.0 openerz-api==0.1.0
# homeassistant.components.oralb # homeassistant.components.oralb
oralb-ble==0.10.0 oralb-ble==0.13.0
# homeassistant.components.ovo_energy # homeassistant.components.ovo_energy
ovoenergy==1.2.0 ovoenergy==1.2.0
# homeassistant.components.p1_monitor # homeassistant.components.p1_monitor
p1monitor==2.1.0 p1monitor==2.1.1
# homeassistant.components.mqtt # homeassistant.components.mqtt
# homeassistant.components.shiftr # homeassistant.components.shiftr
@ -939,7 +942,7 @@ plexauth==0.0.6
plexwebsocket==0.0.13 plexwebsocket==0.0.13
# homeassistant.components.plugwise # homeassistant.components.plugwise
plugwise==0.25.3 plugwise==0.25.7
# homeassistant.components.plum_lightpad # homeassistant.components.plum_lightpad
plumlightpad==0.0.11 plumlightpad==0.0.11
@ -1021,7 +1024,7 @@ pyaehw4a1==0.3.9
pyairnow==1.1.0 pyairnow==1.1.0
# homeassistant.components.airvisual # homeassistant.components.airvisual
pyairvisual==2022.07.0 pyairvisual==2022.11.1
# homeassistant.components.almond # homeassistant.components.almond
pyalmond==0.0.2 pyalmond==0.0.2
@ -1030,7 +1033,7 @@ pyalmond==0.0.2
pyatag==0.3.5.3 pyatag==0.3.5.3
# homeassistant.components.netatmo # homeassistant.components.netatmo
pyatmo==7.3.0 pyatmo==7.4.0
# homeassistant.components.apple_tv # homeassistant.components.apple_tv
pyatv==0.10.3 pyatv==0.10.3
@ -1189,7 +1192,7 @@ pylibrespot-java==0.1.1
pylitejet==0.3.0 pylitejet==0.3.0
# homeassistant.components.litterrobot # homeassistant.components.litterrobot
pylitterbot==2022.10.2 pylitterbot==2022.11.0
# homeassistant.components.lutron_caseta # homeassistant.components.lutron_caseta
pylutron-caseta==0.17.1 pylutron-caseta==0.17.1
@ -1721,7 +1724,7 @@ vehicle==0.4.0
velbus-aio==2022.10.4 velbus-aio==2022.10.4
# homeassistant.components.venstar # homeassistant.components.venstar
venstarcolortouch==0.18 venstarcolortouch==0.19
# homeassistant.components.vilfo # homeassistant.components.vilfo
vilfo-api-client==0.3.2 vilfo-api-client==0.3.2
@ -1808,7 +1811,7 @@ zamg==0.1.1
zeroconf==0.39.4 zeroconf==0.39.4
# homeassistant.components.zha # homeassistant.components.zha
zha-quirks==0.0.84 zha-quirks==0.0.85
# homeassistant.components.zha # homeassistant.components.zha
zigpy-deconz==0.19.0 zigpy-deconz==0.19.0

View File

@ -1,14 +1,14 @@
"""Define tests for the AirVisual config flow.""" """Define tests for the AirVisual config flow."""
from unittest.mock import patch from unittest.mock import patch
from pyairvisual.errors import ( from pyairvisual.cloud_api import (
AirVisualError,
InvalidKeyError, InvalidKeyError,
KeyExpiredError, KeyExpiredError,
NodeProError,
NotFoundError, NotFoundError,
UnauthorizedError, UnauthorizedError,
) )
from pyairvisual.errors import AirVisualError
from pyairvisual.node import NodeProError
import pytest import pytest
from homeassistant import data_entry_flow from homeassistant import data_entry_flow

View File

@ -2,6 +2,7 @@
from dataclasses import asdict from dataclasses import asdict
from unittest.mock import patch from unittest.mock import patch
from elkm1_lib.discovery import ElkSystem
import pytest import pytest
from homeassistant import config_entries 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["type"] == FlowResultType.ABORT
assert result["reason"] == "cannot_connect" 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

View File

@ -104,7 +104,7 @@ async def primary_calendar(
"""Fixture to return the primary calendar.""" """Fixture to return the primary calendar."""
mock_calendar_get( mock_calendar_get(
"primary", "primary",
{"id": primary_calendar_email, "summary": "Personal"}, {"id": primary_calendar_email, "summary": "Personal", "accessRole": "owner"},
exc=primary_calendar_error, exc=primary_calendar_error,
) )

View File

@ -768,7 +768,7 @@ async def test_assign_unique_id(
mock_calendar_get( mock_calendar_get(
"primary", "primary",
{"id": EMAIL_ADDRESS, "summary": "Personal"}, {"id": EMAIL_ADDRESS, "summary": "Personal", "accessRole": "reader"},
) )
mock_calendars_list({"items": [test_api_calendar]}) mock_calendars_list({"items": [test_api_calendar]})

View File

@ -6,7 +6,7 @@ from unittest.mock import patch
import pytest import pytest
from homeassistant.components.device_tracker.legacy import YAML_DEVICES 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.const import BRIDGE_NAME, EVENT_HOMEKIT_CHANGED
from homeassistant.components.homekit.iidmanager import AccessoryIIDStorage from homeassistant.components.homekit.iidmanager import AccessoryIIDStorage
@ -39,7 +39,7 @@ def run_driver(hass, loop, iid_storage):
entry_id="", entry_id="",
entry_title="mock entry", entry_title="mock entry",
bridge_name=BRIDGE_NAME, bridge_name=BRIDGE_NAME,
iid_manager=HomeIIDManager(iid_storage), iid_storage=iid_storage,
address="127.0.0.1", address="127.0.0.1",
loop=loop, loop=loop,
) )
@ -63,7 +63,7 @@ def hk_driver(hass, loop, iid_storage):
entry_id="", entry_id="",
entry_title="mock entry", entry_title="mock entry",
bridge_name=BRIDGE_NAME, bridge_name=BRIDGE_NAME,
iid_manager=HomeIIDManager(iid_storage), iid_storage=iid_storage,
address="127.0.0.1", address="127.0.0.1",
loop=loop, loop=loop,
) )
@ -91,7 +91,7 @@ def mock_hap(hass, loop, iid_storage, mock_zeroconf):
entry_id="", entry_id="",
entry_title="mock entry", entry_title="mock entry",
bridge_name=BRIDGE_NAME, bridge_name=BRIDGE_NAME,
iid_manager=HomeIIDManager(iid_storage), iid_storage=iid_storage,
address="127.0.0.1", address="127.0.0.1",
loop=loop, loop=loop,
) )

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

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

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

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

View File

@ -10,7 +10,6 @@ from homeassistant.components.homekit.accessories import (
HomeAccessory, HomeAccessory,
HomeBridge, HomeBridge,
HomeDriver, HomeDriver,
HomeIIDManager,
) )
from homeassistant.components.homekit.const import ( from homeassistant.components.homekit.const import (
ATTR_DISPLAY_NAME, ATTR_DISPLAY_NAME,
@ -724,7 +723,7 @@ def test_home_driver(iid_storage):
"entry_id", "entry_id",
"name", "name",
"title", "title",
iid_manager=HomeIIDManager(iid_storage), iid_storage=iid_storage,
address=ip_address, address=ip_address,
port=port, port=port,
persist_file=path, persist_file=path,
@ -752,22 +751,3 @@ def test_home_driver(iid_storage):
mock_unpair.assert_called_with("client_uuid") mock_unpair.assert_called_with("client_uuid")
mock_show_msg.assert_called_with("hass", "entry_id", "title (any)", pin, "X-HM://0") 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, {})

View File

@ -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) diag = await get_diagnostics_for_config_entry(hass, hass_client, entry)
assert diag == { assert diag == {
"bridge": {}, "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": [ "accessories": [
{ {
"aid": 1, "aid": 1,
@ -257,6 +269,20 @@ async def test_config_entry_accessory(
}, },
"config_version": 2, "config_version": 2,
"pairing_id": ANY, "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, "status": 1,
} }
with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch(

View File

@ -262,7 +262,7 @@ async def test_homekit_setup(hass, hk_driver, mock_async_zeroconf):
async_zeroconf_instance=zeroconf_mock, async_zeroconf_instance=zeroconf_mock,
zeroconf_server=f"{uuid}-hap.local.", zeroconf_server=f"{uuid}-hap.local.",
loader=ANY, loader=ANY,
iid_manager=ANY, iid_storage=ANY,
) )
assert homekit.driver.safe_mode is False 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, async_zeroconf_instance=mock_async_zeroconf,
zeroconf_server=f"{uuid}-hap.local.", zeroconf_server=f"{uuid}-hap.local.",
loader=ANY, 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, async_zeroconf_instance=async_zeroconf_instance,
zeroconf_server=f"{uuid}-hap.local.", zeroconf_server=f"{uuid}-hap.local.",
loader=ANY, loader=ANY,
iid_manager=ANY, iid_storage=ANY,
) )

View File

@ -1,6 +1,5 @@
"""Tests for the HomeKit IID manager.""" """Tests for the HomeKit IID manager."""
from uuid import UUID from uuid import UUID
from homeassistant.components.homekit.const import DOMAIN from homeassistant.components.homekit.const import DOMAIN
@ -8,9 +7,10 @@ from homeassistant.components.homekit.iidmanager import (
AccessoryIIDStorage, AccessoryIIDStorage,
get_iid_storage_filename_for_entry_id, get_iid_storage_filename_for_entry_id,
) )
from homeassistant.helpers.json import json_loads
from homeassistant.util.uuid import random_uuid_hex 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): 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_iid1
== unique_service_unique_char_new_aid_iid2 == 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() await iid_storage.async_save()
iid_storage2 = AccessoryIIDStorage(hass, entry.entry_id) 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( assert iid_storage.store.path.endswith(
get_iid_storage_filename_for_entry_id(entry.entry_id) 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]}

View File

@ -21,11 +21,14 @@ from homeassistant.components.lifx.manager import (
) )
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT,
ATTR_COLOR_MODE, ATTR_COLOR_MODE,
ATTR_COLOR_NAME,
ATTR_COLOR_TEMP, ATTR_COLOR_TEMP,
ATTR_COLOR_TEMP_KELVIN, ATTR_COLOR_TEMP_KELVIN,
ATTR_EFFECT, ATTR_EFFECT,
ATTR_HS_COLOR, ATTR_HS_COLOR,
ATTR_KELVIN,
ATTR_RGB_COLOR, ATTR_RGB_COLOR,
ATTR_SUPPORTED_COLOR_MODES, ATTR_SUPPORTED_COLOR_MODES,
ATTR_TRANSITION, ATTR_TRANSITION,
@ -1397,6 +1400,131 @@ async def test_transitions_color_bulb(hass: HomeAssistant) -> None:
bulb.set_color.reset_mock() 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: async def test_infrared_color_bulb(hass: HomeAssistant) -> None:
"""Test setting infrared with a color bulb.""" """Test setting infrared with a color bulb."""
already_migrated_config_entry = MockConfigEntry( already_migrated_config_entry = MockConfigEntry(

View File

@ -24,6 +24,7 @@ from homeassistant.const import (
CONF_ENTITIES, CONF_ENTITIES,
CONF_EXCLUDE, CONF_EXCLUDE,
CONF_INCLUDE, CONF_INCLUDE,
EVENT_HOMEASSISTANT_FINAL_WRITE,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_START,
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
@ -52,6 +53,15 @@ def set_utc(hass):
hass.config.set_time_zone("UTC") 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: async def _async_mock_logbook_platform(hass: HomeAssistant) -> None:
class MockLogbookPlatform: class MockLogbookPlatform:
"""Mock a logbook platform.""" """Mock a logbook platform."""
@ -684,7 +694,9 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities(
assert msg["success"] assert msg["success"]
# Check our listener got unsubscribed # 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) @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"] assert msg["success"]
# Check our listener got unsubscribed # 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) @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"] assert msg["success"]
# Check our listener got unsubscribed # 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) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
@ -1386,7 +1402,9 @@ async def test_subscribe_unsubscribe_logbook_stream(
assert msg["success"] assert msg["success"]
# Check our listener got unsubscribed # 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) @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"] assert msg["success"]
# Check our listener got unsubscribed # 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) @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"] assert msg["success"]
# Check our listener got unsubscribed # Check our listener got unsubscribed
listeners = hass.bus.async_listeners() assert listeners_without_writes(
# The async_fire_time_changed above triggers unsubscribe from hass.bus.async_listeners()
# homeassistant_final_write, don't worry about those ) == listeners_without_writes(init_listeners)
init_listeners.pop("homeassistant_final_write")
listeners.pop("homeassistant_final_write")
assert listeners == init_listeners
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @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"] assert msg["success"]
# Check our listener got unsubscribed # 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) @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"] assert msg["success"]
# Check our listener got unsubscribed # 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) @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"] assert msg["success"]
# Check our listener got unsubscribed # 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): 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"] assert msg["success"]
# Check our listener got unsubscribed # 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): 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"] assert msg["success"]
# Check our listener got unsubscribed # 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) @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() await hass.async_block_till_done()
# Check our listener got unsubscribed # 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) @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["type"] == TYPE_RESULT
assert msg["success"] 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): for _ in range(5):
hass.states.async_set("binary_sensor.is_light", STATE_ON) hass.states.async_set("binary_sensor.is_light", STATE_ON)
hass.states.async_set("binary_sensor.is_light", STATE_OFF) 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 # Check our listener got unsubscribed because
# the queue got full and the overload safety tripped # 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() 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) @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() await hass.async_block_till_done()
# Check our listener got unsubscribed # 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) @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() await hass.async_block_till_done()
# Check our listener got unsubscribed # 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) @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"] assert msg["success"]
# Check our listener got unsubscribed # 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) @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() await hass.async_block_till_done()
# Check our listener got unsubscribed # 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)

View File

@ -15,10 +15,15 @@ from homeassistant.const import (
SERVICE_RELOAD, SERVICE_RELOAD,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
) )
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow 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 @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_date_time").state == "07:11:08 PM"
assert hass.states.get("sensor.json_time").state == "07:11:39 PM" assert hass.states.get("sensor.json_time").state == "07:11:39 PM"
assert hass.states.get("binary_sensor.binary_sensor").state == "on" 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)

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from datetime import datetime from datetime import datetime
from unittest.mock import patch 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 ( from homeassistant.components.sensor import (
CONF_STATE_CLASS, CONF_STATE_CLASS,
SensorDeviceClass, SensorDeviceClass,
@ -189,7 +189,7 @@ async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None:
assert state.state == "Current Version: 2021.12.10" assert state.state == "Current Version: 2021.12.10"
mocker.payload = "test_scrape_sensor_no_data" 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() await hass.async_block_till_done()
state = hass.states.get("sensor.ha_version") state = hass.states.get("sensor.ha_version")

View File

@ -63,6 +63,42 @@ async def test_flow_ssdp(hass: HomeAssistant):
CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN,
CONFIG_ENTRY_LOCATION: TEST_LOCATION, CONFIG_ENTRY_LOCATION: TEST_LOCATION,
CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, 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_ORIGINAL_UDN: TEST_UDN,
CONFIG_ENTRY_LOCATION: TEST_LOCATION, CONFIG_ENTRY_LOCATION: TEST_LOCATION,
CONFIG_ENTRY_MAC_ADDRESS: None, 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_ORIGINAL_UDN: TEST_UDN,
CONFIG_ENTRY_LOCATION: TEST_LOCATION, CONFIG_ENTRY_LOCATION: TEST_LOCATION,
CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS,
CONFIG_ENTRY_HOST: TEST_HOST,
} }

View File

@ -819,6 +819,24 @@ async def test_info_from_service_with_link_local_address_first(hass):
assert info.host == "192.168.66.12" 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): async def test_info_from_service_with_link_local_address_second(hass):
"""Test that the link local address is ignored.""" """Test that the link local address is ignored."""
service_type = "_test._tcp.local." service_type = "_test._tcp.local."

View File

@ -290,7 +290,7 @@ async def test_action(hass, device_ias, device_inovelli):
"domain": DOMAIN, "domain": DOMAIN,
"device_id": inovelli_reg_device.id, "device_id": inovelli_reg_device.id,
"type": "issue_individual_led_effect", "type": "issue_individual_led_effect",
"effect_type": "Open_Close", "effect_type": "Falling",
"led_number": 1, "led_number": 1,
"duration": 5, "duration": 5,
"level": 10, "level": 10,

View File

@ -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 = zigpy_device.endpoints[1].light_color
color_cluster.PLUGGED_ATTR_READS = { color_cluster.PLUGGED_ATTR_READS = {
"color_capabilities": lighting.Color.ColorCapabilities.Color_temperature "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 = await zha_device_joined(zigpy_device)
zha_device.available = True zha_device.available = True
@ -1192,6 +1194,8 @@ async def test_transitions(
assert eWeLink_state.state == STATE_ON assert eWeLink_state.state == STATE_ON
assert eWeLink_state.attributes["color_temp"] == 235 assert eWeLink_state.attributes["color_temp"] == 235
assert eWeLink_state.attributes["color_mode"] == ColorMode.COLOR_TEMP 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): async def async_test_on_off_from_light(hass, cluster, entity_id):

View File

@ -3287,6 +3287,7 @@ async def test_disallow_entry_reload_with_setup_in_progresss(hass, manager):
async def test_reauth(hass): async def test_reauth(hass):
"""Test the async_reauth_helper.""" """Test the async_reauth_helper."""
entry = MockConfigEntry(title="test_title", domain="test") entry = MockConfigEntry(title="test_title", domain="test")
entry2 = MockConfigEntry(title="test_title", domain="test")
mock_setup_entry = AsyncMock(return_value=True) mock_setup_entry = AsyncMock(return_value=True)
mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) 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 mock_init.call_args.kwargs["data"]["extra_data"] == 1234
assert entry.entry_id != entry2.entry_id
# Check we can't start duplicate flows # Check we can't start duplicate flows
entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) entry.async_start_reauth(hass, {"extra_context": "some_extra_context"})
await hass.async_block_till_done() 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