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 pyairvisual import CloudAPI, NodeSamba
from pyairvisual.errors import (
AirVisualError,
InvalidKeyError,
KeyExpiredError,
NodeProError,
UnauthorizedError,
)
from pyairvisual.cloud_api import InvalidKeyError, KeyExpiredError, UnauthorizedError
from pyairvisual.errors import AirVisualError
from pyairvisual.node import NodeProError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (

View File

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

View File

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

View File

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

View File

@ -262,7 +262,11 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow):
self.config_entry.entry_id
]
await coordinator.async_update_sources()
try:
await coordinator.async_update_sources()
except BraviaTVError:
return self.async_abort(reason="failed_update")
sources = coordinator.source_map.values()
self.source_list = [item["title"] for item in sources]
return await self.async_step_user()

View File

@ -48,6 +48,9 @@
"ignored_sources": "List of ignored sources"
}
}
},
"abort": {
"failed_update": "An error occurred while updating the list of sources.\n\n Ensure that your TV is turned on before trying to set it up."
}
}
}

View File

@ -41,6 +41,9 @@
}
},
"options": {
"abort": {
"failed_update": "An error occurred while updating the list of sources.\n\n Ensure that your TV is turned on before trying to set it up."
},
"step": {
"user": {
"data": {

View File

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

View File

@ -3,7 +3,7 @@
"name": "DLNA Digital Media Renderer",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
"requirements": ["async-upnp-client==0.32.1"],
"requirements": ["async-upnp-client==0.32.2"],
"dependencies": ["ssdp"],
"after_dependencies": ["media_source"],
"ssdp": [

View File

@ -3,7 +3,7 @@
"name": "DLNA Digital Media Server",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
"requirements": ["async-upnp-client==0.32.1"],
"requirements": ["async-upnp-client==0.32.2"],
"dependencies": ["ssdp"],
"after_dependencies": ["media_source"],
"ssdp": [

View File

@ -7,11 +7,11 @@ import logging
import re
from types import MappingProxyType
from typing import Any, cast
from urllib.parse import urlparse
import async_timeout
from elkm1_lib.elements import Element
from elkm1_lib.elk import Elk
from elkm1_lib.util import parse_url
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
@ -96,6 +96,11 @@ SET_TIME_SERVICE_SCHEMA = vol.Schema(
)
def hostname_from_url(url: str) -> str:
"""Return the hostname from a url."""
return parse_url(url)[1]
def _host_validator(config: dict[str, str]) -> dict[str, str]:
"""Validate that a host is properly configured."""
if config[CONF_HOST].startswith("elks://"):
@ -231,7 +236,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Elk-M1 Control from a config entry."""
conf: MappingProxyType[str, Any] = entry.data
host = urlparse(entry.data[CONF_HOST]).hostname
host = hostname_from_url(entry.data[CONF_HOST])
_LOGGER.debug("Setting up elkm1 %s", conf["host"])

View File

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

View File

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

View File

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

View File

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

View File

@ -3,11 +3,12 @@
from __future__ import annotations
import asyncio
from collections.abc import Iterable
from datetime import datetime, timedelta
import logging
from typing import Any
from gcal_sync.api import SyncEventsRequest
from gcal_sync.api import GoogleCalendarService, ListEventsRequest, SyncEventsRequest
from gcal_sync.exceptions import ApiException
from gcal_sync.model import DateOrDatetime, Event
from gcal_sync.store import ScopedCalendarStore
@ -196,21 +197,30 @@ async def async_setup_entry(
entity_registry.async_remove(
entity_entry.entity_id,
)
request_template = SyncEventsRequest(
calendar_id=calendar_id,
search=data.get(CONF_SEARCH),
start_time=dt_util.now() + SYNC_EVENT_MIN_TIME,
)
sync = CalendarEventSyncManager(
calendar_service,
store=ScopedCalendarStore(store, unique_id or entity_name),
request_template=request_template,
)
coordinator = CalendarUpdateCoordinator(
hass,
sync,
data[CONF_NAME],
)
coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator
if search := data.get(CONF_SEARCH):
coordinator = CalendarQueryUpdateCoordinator(
hass,
calendar_service,
data[CONF_NAME],
calendar_id,
search,
)
else:
request_template = SyncEventsRequest(
calendar_id=calendar_id,
start_time=dt_util.now() + SYNC_EVENT_MIN_TIME,
)
sync = CalendarEventSyncManager(
calendar_service,
store=ScopedCalendarStore(store, unique_id or entity_name),
request_template=request_template,
)
coordinator = CalendarSyncUpdateCoordinator(
hass,
sync,
data[CONF_NAME],
)
entities.append(
GoogleCalendarEntity(
coordinator,
@ -242,8 +252,8 @@ async def async_setup_entry(
)
class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]):
"""Coordinator for calendar RPC calls."""
class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]):
"""Coordinator for calendar RPC calls that use an efficient sync."""
def __init__(
self,
@ -251,7 +261,7 @@ class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]):
sync: CalendarEventSyncManager,
name: str,
) -> None:
"""Create the Calendar event device."""
"""Create the CalendarSyncUpdateCoordinator."""
super().__init__(
hass,
_LOGGER,
@ -271,6 +281,87 @@ class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]):
dt_util.DEFAULT_TIME_ZONE
)
async def async_get_events(
self, start_date: datetime, end_date: datetime
) -> Iterable[Event]:
"""Get all events in a specific time frame."""
if not self.data:
raise HomeAssistantError(
"Unable to get events: Sync from server has not completed"
)
return self.data.overlapping(
dt_util.as_local(start_date),
dt_util.as_local(end_date),
)
@property
def upcoming(self) -> Iterable[Event] | None:
"""Return upcoming events if any."""
if self.data:
return self.data.active_after(dt_util.now())
return None
class CalendarQueryUpdateCoordinator(DataUpdateCoordinator[list[Event]]):
"""Coordinator for calendar RPC calls.
This sends a polling RPC, not using sync, as a workaround
for limitations in the calendar API for supporting search.
"""
def __init__(
self,
hass: HomeAssistant,
calendar_service: GoogleCalendarService,
name: str,
calendar_id: str,
search: str | None,
) -> None:
"""Create the CalendarQueryUpdateCoordinator."""
super().__init__(
hass,
_LOGGER,
name=name,
update_interval=MIN_TIME_BETWEEN_UPDATES,
)
self.calendar_service = calendar_service
self.calendar_id = calendar_id
self._search = search
async def async_get_events(
self, start_date: datetime, end_date: datetime
) -> Iterable[Event]:
"""Get all events in a specific time frame."""
request = ListEventsRequest(
calendar_id=self.calendar_id,
start_time=start_date,
end_time=end_date,
search=self._search,
)
result_items = []
try:
result = await self.calendar_service.async_list_events(request)
async for result_page in result:
result_items.extend(result_page.items)
except ApiException as err:
self.async_set_update_error(err)
raise HomeAssistantError(str(err)) from err
return result_items
async def _async_update_data(self) -> list[Event]:
"""Fetch data from API endpoint."""
request = ListEventsRequest(calendar_id=self.calendar_id, search=self._search)
try:
result = await self.calendar_service.async_list_events(request)
except ApiException as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
return result.items
@property
def upcoming(self) -> Iterable[Event] | None:
"""Return the next upcoming event if any."""
return self.data
class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
"""A calendar event entity."""
@ -279,7 +370,7 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
def __init__(
self,
coordinator: CalendarUpdateCoordinator,
coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator,
calendar_id: str,
data: dict[str, Any],
entity_id: str,
@ -352,14 +443,7 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
) -> list[CalendarEvent]:
"""Get all events in a specific time frame."""
if not (timeline := self.coordinator.data):
raise HomeAssistantError(
"Unable to get events: Sync from server has not completed"
)
result_items = timeline.overlapping(
dt_util.as_local(start_date),
dt_util.as_local(end_date),
)
result_items = await self.coordinator.async_get_events(start_date, end_date)
return [
_get_calendar_event(event)
for event in filter(self._event_filter, result_items)
@ -367,14 +451,12 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
def _apply_coordinator_update(self) -> None:
"""Copy state from the coordinator to this entity."""
if (timeline := self.coordinator.data) and (
api_event := next(
filter(
self._event_filter,
timeline.active_after(dt_util.now()),
),
None,
)
if api_event := next(
filter(
self._event_filter,
self.coordinator.upcoming or [],
),
None,
):
self._event = _get_calendar_event(api_event)
(self._event.summary, self._offset_value) = extract_offset(

View File

@ -4,7 +4,7 @@
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/calendar.google/",
"requirements": ["gcal-sync==2.2.3", "oauth2client==4.1.3"],
"requirements": ["gcal-sync==4.0.0", "oauth2client==4.1.3"],
"codeowners": ["@allenporter"],
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"]

View File

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

View File

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

View File

@ -31,6 +31,8 @@ async def async_get_config_entry_diagnostics(
"options": dict(entry.options),
},
}
if homekit.iid_storage:
data["iid_storage"] = homekit.iid_storage.allocations
if not homekit.driver: # not started yet or startup failed
return data
driver: AccessoryDriver = homekit.driver

View File

@ -17,7 +17,7 @@ from homeassistant.helpers.storage import Store
from .util import get_iid_storage_filename_for_entry_id
IID_MANAGER_STORAGE_VERSION = 1
IID_MANAGER_STORAGE_VERSION = 2
IID_MANAGER_SAVE_DELAY = 2
ALLOCATIONS_KEY = "allocations"
@ -26,6 +26,40 @@ IID_MIN = 1
IID_MAX = 18446744073709551615
ACCESSORY_INFORMATION_SERVICE = "3E"
class IIDStorage(Store):
"""Storage class for IIDManager."""
async def _async_migrate_func(
self,
old_major_version: int,
old_minor_version: int,
old_data: dict,
):
"""Migrate to the new version."""
if old_major_version == 1:
# Convert v1 to v2 format which uses a unique iid set per accessory
# instead of per pairing since we need the ACCESSORY_INFORMATION_SERVICE
# to always have iid 1 for each bridged accessory as well as the bridge
old_allocations: dict[str, int] = old_data.pop(ALLOCATIONS_KEY, {})
new_allocation: dict[str, dict[str, int]] = {}
old_data[ALLOCATIONS_KEY] = new_allocation
for allocation_key, iid in old_allocations.items():
aid_str, new_allocation_key = allocation_key.split("_", 1)
service_type, _, char_type, *_ = new_allocation_key.split("_")
accessory_allocation = new_allocation.setdefault(aid_str, {})
if service_type == ACCESSORY_INFORMATION_SERVICE and not char_type:
accessory_allocation[new_allocation_key] = 1
elif iid != 1:
accessory_allocation[new_allocation_key] = iid
return old_data
raise NotImplementedError
class AccessoryIIDStorage:
"""
Provide stable allocation of IIDs for the lifetime of an accessory.
@ -37,15 +71,15 @@ class AccessoryIIDStorage:
def __init__(self, hass: HomeAssistant, entry_id: str) -> None:
"""Create a new iid store."""
self.hass = hass
self.allocations: dict[str, int] = {}
self.allocated_iids: list[int] = []
self.allocations: dict[str, dict[str, int]] = {}
self.allocated_iids: dict[str, list[int]] = {}
self.entry_id = entry_id
self.store: Store | None = None
self.store: IIDStorage | None = None
async def async_initialize(self) -> None:
"""Load the latest IID data."""
iid_store = get_iid_storage_filename_for_entry_id(self.entry_id)
self.store = Store(self.hass, IID_MANAGER_STORAGE_VERSION, iid_store)
self.store = IIDStorage(self.hass, IID_MANAGER_STORAGE_VERSION, iid_store)
if not (raw_storage := await self.store.async_load()):
# There is no data about iid allocations yet
@ -53,7 +87,8 @@ class AccessoryIIDStorage:
assert isinstance(raw_storage, dict)
self.allocations = raw_storage.get(ALLOCATIONS_KEY, {})
self.allocated_iids = sorted(self.allocations.values())
for aid_str, allocations in self.allocations.items():
self.allocated_iids[aid_str] = sorted(allocations.values())
def get_or_allocate_iid(
self,
@ -68,16 +103,25 @@ class AccessoryIIDStorage:
char_hap_type: str | None = uuid_to_hap_type(char_uuid) if char_uuid else None
# Allocation key must be a string since we are saving it to JSON
allocation_key = (
f'{aid}_{service_hap_type}_{service_unique_id or ""}_'
f'{service_hap_type}_{service_unique_id or ""}_'
f'{char_hap_type or ""}_{char_unique_id or ""}'
)
if allocation_key in self.allocations:
return self.allocations[allocation_key]
next_iid = self.allocated_iids[-1] + 1 if self.allocated_iids else 1
self.allocations[allocation_key] = next_iid
self.allocated_iids.append(next_iid)
# AID must be a string since JSON keys cannot be int
aid_str = str(aid)
accessory_allocation = self.allocations.setdefault(aid_str, {})
accessory_allocated_iids = self.allocated_iids.setdefault(aid_str, [1])
if service_hap_type == ACCESSORY_INFORMATION_SERVICE and char_uuid is None:
return 1
if allocation_key in accessory_allocation:
return accessory_allocation[allocation_key]
if accessory_allocated_iids:
allocated_iid = accessory_allocated_iids[-1] + 1
else:
allocated_iid = 2
accessory_allocation[allocation_key] = allocated_iid
accessory_allocated_iids.append(allocated_iid)
self._async_schedule_save()
return next_iid
return allocated_iid
@callback
def _async_schedule_save(self) -> None:
@ -91,6 +135,6 @@ class AccessoryIIDStorage:
return await self.store.async_save(self._data_to_save())
@callback
def _data_to_save(self) -> dict[str, dict[str, int]]:
def _data_to_save(self) -> dict[str, dict[str, dict[str, int]]]:
"""Return data of entity map to store in a file."""
return {ALLOCATIONS_KEY: self.allocations}

View File

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

View File

@ -3,7 +3,7 @@
"name": "HomeKit Controller",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"requirements": ["aiohomekit==2.2.14"],
"requirements": ["aiohomekit==2.2.18"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }],
"dependencies": ["bluetooth", "zeroconf"],

View File

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

View File

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

View File

@ -14,8 +14,11 @@ from awesomeversion import AwesomeVersion
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT,
ATTR_COLOR_NAME,
ATTR_COLOR_TEMP_KELVIN,
ATTR_HS_COLOR,
ATTR_KELVIN,
ATTR_RGB_COLOR,
ATTR_XY_COLOR,
)
@ -24,7 +27,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
import homeassistant.util.color as color_util
from .const import DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT
from .const import _LOGGER, DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT
FIX_MAC_FW = AwesomeVersion("3.70")
@ -80,6 +83,17 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] |
"""
hue, saturation, brightness, kelvin = [None] * 4
if (color_name := kwargs.get(ATTR_COLOR_NAME)) is not None:
try:
hue, saturation = color_util.color_RGB_to_hs(
*color_util.color_name_to_rgb(color_name)
)
except ValueError:
_LOGGER.warning(
"Got unknown color %s, falling back to neutral white", color_name
)
hue, saturation = (0, 0)
if ATTR_HS_COLOR in kwargs:
hue, saturation = kwargs[ATTR_HS_COLOR]
elif ATTR_RGB_COLOR in kwargs:
@ -93,6 +107,13 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] |
saturation = int(saturation / 100 * 65535)
kelvin = 3500
if ATTR_KELVIN in kwargs:
_LOGGER.warning(
"The 'kelvin' parameter is deprecated. Please use 'color_temp_kelvin' for all service calls"
)
kelvin = kwargs.pop(ATTR_KELVIN)
saturation = 0
if ATTR_COLOR_TEMP_KELVIN in kwargs:
kelvin = kwargs.pop(ATTR_COLOR_TEMP_KELVIN)
saturation = 0
@ -100,6 +121,9 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] |
if ATTR_BRIGHTNESS in kwargs:
brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS])
if ATTR_BRIGHTNESS_PCT in kwargs:
brightness = convert_8_to_16(round(255 * kwargs[ATTR_BRIGHTNESS_PCT] / 100))
hsbk = [hue, saturation, brightness, kelvin]
return None if hsbk == [None] * 4 else hsbk

View File

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

View File

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

View File

@ -80,6 +80,11 @@ class NexiaThermostatEntity(NexiaEntity):
self.hass, f"{SIGNAL_THERMOSTAT_UPDATE}-{self._thermostat.thermostat_id}"
)
@property
def available(self) -> bool:
"""Return True if thermostat is available and data is available."""
return super().available and self._thermostat.is_online
class NexiaThermostatZoneEntity(NexiaThermostatEntity):
"""Base class for nexia devices attached to a thermostat."""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -89,6 +89,13 @@ COMBINED_SCHEMA = vol.Schema(
)
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.All(cv.ensure_list, [COMBINED_SCHEMA])},
{
DOMAIN: vol.All(
# convert empty dict to empty list
lambda x: [] if x == {} else x,
cv.ensure_list,
[COMBINED_SCHEMA],
)
},
extra=vol.ALLOW_EXTRA,
)

View File

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

View File

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

View File

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

View File

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

View File

@ -5,13 +5,13 @@ from datetime import datetime, timedelta
from typing import Any, cast
from aiohttp.web import Request, WebSocketResponse
from aioshelly.block_device import BLOCK_VALUE_UNIT, COAP, Block, BlockDevice
from aioshelly.block_device import COAP, Block, BlockDevice
from aioshelly.const import MODEL_NAMES
from aioshelly.rpc_device import RpcDevice, WsServer
from homeassistant.components.http import HomeAssistantView
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry, entity_registry, singleton
from homeassistant.helpers.typing import EventType
@ -43,13 +43,6 @@ def async_remove_shelly_entity(
entity_reg.async_remove(entity_id)
def temperature_unit(block_info: dict[str, Any]) -> str:
"""Detect temperature unit."""
if block_info[BLOCK_VALUE_UNIT] == "F":
return TEMP_FAHRENHEIT
return TEMP_CELSIUS
def get_block_device_name(device: BlockDevice) -> str:
"""Naming for device."""
return cast(str, device.settings["name"] or device.settings["device"]["hostname"])
@ -364,13 +357,6 @@ def is_rpc_channel_type_light(config: dict[str, Any], channel: int) -> bool:
return con_types is not None and con_types[channel].lower().startswith("light")
def is_rpc_device_externally_powered(
config: dict[str, Any], status: dict[str, Any], key: str
) -> bool:
"""Return true if device has external power instead of battery."""
return cast(bool, status[key]["external"]["present"])
def get_rpc_input_triggers(device: RpcDevice) -> list[tuple[str, str]]:
"""Return list of input triggers for RPC device."""
triggers = []

View File

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

View File

@ -2,7 +2,7 @@
"domain": "ssdp",
"name": "Simple Service Discovery Protocol (SSDP)",
"documentation": "https://www.home-assistant.io/integrations/ssdp",
"requirements": ["async-upnp-client==0.32.1"],
"requirements": ["async-upnp-client==0.32.2"],
"dependencies": ["network"],
"after_dependencies": ["zeroconf"],
"codeowners": [],

View File

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

View File

@ -20,6 +20,7 @@ from .const import (
CONFIG_ENTRY_ST,
CONFIG_ENTRY_UDN,
DOMAIN,
DOMAIN_DISCOVERIES,
LOGGER,
ST_IGD_V1,
ST_IGD_V2,
@ -47,7 +48,7 @@ def _is_complete_discovery(discovery_info: ssdp.SsdpServiceInfo) -> bool:
)
async def _async_discover_igd_devices(
async def _async_discovered_igd_devices(
hass: HomeAssistant,
) -> list[ssdp.SsdpServiceInfo]:
"""Discovery IGD devices."""
@ -79,9 +80,19 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
# - ssdp(discovery_info) --> ssdp_confirm(None) --> ssdp_confirm({}) --> create_entry()
# - user(None): scan --> user({...}) --> create_entry()
def __init__(self) -> None:
"""Initialize the UPnP/IGD config flow."""
self._discoveries: list[SsdpServiceInfo] | None = None
@property
def _discoveries(self) -> dict[str, SsdpServiceInfo]:
"""Get current discoveries."""
domain_data: dict = self.hass.data.setdefault(DOMAIN, {})
return domain_data.setdefault(DOMAIN_DISCOVERIES, {})
def _add_discovery(self, discovery: SsdpServiceInfo) -> None:
"""Add a discovery."""
self._discoveries[discovery.ssdp_usn] = discovery
def _remove_discovery(self, usn: str) -> SsdpServiceInfo:
"""Remove a discovery by its USN/unique_id."""
return self._discoveries.pop(usn)
async def async_step_user(
self, user_input: Mapping[str, Any] | None = None
@ -95,7 +106,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
discovery = next(
iter(
discovery
for discovery in self._discoveries
for discovery in self._discoveries.values()
if discovery.ssdp_usn == user_input["unique_id"]
)
)
@ -103,21 +114,19 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return await self._async_create_entry_from_discovery(discovery)
# Discover devices.
discoveries = await _async_discover_igd_devices(self.hass)
discoveries = await _async_discovered_igd_devices(self.hass)
# Store discoveries which have not been configured.
current_unique_ids = {
entry.unique_id for entry in self._async_current_entries()
}
self._discoveries = [
discovery
for discovery in discoveries
for discovery in discoveries:
if (
_is_complete_discovery(discovery)
and _is_igd_device(discovery)
and discovery.ssdp_usn not in current_unique_ids
)
]
):
self._add_discovery(discovery)
# Ensure anything to add.
if not self._discoveries:
@ -128,7 +137,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
vol.Required("unique_id"): vol.In(
{
discovery.ssdp_usn: _friendly_name_from_discovery(discovery)
for discovery in self._discoveries
for discovery in self._discoveries.values()
}
),
}
@ -163,12 +172,13 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
mac_address = await _async_mac_address_from_discovery(self.hass, discovery_info)
host = discovery_info.ssdp_headers["_host"]
self._abort_if_unique_id_configured(
# Store mac address for older entries.
# Store mac address and other data for older entries.
# The location is stored in the config entry such that when the location changes, the entry is reloaded.
updates={
CONFIG_ENTRY_MAC_ADDRESS: mac_address,
CONFIG_ENTRY_LOCATION: discovery_info.ssdp_location,
CONFIG_ENTRY_HOST: host,
CONFIG_ENTRY_ST: discovery_info.ssdp_st,
},
)
@ -204,7 +214,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="config_entry_updated")
# Store discovery.
self._discoveries = [discovery_info]
self._add_discovery(discovery_info)
# Ensure user recognizable.
self.context["title_placeholders"] = {
@ -221,10 +231,27 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is None:
return self.async_show_form(step_id="ssdp_confirm")
assert self._discoveries
discovery = self._discoveries[0]
assert self.unique_id
discovery = self._remove_discovery(self.unique_id)
return await self._async_create_entry_from_discovery(discovery)
async def async_step_ignore(self, user_input: dict[str, Any]) -> FlowResult:
"""Ignore this config flow."""
usn = user_input["unique_id"]
discovery = self._remove_discovery(usn)
mac_address = await _async_mac_address_from_discovery(self.hass, discovery)
data = {
CONFIG_ENTRY_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN],
CONFIG_ENTRY_ST: discovery.ssdp_st,
CONFIG_ENTRY_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN],
CONFIG_ENTRY_MAC_ADDRESS: mac_address,
CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"],
CONFIG_ENTRY_LOCATION: discovery.ssdp_location,
}
await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False)
return self.async_create_entry(title=user_input["title"], data=data)
async def _async_create_entry_from_discovery(
self,
discovery: SsdpServiceInfo,
@ -243,5 +270,6 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
CONFIG_ENTRY_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN],
CONFIG_ENTRY_LOCATION: discovery.ssdp_location,
CONFIG_ENTRY_MAC_ADDRESS: mac_address,
CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"],
}
return self.async_create_entry(title=title, data=data)

View File

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

View File

@ -3,7 +3,7 @@
"name": "UPnP/IGD",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/upnp",
"requirements": ["async-upnp-client==0.32.1", "getmac==0.8.2"],
"requirements": ["async-upnp-client==0.32.2", "getmac==0.8.2"],
"dependencies": ["network", "ssdp"],
"codeowners": ["@StevenLooman"],
"ssdp": [

View File

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

View File

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

View File

@ -2,7 +2,7 @@
"domain": "yeelight",
"name": "Yeelight",
"documentation": "https://www.home-assistant.io/integrations/yeelight",
"requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.1"],
"requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.2"],
"codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"],
"config_flow": true,
"dependencies": ["network"],

View File

@ -552,12 +552,20 @@ def _first_non_link_local_address(
"""Return the first ipv6 or non-link local ipv4 address, preferring IPv4."""
for address in addresses:
ip_addr = ip_address(address)
if not ip_addr.is_link_local and ip_addr.version == 4:
if (
not ip_addr.is_link_local
and not ip_addr.is_unspecified
and ip_addr.version == 4
):
return str(ip_addr)
# If we didn't find a good IPv4 address, check for IPv6 addresses.
for address in addresses:
ip_addr = ip_address(address)
if not ip_addr.is_link_local and ip_addr.version == 6:
if (
not ip_addr.is_link_local
and not ip_addr.is_unspecified
and ip_addr.version == 6
):
return str(ip_addr)
return None

View File

@ -98,12 +98,26 @@ class ColorChannel(ZigbeeChannel):
@property
def min_mireds(self) -> int:
"""Return the coldest color_temp that this channel supports."""
return self.cluster.get("color_temp_physical_min", self.MIN_MIREDS)
min_mireds = self.cluster.get("color_temp_physical_min", self.MIN_MIREDS)
if min_mireds == 0:
self.warning(
"[Min mireds is 0, setting to %s] Please open an issue on the quirks repo to have this device corrected",
self.MIN_MIREDS,
)
min_mireds = self.MIN_MIREDS
return min_mireds
@property
def max_mireds(self) -> int:
"""Return the warmest color_temp that this channel supports."""
return self.cluster.get("color_temp_physical_max", self.MAX_MIREDS)
max_mireds = self.cluster.get("color_temp_physical_max", self.MAX_MIREDS)
if max_mireds == 0:
self.warning(
"[Max mireds is 0, setting to %s] Please open an issue on the quirks repo to have this device corrected",
self.MAX_MIREDS,
)
max_mireds = self.MAX_MIREDS
return max_mireds
@property
def hs_supported(self) -> bool:

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from zigpy import types
from zhaquirks.inovelli.types import AllLEDEffectType, SingleLEDEffectType
from zigpy.exceptions import ZigbeeException
import zigpy.zcl
@ -183,59 +183,47 @@ class InovelliNotificationChannel(ClientChannel):
class InovelliConfigEntityChannel(ZigbeeChannel):
"""Inovelli Configuration Entity channel."""
class LEDEffectType(types.enum8):
"""Effect type for Inovelli Blue Series switch."""
Off = 0x00
Solid = 0x01
Fast_Blink = 0x02
Slow_Blink = 0x03
Pulse = 0x04
Chase = 0x05
Open_Close = 0x06
Small_To_Big = 0x07
Clear = 0xFF
REPORT_CONFIG = ()
ZCL_INIT_ATTRS = {
"dimming_speed_up_remote": False,
"dimming_speed_up_local": False,
"ramp_rate_off_to_on_local": False,
"ramp_rate_off_to_on_remote": False,
"dimming_speed_down_remote": False,
"dimming_speed_down_local": False,
"ramp_rate_on_to_off_local": False,
"ramp_rate_on_to_off_remote": False,
"minimum_level": False,
"maximum_level": False,
"invert_switch": False,
"auto_off_timer": False,
"default_level_local": False,
"default_level_remote": False,
"state_after_power_restored": False,
"load_level_indicator_timeout": False,
"active_power_reports": False,
"periodic_power_and_energy_reports": False,
"active_energy_reports": False,
"dimming_speed_up_remote": True,
"dimming_speed_up_local": True,
"ramp_rate_off_to_on_local": True,
"ramp_rate_off_to_on_remote": True,
"dimming_speed_down_remote": True,
"dimming_speed_down_local": True,
"ramp_rate_on_to_off_local": True,
"ramp_rate_on_to_off_remote": True,
"minimum_level": True,
"maximum_level": True,
"invert_switch": True,
"auto_off_timer": True,
"default_level_local": True,
"default_level_remote": True,
"state_after_power_restored": True,
"load_level_indicator_timeout": True,
"active_power_reports": True,
"periodic_power_and_energy_reports": True,
"active_energy_reports": True,
"power_type": False,
"switch_type": False,
"button_delay": False,
"smart_bulb_mode": False,
"double_tap_up_for_full_brightness": False,
"led_color_when_on": False,
"led_color_when_off": False,
"led_intensity_when_on": False,
"led_intensity_when_off": False,
"double_tap_up_for_full_brightness": True,
"led_color_when_on": True,
"led_color_when_off": True,
"led_intensity_when_on": True,
"led_intensity_when_off": True,
"local_protection": False,
"output_mode": False,
"on_off_led_mode": False,
"firmware_progress_led": False,
"relay_click_in_on_off_mode": False,
"on_off_led_mode": True,
"firmware_progress_led": True,
"relay_click_in_on_off_mode": True,
"disable_clear_notifications_double_tap": True,
}
async def issue_all_led_effect(
self,
effect_type: LEDEffectType | int = LEDEffectType.Fast_Blink,
effect_type: AllLEDEffectType | int = AllLEDEffectType.Fast_Blink,
color: int = 200,
level: int = 100,
duration: int = 3,
@ -251,7 +239,7 @@ class InovelliConfigEntityChannel(ZigbeeChannel):
async def issue_individual_led_effect(
self,
led_number: int = 1,
effect_type: LEDEffectType | int = LEDEffectType.Fast_Blink,
effect_type: SingleLEDEffectType | int = SingleLEDEffectType.Fast_Blink,
color: int = 200,
level: int = 100,
duration: int = 3,

View File

@ -13,7 +13,7 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import DOMAIN
from .api import SERVICE_WARNING_DEVICE_SQUAWK, SERVICE_WARNING_DEVICE_WARN
from .core.channels.manufacturerspecific import InovelliConfigEntityChannel
from .core.channels.manufacturerspecific import AllLEDEffectType, SingleLEDEffectType
from .core.const import CHANNEL_IAS_WD, CHANNEL_INOVELLI
from .core.helpers import async_get_zha_device
@ -40,9 +40,7 @@ INOVELLI_ALL_LED_EFFECT_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): INOVELLI_ALL_LED_EFFECT,
vol.Required(CONF_DOMAIN): DOMAIN,
vol.Required(
"effect_type"
): InovelliConfigEntityChannel.LEDEffectType.__getitem__,
vol.Required("effect_type"): AllLEDEffectType.__getitem__,
vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)),
vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)),
@ -52,10 +50,16 @@ INOVELLI_ALL_LED_EFFECT_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA = INOVELLI_ALL_LED_EFFECT_SCHEMA.extend(
{
vol.Required(CONF_TYPE): INOVELLI_INDIVIDUAL_LED_EFFECT,
vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(1, 7)),
vol.Required("effect_type"): SingleLEDEffectType.__getitem__,
vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)),
}
)
ACTION_SCHEMA_MAP = {
INOVELLI_ALL_LED_EFFECT: INOVELLI_ALL_LED_EFFECT_SCHEMA,
INOVELLI_INDIVIDUAL_LED_EFFECT: INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA,
}
ACTION_SCHEMA = vol.Any(
INOVELLI_ALL_LED_EFFECT_SCHEMA,
INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA,
@ -83,9 +87,7 @@ DEVICE_ACTION_TYPES = {
DEVICE_ACTION_SCHEMAS = {
INOVELLI_ALL_LED_EFFECT: vol.Schema(
{
vol.Required("effect_type"): vol.In(
InovelliConfigEntityChannel.LEDEffectType.__members__.keys()
),
vol.Required("effect_type"): vol.In(AllLEDEffectType.__members__.keys()),
vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)),
vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)),
@ -94,9 +96,7 @@ DEVICE_ACTION_SCHEMAS = {
INOVELLI_INDIVIDUAL_LED_EFFECT: vol.Schema(
{
vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)),
vol.Required("effect_type"): vol.In(
InovelliConfigEntityChannel.LEDEffectType.__members__.keys()
),
vol.Required("effect_type"): vol.In(SingleLEDEffectType.__members__.keys()),
vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)),
vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)),
@ -127,6 +127,15 @@ async def async_call_action_from_config(
)
async def async_validate_action_config(
hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
schema = ACTION_SCHEMA_MAP.get(config[CONF_TYPE], DEFAULT_ACTION_SCHEMA)
config = schema(config)
return config
async def async_get_actions(
hass: HomeAssistant, device_id: str
) -> list[dict[str, str]]:

View File

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

View File

@ -418,3 +418,15 @@ class InovelliRelayClickInOnOffMode(
_zcl_attribute: str = "relay_click_in_on_off_mode"
_attr_name: str = "Disable relay click in on off mode"
@CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI,
)
class InovelliDisableDoubleTapClearNotificationsMode(
ZHASwitchConfigurationEntity, id_suffix="disable_clear_notifications_double_tap"
):
"""Inovelli disable clear notifications double tap control."""
_zcl_attribute: str = "disable_clear_notifications_double_tap"
_attr_name: str = "Disable config 2x tap to clear notifications"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@
from dataclasses import asdict
from unittest.mock import patch
from elkm1_lib.discovery import ElkSystem
import pytest
from homeassistant import config_entries
@ -1317,3 +1318,285 @@ async def test_discovered_by_dhcp_no_udp_response(hass):
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "cannot_connect"
async def test_multiple_instances_with_discovery(hass):
"""Test we can setup a secure elk."""
elk_discovery_1 = ElkSystem("aa:bb:cc:dd:ee:ff", "127.0.0.1", 2601)
elk_discovery_2 = ElkSystem("aa:bb:cc:dd:ee:fe", "127.0.0.2", 2601)
with _patch_discovery(device=elk_discovery_1):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == "form"
assert not result["errors"]
assert result["step_id"] == "user"
mocked_elk = mock_elk(invalid_auth=False, sync_complete=True)
with _patch_elk(elk=mocked_elk):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"device": elk_discovery_1.mac_address},
)
await hass.async_block_till_done()
with _patch_discovery(device=elk_discovery_1), _patch_elk(elk=mocked_elk), patch(
"homeassistant.components.elkm1.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.elkm1.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
"username": "test-username",
"password": "test-password",
},
)
await hass.async_block_till_done()
assert result3["type"] == "create_entry"
assert result3["title"] == "ElkM1 ddeeff"
assert result3["data"] == {
"auto_configure": True,
"host": "elks://127.0.0.1",
"password": "test-password",
"prefix": "",
"username": "test-username",
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
# Now try to add another instance with the different discovery info
with _patch_discovery(device=elk_discovery_2):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == "form"
assert not result["errors"]
assert result["step_id"] == "user"
mocked_elk = mock_elk(invalid_auth=False, sync_complete=True)
with _patch_elk(elk=mocked_elk):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"device": elk_discovery_2.mac_address},
)
await hass.async_block_till_done()
with _patch_discovery(device=elk_discovery_2), _patch_elk(elk=mocked_elk), patch(
"homeassistant.components.elkm1.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
"username": "test-username",
"password": "test-password",
},
)
await hass.async_block_till_done()
assert result3["type"] == "create_entry"
assert result3["title"] == "ElkM1 ddeefe"
assert result3["data"] == {
"auto_configure": True,
"host": "elks://127.0.0.2",
"password": "test-password",
"prefix": "ddeefe",
"username": "test-username",
}
assert len(mock_setup_entry.mock_calls) == 1
# Finally, try to add another instance manually with no discovery info
with _patch_discovery(no_device=True):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == "form"
assert result["errors"] == {}
assert result["step_id"] == "manual_connection"
mocked_elk = mock_elk(invalid_auth=None, sync_complete=True)
with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch(
"homeassistant.components.elkm1.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"protocol": "non-secure",
"address": "1.2.3.4",
"prefix": "guest_house",
},
)
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["title"] == "guest_house"
assert result2["data"] == {
"auto_configure": True,
"host": "elk://1.2.3.4",
"prefix": "guest_house",
"username": "",
"password": "",
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_multiple_instances_with_tls_v12(hass):
"""Test we can setup a secure elk with tls v1_2."""
elk_discovery_1 = ElkSystem("aa:bb:cc:dd:ee:ff", "127.0.0.1", 2601)
elk_discovery_2 = ElkSystem("aa:bb:cc:dd:ee:fe", "127.0.0.2", 2601)
with _patch_discovery(device=elk_discovery_1):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == "form"
assert not result["errors"]
assert result["step_id"] == "user"
mocked_elk = mock_elk(invalid_auth=False, sync_complete=True)
with _patch_elk(elk=mocked_elk):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"device": elk_discovery_1.mac_address},
)
await hass.async_block_till_done()
assert result2["type"] == "form"
assert not result["errors"]
assert result2["step_id"] == "discovered_connection"
with _patch_discovery(device=elk_discovery_1), _patch_elk(elk=mocked_elk), patch(
"homeassistant.components.elkm1.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.elkm1.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
"protocol": "TLS 1.2",
"username": "test-username",
"password": "test-password",
},
)
await hass.async_block_till_done()
assert result3["type"] == "create_entry"
assert result3["title"] == "ElkM1 ddeeff"
assert result3["data"] == {
"auto_configure": True,
"host": "elksv1_2://127.0.0.1",
"password": "test-password",
"prefix": "",
"username": "test-username",
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
# Now try to add another instance with the different discovery info
with _patch_discovery(device=elk_discovery_2):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == "form"
assert not result["errors"]
assert result["step_id"] == "user"
mocked_elk = mock_elk(invalid_auth=False, sync_complete=True)
with _patch_elk(elk=mocked_elk):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"device": elk_discovery_2.mac_address},
)
await hass.async_block_till_done()
with _patch_discovery(device=elk_discovery_2), _patch_elk(elk=mocked_elk), patch(
"homeassistant.components.elkm1.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
"protocol": "TLS 1.2",
"username": "test-username",
"password": "test-password",
},
)
await hass.async_block_till_done()
assert result3["type"] == "create_entry"
assert result3["title"] == "ElkM1 ddeefe"
assert result3["data"] == {
"auto_configure": True,
"host": "elksv1_2://127.0.0.2",
"password": "test-password",
"prefix": "ddeefe",
"username": "test-username",
}
assert len(mock_setup_entry.mock_calls) == 1
# Finally, try to add another instance manually with no discovery info
with _patch_discovery(no_device=True):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == "form"
assert result["errors"] == {}
assert result["step_id"] == "manual_connection"
mocked_elk = mock_elk(invalid_auth=False, sync_complete=True)
with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch(
"homeassistant.components.elkm1.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"protocol": "TLS 1.2",
"address": "1.2.3.4",
"prefix": "guest_house",
"password": "test-password",
"username": "test-username",
},
)
await hass.async_block_till_done()
import pprint
pprint.pprint(result2)
assert result2["type"] == "create_entry"
assert result2["title"] == "guest_house"
assert result2["data"] == {
"auto_configure": True,
"host": "elksv1_2://1.2.3.4",
"prefix": "guest_house",
"password": "test-password",
"username": "test-username",
}
assert len(mock_setup_entry.mock_calls) == 1

View File

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

View File

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

View File

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

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,
HomeBridge,
HomeDriver,
HomeIIDManager,
)
from homeassistant.components.homekit.const import (
ATTR_DISPLAY_NAME,
@ -724,7 +723,7 @@ def test_home_driver(iid_storage):
"entry_id",
"name",
"title",
iid_manager=HomeIIDManager(iid_storage),
iid_storage=iid_storage,
address=ip_address,
port=port,
persist_file=path,
@ -752,22 +751,3 @@ def test_home_driver(iid_storage):
mock_unpair.assert_called_with("client_uuid")
mock_show_msg.assert_called_with("hass", "entry_id", "title (any)", pin, "X-HM://0")
async def test_iid_collision_raises(hass, hk_driver):
"""Test iid collision raises.
If we try to allocate the same IID to the an accessory twice, we should
raise an exception.
"""
entity_id = "light.accessory"
entity_id2 = "light.accessory2"
hass.states.async_set(entity_id, STATE_OFF)
hass.states.async_set(entity_id2, STATE_OFF)
HomeAccessory(hass, hk_driver, "Home Accessory", entity_id, 2, {})
with pytest.raises(RuntimeError):
HomeAccessory(hass, hk_driver, "Home Accessory", entity_id2, 2, {})

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)
assert diag == {
"bridge": {},
"iid_storage": {
"1": {
"3E__14_": 2,
"3E__20_": 3,
"3E__21_": 4,
"3E__23_": 5,
"3E__30_": 6,
"3E__52_": 7,
"A2__37_": 9,
"A2___": 8,
}
},
"accessories": [
{
"aid": 1,
@ -257,6 +269,20 @@ async def test_config_entry_accessory(
},
"config_version": 2,
"pairing_id": ANY,
"iid_storage": {
"1": {
"3E__14_": 2,
"3E__20_": 3,
"3E__21_": 4,
"3E__23_": 5,
"3E__30_": 6,
"3E__52_": 7,
"43__25_": 11,
"43___": 10,
"A2__37_": 9,
"A2___": 8,
}
},
"status": 1,
}
with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch(

View File

@ -262,7 +262,7 @@ async def test_homekit_setup(hass, hk_driver, mock_async_zeroconf):
async_zeroconf_instance=zeroconf_mock,
zeroconf_server=f"{uuid}-hap.local.",
loader=ANY,
iid_manager=ANY,
iid_storage=ANY,
)
assert homekit.driver.safe_mode is False
@ -306,7 +306,7 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_async_zeroconf):
async_zeroconf_instance=mock_async_zeroconf,
zeroconf_server=f"{uuid}-hap.local.",
loader=ANY,
iid_manager=ANY,
iid_storage=ANY,
)
@ -350,7 +350,7 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_async_zeroconf):
async_zeroconf_instance=async_zeroconf_instance,
zeroconf_server=f"{uuid}-hap.local.",
loader=ANY,
iid_manager=ANY,
iid_storage=ANY,
)

View File

@ -1,6 +1,5 @@
"""Tests for the HomeKit IID manager."""
from uuid import UUID
from homeassistant.components.homekit.const import DOMAIN
@ -8,9 +7,10 @@ from homeassistant.components.homekit.iidmanager import (
AccessoryIIDStorage,
get_iid_storage_filename_for_entry_id,
)
from homeassistant.helpers.json import json_loads
from homeassistant.util.uuid import random_uuid_hex
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, load_fixture
async def test_iid_generation_and_restore(hass, iid_storage, hass_storage):
@ -77,9 +77,6 @@ async def test_iid_generation_and_restore(hass, iid_storage, hass_storage):
unique_service_unique_char_new_aid_iid1
== unique_service_unique_char_new_aid_iid2
)
assert unique_service_unique_char_new_aid_iid1 != iid1
assert unique_service_unique_char_new_aid_iid1 != unique_service_unique_char_iid1
await iid_storage.async_save()
iid_storage2 = AccessoryIIDStorage(hass, entry.entry_id)
@ -99,3 +96,85 @@ async def test_iid_storage_filename(hass, iid_storage, hass_storage):
assert iid_storage.store.path.endswith(
get_iid_storage_filename_for_entry_id(entry.entry_id)
)
async def test_iid_migration_to_v2(hass, iid_storage, hass_storage):
"""Test iid storage migration."""
v1_iids = json_loads(load_fixture("iids_v1", DOMAIN))
v2_iids = json_loads(load_fixture("iids_v2", DOMAIN))
hass_storage["homekit.v1.iids"] = v1_iids
hass_storage["homekit.v2.iids"] = v2_iids
iid_storage_v2 = AccessoryIIDStorage(hass, "v1")
await iid_storage_v2.async_initialize()
iid_storage_v1 = AccessoryIIDStorage(hass, "v2")
await iid_storage_v1.async_initialize()
assert iid_storage_v1.allocations == iid_storage_v2.allocations
assert iid_storage_v1.allocated_iids == iid_storage_v2.allocated_iids
assert len(iid_storage_v2.allocations) == 12
for allocations in iid_storage_v2.allocations.values():
assert allocations["3E___"] == 1
async def test_iid_migration_to_v2_with_underscore(hass, iid_storage, hass_storage):
"""Test iid storage migration with underscore."""
v1_iids = json_loads(load_fixture("iids_v1_with_underscore", DOMAIN))
v2_iids = json_loads(load_fixture("iids_v2_with_underscore", DOMAIN))
hass_storage["homekit.v1_with_underscore.iids"] = v1_iids
hass_storage["homekit.v2_with_underscore.iids"] = v2_iids
iid_storage_v2 = AccessoryIIDStorage(hass, "v1_with_underscore")
await iid_storage_v2.async_initialize()
iid_storage_v1 = AccessoryIIDStorage(hass, "v2_with_underscore")
await iid_storage_v1.async_initialize()
assert iid_storage_v1.allocations == iid_storage_v2.allocations
assert iid_storage_v1.allocated_iids == iid_storage_v2.allocated_iids
assert len(iid_storage_v2.allocations) == 2
for allocations in iid_storage_v2.allocations.values():
assert allocations["3E___"] == 1
async def test_iid_generation_and_restore_v2(hass, iid_storage, hass_storage):
"""Test generating iids and restoring them from storage."""
entry = MockConfigEntry(domain=DOMAIN)
iid_storage = AccessoryIIDStorage(hass, entry.entry_id)
await iid_storage.async_initialize()
not_accessory_info_service_iid = iid_storage.get_or_allocate_iid(
1, "000000AA-0000-1000-8000-0026BB765291", None, None, None
)
assert not_accessory_info_service_iid == 2
assert iid_storage.allocated_iids == {"1": [1, 2]}
not_accessory_info_service_iid_2 = iid_storage.get_or_allocate_iid(
1, "000000BB-0000-1000-8000-0026BB765291", None, None, None
)
assert not_accessory_info_service_iid_2 == 3
assert iid_storage.allocated_iids == {"1": [1, 2, 3]}
not_accessory_info_service_iid_2 = iid_storage.get_or_allocate_iid(
1, "000000BB-0000-1000-8000-0026BB765291", None, None, None
)
assert not_accessory_info_service_iid_2 == 3
assert iid_storage.allocated_iids == {"1": [1, 2, 3]}
accessory_info_service_iid = iid_storage.get_or_allocate_iid(
1, "0000003E-0000-1000-8000-0026BB765291", None, None, None
)
assert accessory_info_service_iid == 1
assert iid_storage.allocated_iids == {"1": [1, 2, 3]}
accessory_info_service_iid = iid_storage.get_or_allocate_iid(
1, "0000003E-0000-1000-8000-0026BB765291", None, None, None
)
assert accessory_info_service_iid == 1
assert iid_storage.allocated_iids == {"1": [1, 2, 3]}
accessory_info_service_iid = iid_storage.get_or_allocate_iid(
2, "0000003E-0000-1000-8000-0026BB765291", None, None, None
)
assert accessory_info_service_iid == 1
assert iid_storage.allocated_iids == {"1": [1, 2, 3], "2": [1]}

View File

@ -21,11 +21,14 @@ from homeassistant.components.lifx.manager import (
)
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT,
ATTR_COLOR_MODE,
ATTR_COLOR_NAME,
ATTR_COLOR_TEMP,
ATTR_COLOR_TEMP_KELVIN,
ATTR_EFFECT,
ATTR_HS_COLOR,
ATTR_KELVIN,
ATTR_RGB_COLOR,
ATTR_SUPPORTED_COLOR_MODES,
ATTR_TRANSITION,
@ -1397,6 +1400,131 @@ async def test_transitions_color_bulb(hass: HomeAssistant) -> None:
bulb.set_color.reset_mock()
async def test_lifx_set_state_color(hass: HomeAssistant) -> None:
"""Test lifx.set_state works with color names and RGB."""
config_entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL
)
config_entry.add_to_hass(hass)
bulb = _mocked_bulb_new_firmware()
bulb.power_level = 65535
bulb.color = [32000, None, 32000, 2700]
with _patch_discovery(device=bulb), _patch_config_flow_try_connect(
device=bulb
), _patch_device(device=bulb):
await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}})
await hass.async_block_till_done()
entity_id = "light.my_bulb"
# brightness should convert from 8 to 16 bits
await hass.services.async_call(
DOMAIN,
"set_state",
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255},
blocking=True,
)
assert bulb.set_color.calls[0][0][0] == [32000, None, 65535, 2700]
bulb.set_color.reset_mock()
# brightness_pct should convert into 16 bit
await hass.services.async_call(
DOMAIN,
"set_state",
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 90},
blocking=True,
)
assert bulb.set_color.calls[0][0][0] == [32000, None, 59110, 2700]
bulb.set_color.reset_mock()
# color name should turn into hue, saturation
await hass.services.async_call(
DOMAIN,
"set_state",
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_NAME: "red", ATTR_BRIGHTNESS_PCT: 100},
blocking=True,
)
assert bulb.set_color.calls[0][0][0] == [0, 65535, 65535, 3500]
bulb.set_color.reset_mock()
# unknown color name should reset back to neutral white, i.e. 3500K
await hass.services.async_call(
DOMAIN,
"set_state",
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_NAME: "deepblack"},
blocking=True,
)
assert bulb.set_color.calls[0][0][0] == [0, 0, 32000, 3500]
bulb.set_color.reset_mock()
# RGB should convert to hue, saturation
await hass.services.async_call(
DOMAIN,
"set_state",
{ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (0, 255, 0)},
blocking=True,
)
assert bulb.set_color.calls[0][0][0] == [21845, 65535, 32000, 3500]
bulb.set_color.reset_mock()
# XY should convert to hue, saturation
await hass.services.async_call(
DOMAIN,
"set_state",
{ATTR_ENTITY_ID: entity_id, ATTR_XY_COLOR: (0.34, 0.339)},
blocking=True,
)
assert bulb.set_color.calls[0][0][0] == [5461, 5139, 32000, 3500]
bulb.set_color.reset_mock()
async def test_lifx_set_state_kelvin(hass: HomeAssistant) -> None:
"""Test set_state works with old and new kelvin parameter names."""
already_migrated_config_entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL
)
already_migrated_config_entry.add_to_hass(hass)
bulb = _mocked_bulb_new_firmware()
bulb.power_level = 65535
bulb.color = [32000, None, 32000, 6000]
with _patch_discovery(device=bulb), _patch_config_flow_try_connect(
device=bulb
), _patch_device(device=bulb):
await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}})
await hass.async_block_till_done()
entity_id = "light.my_bulb"
state = hass.states.get(entity_id)
assert state.state == "on"
attributes = state.attributes
assert attributes[ATTR_BRIGHTNESS] == 125
assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP
await hass.services.async_call(
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
assert bulb.set_power.calls[0][0][0] is False
bulb.set_power.reset_mock()
await hass.services.async_call(
DOMAIN,
"set_state",
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255, ATTR_KELVIN: 3500},
blocking=True,
)
assert bulb.set_color.calls[0][0][0] == [32000, 0, 65535, 3500]
bulb.set_color.reset_mock()
await hass.services.async_call(
DOMAIN,
"set_state",
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100, ATTR_COLOR_TEMP_KELVIN: 2700},
blocking=True,
)
assert bulb.set_color.calls[0][0][0] == [32000, 0, 25700, 2700]
bulb.set_color.reset_mock()
async def test_infrared_color_bulb(hass: HomeAssistant) -> None:
"""Test setting infrared with a color bulb."""
already_migrated_config_entry = MockConfigEntry(

View File

@ -24,6 +24,7 @@ from homeassistant.const import (
CONF_ENTITIES,
CONF_EXCLUDE,
CONF_INCLUDE,
EVENT_HOMEASSISTANT_FINAL_WRITE,
EVENT_HOMEASSISTANT_START,
STATE_OFF,
STATE_ON,
@ -52,6 +53,15 @@ def set_utc(hass):
hass.config.set_time_zone("UTC")
def listeners_without_writes(listeners: dict[str, int]) -> dict[str, int]:
"""Return listeners without final write listeners since we are not testing for these."""
return {
key: value
for key, value in listeners.items()
if key != EVENT_HOMEASSISTANT_FINAL_WRITE
}
async def _async_mock_logbook_platform(hass: HomeAssistant) -> None:
class MockLogbookPlatform:
"""Mock a logbook platform."""
@ -684,7 +694,9 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities(
assert msg["success"]
# Check our listener got unsubscribed
assert hass.bus.async_listeners() == init_listeners
assert listeners_without_writes(
hass.bus.async_listeners()
) == listeners_without_writes(init_listeners)
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
@ -892,7 +904,9 @@ async def test_subscribe_unsubscribe_logbook_stream_included_entities(
assert msg["success"]
# Check our listener got unsubscribed
assert hass.bus.async_listeners() == init_listeners
assert listeners_without_writes(
hass.bus.async_listeners()
) == listeners_without_writes(init_listeners)
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
@ -1083,7 +1097,9 @@ async def test_logbook_stream_excluded_entities_inherits_filters_from_recorder(
assert msg["success"]
# Check our listener got unsubscribed
assert hass.bus.async_listeners() == init_listeners
assert listeners_without_writes(
hass.bus.async_listeners()
) == listeners_without_writes(init_listeners)
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
@ -1386,7 +1402,9 @@ async def test_subscribe_unsubscribe_logbook_stream(
assert msg["success"]
# Check our listener got unsubscribed
assert hass.bus.async_listeners() == init_listeners
assert listeners_without_writes(
hass.bus.async_listeners()
) == listeners_without_writes(init_listeners)
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
@ -1484,7 +1502,9 @@ async def test_subscribe_unsubscribe_logbook_stream_entities(
assert msg["success"]
# Check our listener got unsubscribed
assert hass.bus.async_listeners() == init_listeners
assert listeners_without_writes(
hass.bus.async_listeners()
) == listeners_without_writes(init_listeners)
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
@ -1586,12 +1606,9 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_with_end_time(
assert msg["success"]
# Check our listener got unsubscribed
listeners = hass.bus.async_listeners()
# The async_fire_time_changed above triggers unsubscribe from
# homeassistant_final_write, don't worry about those
init_listeners.pop("homeassistant_final_write")
listeners.pop("homeassistant_final_write")
assert listeners == init_listeners
assert listeners_without_writes(
hass.bus.async_listeners()
) == listeners_without_writes(init_listeners)
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
@ -1659,7 +1676,9 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_past_only(
assert msg["success"]
# Check our listener got unsubscribed
assert hass.bus.async_listeners() == init_listeners
assert listeners_without_writes(
hass.bus.async_listeners()
) == listeners_without_writes(init_listeners)
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
@ -1759,7 +1778,9 @@ async def test_subscribe_unsubscribe_logbook_stream_big_query(
assert msg["success"]
# Check our listener got unsubscribed
assert hass.bus.async_listeners() == init_listeners
assert listeners_without_writes(
hass.bus.async_listeners()
) == listeners_without_writes(init_listeners)
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
@ -1853,7 +1874,9 @@ async def test_subscribe_unsubscribe_logbook_stream_device(
assert msg["success"]
# Check our listener got unsubscribed
assert hass.bus.async_listeners() == init_listeners
assert listeners_without_writes(
hass.bus.async_listeners()
) == listeners_without_writes(init_listeners)
async def test_event_stream_bad_start_time(recorder_mock, hass, hass_ws_client):
@ -1968,7 +1991,9 @@ async def test_logbook_stream_match_multiple_entities(
assert msg["success"]
# Check our listener got unsubscribed
assert hass.bus.async_listeners() == init_listeners
assert listeners_without_writes(
hass.bus.async_listeners()
) == listeners_without_writes(init_listeners)
async def test_event_stream_bad_end_time(recorder_mock, hass, hass_ws_client):
@ -2091,7 +2116,9 @@ async def test_live_stream_with_one_second_commit_interval(
assert msg["success"]
# Check our listener got unsubscribed
assert hass.bus.async_listeners() == init_listeners
assert listeners_without_writes(
hass.bus.async_listeners()
) == listeners_without_writes(init_listeners)
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
@ -2146,7 +2173,9 @@ async def test_subscribe_disconnected(recorder_mock, hass, hass_ws_client):
await hass.async_block_till_done()
# Check our listener got unsubscribed
assert hass.bus.async_listeners() == init_listeners
assert listeners_without_writes(
hass.bus.async_listeners()
) == listeners_without_writes(init_listeners)
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
@ -2189,7 +2218,9 @@ async def test_stream_consumer_stop_processing(recorder_mock, hass, hass_ws_clie
assert msg["type"] == TYPE_RESULT
assert msg["success"]
assert hass.bus.async_listeners() != init_listeners
assert listeners_without_writes(
hass.bus.async_listeners()
) != listeners_without_writes(init_listeners)
for _ in range(5):
hass.states.async_set("binary_sensor.is_light", STATE_ON)
hass.states.async_set("binary_sensor.is_light", STATE_OFF)
@ -2197,9 +2228,13 @@ async def test_stream_consumer_stop_processing(recorder_mock, hass, hass_ws_clie
# Check our listener got unsubscribed because
# the queue got full and the overload safety tripped
assert hass.bus.async_listeners() == after_ws_created_listeners
assert listeners_without_writes(
hass.bus.async_listeners()
) == listeners_without_writes(after_ws_created_listeners)
await websocket_client.close()
assert hass.bus.async_listeners() == init_listeners
assert listeners_without_writes(
hass.bus.async_listeners()
) == listeners_without_writes(init_listeners)
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
@ -2332,7 +2367,9 @@ async def test_subscribe_all_entities_are_continuous(
await hass.async_block_till_done()
# Check our listener got unsubscribed
assert hass.bus.async_listeners() == init_listeners
assert listeners_without_writes(
hass.bus.async_listeners()
) == listeners_without_writes(init_listeners)
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
@ -2494,7 +2531,9 @@ async def test_subscribe_entities_some_have_uom_multiple(
await hass.async_block_till_done()
# Check our listener got unsubscribed
assert hass.bus.async_listeners() == init_listeners
assert listeners_without_writes(
hass.bus.async_listeners()
) == listeners_without_writes(init_listeners)
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
@ -2608,7 +2647,9 @@ async def test_logbook_stream_ignores_forced_updates(
assert msg["success"]
# Check our listener got unsubscribed
assert hass.bus.async_listeners() == init_listeners
assert listeners_without_writes(
hass.bus.async_listeners()
) == listeners_without_writes(init_listeners)
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
@ -2703,4 +2744,6 @@ async def test_subscribe_all_entities_are_continuous_with_device(
await hass.async_block_till_done()
# Check our listener got unsubscribed
assert hass.bus.async_listeners() == init_listeners
assert listeners_without_writes(
hass.bus.async_listeners()
) == listeners_without_writes(init_listeners)

View File

@ -15,10 +15,15 @@ from homeassistant.const import (
SERVICE_RELOAD,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
from tests.common import async_fire_time_changed, get_fixture_path
from tests.common import (
assert_setup_component,
async_fire_time_changed,
get_fixture_path,
)
@respx.mock
@ -399,3 +404,17 @@ async def test_multiple_rest_endpoints(hass):
assert hass.states.get("sensor.json_date_time").state == "07:11:08 PM"
assert hass.states.get("sensor.json_time").state == "07:11:39 PM"
assert hass.states.get("binary_sensor.binary_sensor").state == "on"
async def test_empty_config(hass: HomeAssistant) -> None:
"""Test setup with empty configuration.
For example (with rest.yaml an empty file):
rest: !include rest.yaml
"""
assert await async_setup_component(
hass,
DOMAIN,
{DOMAIN: {}},
)
assert_setup_component(0, DOMAIN)

View File

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

View File

@ -63,6 +63,42 @@ async def test_flow_ssdp(hass: HomeAssistant):
CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN,
CONFIG_ENTRY_LOCATION: TEST_LOCATION,
CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS,
CONFIG_ENTRY_HOST: TEST_HOST,
}
@pytest.mark.usefixtures(
"ssdp_instant_discovery",
"mock_setup_entry",
"mock_get_source_ip",
"mock_mac_address_from_host",
)
async def test_flow_ssdp_ignore(hass: HomeAssistant):
"""Test config flow: discovered + ignore through ssdp."""
# Discovered via step ssdp.
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_SSDP},
data=TEST_DISCOVERY,
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "ssdp_confirm"
# Ignore entry.
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IGNORE},
data={"unique_id": TEST_USN, "title": TEST_FRIENDLY_NAME},
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["title"] == TEST_FRIENDLY_NAME
assert result["data"] == {
CONFIG_ENTRY_ST: TEST_ST,
CONFIG_ENTRY_UDN: TEST_UDN,
CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN,
CONFIG_ENTRY_LOCATION: TEST_LOCATION,
CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS,
CONFIG_ENTRY_HOST: TEST_HOST,
}
@ -138,6 +174,7 @@ async def test_flow_ssdp_no_mac_address(hass: HomeAssistant):
CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN,
CONFIG_ENTRY_LOCATION: TEST_LOCATION,
CONFIG_ENTRY_MAC_ADDRESS: None,
CONFIG_ENTRY_HOST: TEST_HOST,
}
@ -382,6 +419,7 @@ async def test_flow_user(hass: HomeAssistant):
CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN,
CONFIG_ENTRY_LOCATION: TEST_LOCATION,
CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS,
CONFIG_ENTRY_HOST: TEST_HOST,
}

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"
async def test_info_from_service_with_unspecified_address_first(hass):
"""Test that the unspecified address is ignored."""
service_type = "_test._tcp.local."
service_info = get_service_info_mock(service_type, f"test.{service_type}")
service_info.addresses = ["0.0.0.0", "192.168.66.12"]
info = zeroconf.info_from_service(service_info)
assert info.host == "192.168.66.12"
async def test_info_from_service_with_unspecified_address_only(hass):
"""Test that the unspecified address is ignored."""
service_type = "_test._tcp.local."
service_info = get_service_info_mock(service_type, f"test.{service_type}")
service_info.addresses = ["0.0.0.0"]
info = zeroconf.info_from_service(service_info)
assert info is None
async def test_info_from_service_with_link_local_address_second(hass):
"""Test that the link local address is ignored."""
service_type = "_test._tcp.local."

View File

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

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

View File

@ -3287,6 +3287,7 @@ async def test_disallow_entry_reload_with_setup_in_progresss(hass, manager):
async def test_reauth(hass):
"""Test the async_reauth_helper."""
entry = MockConfigEntry(title="test_title", domain="test")
entry2 = MockConfigEntry(title="test_title", domain="test")
mock_setup_entry = AsyncMock(return_value=True)
mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry))
@ -3313,7 +3314,19 @@ async def test_reauth(hass):
assert mock_init.call_args.kwargs["data"]["extra_data"] == 1234
assert entry.entry_id != entry2.entry_id
# Check we can't start duplicate flows
entry.async_start_reauth(hass, {"extra_context": "some_extra_context"})
await hass.async_block_till_done()
assert len(flows) == 1
assert len(hass.config_entries.flow.async_progress()) == 1
# Check we can't start duplicate when the context context is different
entry.async_start_reauth(hass, {"diff": "diff"})
await hass.async_block_till_done()
assert len(hass.config_entries.flow.async_progress()) == 1
# Check we can start a reauth for a different entry
entry2.async_start_reauth(hass, {"extra_context": "some_extra_context"})
await hass.async_block_till_done()
assert len(hass.config_entries.flow.async_progress()) == 2