Compare commits

...

36 Commits

Author SHA1 Message Date
Paulus Schoutsen
72c0526d87 Bumped version to 2023.3.0b5 2023-02-27 20:58:22 -05:00
Matthias Alphart
9ed4e01e94 Update xknx to 2.6.0 (#88864) 2023-02-27 20:58:11 -05:00
Paul Bottein
dcf1ecfeb5 Update frontend to 20230227.0 (#88857) 2023-02-27 20:58:10 -05:00
Klaas Schoute
b72224ceff Bump odp-amsterdam to v5.1.0 (#88847) 2023-02-27 20:58:09 -05:00
Erik Montnemery
96ad5c9666 Add thread user flow (#88842) 2023-02-27 20:58:09 -05:00
Erik Montnemery
00b59c142a Fix sensor unit conversion bug (#88825)
* Fix sensor unit conversion bug

* Ensure the correct unit is stored in the entity registry
2023-02-27 20:58:08 -05:00
Michael Davie
b054c81e13 Bump env_canada to 0.5.29 (#88821) 2023-02-27 20:58:07 -05:00
puddly
b0cbcad440 Bump ZHA dependencies (#88799)
* Bump ZHA dependencies

* Use `importlib.metadata.version` to get package versions
2023-02-27 20:58:06 -05:00
stickpin
bafe552af6 Upgrade caldav to 1.2.0 (#88791) 2023-02-27 20:58:05 -05:00
stickpin
d399855e50 Upgrade caldav to 1.1.3 (#88681)
* Update caldav to 1.1.3

* update caldav to 1.1.3

* update caldav to 1.1.3

---------

Co-authored-by: Allen Porter <allen@thebends.org>
2023-02-27 20:58:03 -05:00
mkmer
d26f430766 Bump aiosomecomfort to 0.0.10 (#88766) 2023-02-27 20:56:46 -05:00
Erik Montnemery
f2e4943a53 Catch CancelledError when setting up components (#88635)
* Catch CancelledError when setting up components

* Catch CancelledError when setting up components

* Also catch SystemExit
2023-02-27 20:56:45 -05:00
Bouwe Westerdijk
6512cd901f Correct Plugwise gas_consumed_interval sensor (#87449)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2023-02-27 20:56:45 -05:00
Paulus Schoutsen
fbe1524f6c Bumped version to 2023.3.0b4 2023-02-26 22:37:34 -05:00
J. Nick Koston
95e337277c Avoid starting a bluetooth poll when Home Assistant is stopping (#88819)
* Avoid starting a bluetooth poll when Home Assistant is stopping

* tests
2023-02-26 22:37:26 -05:00
J. Nick Koston
1503674bd6 Prevent integrations from retrying setup once shutdown has started (#88818)
* Prevent integrations from retrying setup once shutdown has started

* coverage
2023-02-26 22:37:25 -05:00
J. Nick Koston
ab6bd75b70 Fix flux_led discovery running at shutdown (#88817) 2023-02-26 22:37:24 -05:00
J. Nick Koston
2fff836bd4 Fix lock services not removing entity fields (#88805) 2023-02-26 22:37:23 -05:00
J. Nick Koston
d8850758f1 Fix unifiprotect discovery running at shutdown (#88802)
* Fix unifiprotect discovery running at shutdown

Move the discovery start into `async_setup` so we only
start discovery once reguardless of how many config entries
for unifiprotect they have (or how many times they reload).

Always make discovery a background task so it does not get
to block shutdown

* missing decorator
2023-02-26 22:37:22 -05:00
J. Nick Koston
0449856064 Bump yalexs-ble to 2.0.4 (#88798)
changelog: https://github.com/bdraco/yalexs-ble/compare/v2.0.3...v2.0.4
2023-02-26 22:37:21 -05:00
starkillerOG
e48089e0c9 Do not block on reolink firmware check fail (#88797)
Do not block on firmware check fail
2023-02-26 22:37:20 -05:00
starkillerOG
a7e081f70d Simplify reolink update unique_id (#88794)
simplify unique_id
2023-02-26 22:37:19 -05:00
Paulus Schoutsen
fe181425d8 Check circular dependencies (#88778) 2023-02-26 22:37:18 -05:00
Joakim Plate
8c7b29db25 Update nibe library to 2.0.0 (#88769) 2023-02-26 22:37:17 -05:00
J. Nick Koston
aaa5bb9f86 Fix checking if a package is installed on py3.11 (#88768)
pkg_resources is abandoned and we need to move away
from using it https://github.com/pypa/pkg_resources

In the mean time we need to keep it working. This fixes
a new exception in py3.11 when a module is not installed
which allows proper fallback to pkg_resources.Requirement.parse
when needed

```
2023-02-25 15:46:21.101 ERROR (MainThread) [aiohttp.server] Error handling request
Traceback (most recent call last):
  File "/opt/homebrew/lib/python3.11/site-packages/aiohttp/web_protocol.py", line 433, in _handle_request
    resp = await request_handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/aiohttp/web_app.py", line 504, in _handle
    resp = await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/aiohttp/web_middlewares.py", line 117, in impl
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/components/http/security_filter.py", line 60, in security_filter_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/components/http/forwarded.py", line 100, in forwarded_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/components/http/request_context.py", line 28, in request_context_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/components/http/ban.py", line 80, in ban_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/components/http/auth.py", line 235, in auth_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/components/http/view.py", line 146, in handle
    result = await result
             ^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/components/config/config_entries.py", line 148, in post
    return await super().post(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/components/http/data_validator.py", line 72, in wrapper
    result = await method(view, request, data, *args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/helpers/data_entry_flow.py", line 71, in post
    result = await self._flow_mgr.async_init(
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/config_entries.py", line 826, in async_init
    flow, result = await task
                   ^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/config_entries.py", line 844, in _async_init
    flow = await self.async_create_flow(handler, context=context, data=data)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/config_entries.py", line 950, in async_create_flow
    await async_process_deps_reqs(self.hass, self._hass_config, integration)
  File "/Users/bdraco/home-assistant/homeassistant/setup.py", line 384, in async_process_deps_reqs
    await requirements.async_get_integration_with_requirements(
  File "/Users/bdraco/home-assistant/homeassistant/requirements.py", line 52, in async_get_integration_with_requirements
    return await manager.async_get_integration_with_requirements(domain)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/requirements.py", line 171, in async_get_integration_with_requirements
    await self._async_process_integration(integration, done)
  File "/Users/bdraco/home-assistant/homeassistant/requirements.py", line 186, in _async_process_integration
    await self.async_process_requirements(
  File "/Users/bdraco/home-assistant/homeassistant/requirements.py", line 252, in async_process_requirements
    await self._async_process_requirements(name, missing)
  File "/Users/bdraco/home-assistant/homeassistant/requirements.py", line 284, in _async_process_requirements
    installed, failures = await self.hass.async_add_executor_job(
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.11/3.11.1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/concurrent/futures/thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/requirements.py", line 113, in _install_requirements_if_missing
    if pkg_util.is_installed(req) or _install_with_retry(req, kwargs):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/util/package.py", line 40, in is_installed
    pkg_resources.get_distribution(package)
  File "/opt/homebrew/lib/python3.11/site-packages/pkg_resources/__init__.py", line 478, in get_distribution
    dist = get_provider(dist)
           ^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/pkg_resources/__init__.py", line 354, in get_provider
    return working_set.find(moduleOrReq) or require(str(moduleOrReq))[0]
                                            ~~~~~~~~~~~~~~~~~~~~~~~~~^^^
IndexError: list index out of range
``
2023-02-26 22:37:17 -05:00
J. Nick Koston
5b78e0c4ff Restore previous behavior of only waiting for new tasks at shutdown (#88740)
* Restore previous behavior of only waiting for new tasks at shutdown

* cleanup

* do a swap instead

* await canceled tasks

* await canceled tasks

* fix

* not needed since we no longer clear

* log it

* reword

* wait for airvisual

* tests
2023-02-26 22:37:16 -05:00
Franck Nijhof
2063dbf00d Bumped version to 2023.3.0b3 2023-02-25 12:07:47 +01:00
Joakim Sørensen
91a03ab83d Remove homeassistant_hardware after dependency from zha (#88751) 2023-02-25 12:07:25 +01:00
J. Nick Koston
ed8f538890 Prevent new discovery flows from being created when stopping (#88743) 2023-02-25 12:07:22 +01:00
J. Nick Koston
6196607c5d Make hass.async_stop an untracked task (#88738) 2023-02-25 12:07:19 +01:00
J. Nick Koston
833ccafb76 Log futures that are blocking shutdown stages (#88736) 2023-02-25 12:07:15 +01:00
mkmer
ca539d0a09 Add missing reauth strings to Honeywell (#88733)
Add missing reauth strings
2023-02-25 12:07:12 +01:00
Austin Mroczek
0e3e954000 Bump total_connect_client to v2023.2 (#88729)
* bump total_connect_client to v2023.2

* Trigger Build
2023-02-25 12:07:09 +01:00
avee87
4ef96c76e4 Fix log message in recorder on total_increasing reset (#88710) 2023-02-25 12:07:05 +01:00
Álvaro Fernández Rojas
d5b0c1faa0 Update aioqsw v0.3.2 (#88695)
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2023-02-25 12:07:02 +01:00
Arturo
2405908cdd Fix matter light color capabilities bit map (#88693)
* Adds matter light color capabilities bit map

* Fixed matter light hue and saturation test
2023-02-25 12:06:58 +01:00
61 changed files with 764 additions and 222 deletions

View File

@@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==1.2.7", "yalexs_ble==2.0.3"]
"requirements": ["yalexs==1.2.7", "yalexs_ble==2.0.4"]
}

View File

@@ -106,6 +106,8 @@ class ActiveBluetoothDataUpdateCoordinator(
def needs_poll(self, service_info: BluetoothServiceInfoBleak) -> bool:
"""Return true if time to try and poll."""
if self.hass.is_stopping:
return False
poll_age: float | None = None
if self._last_poll:
poll_age = monotonic_time_coarse() - self._last_poll

View File

@@ -99,6 +99,8 @@ class ActiveBluetoothProcessorCoordinator(
def needs_poll(self, service_info: BluetoothServiceInfoBleak) -> bool:
"""Return true if time to try and poll."""
if self.hass.is_stopping:
return False
poll_age: float | None = None
if self._last_poll:
poll_age = monotonic_time_coarse() - self._last_poll

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/caldav",
"iot_class": "cloud_polling",
"loggers": ["caldav", "vobject"],
"requirements": ["caldav==1.1.1"]
"requirements": ["caldav==1.2.0"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env_canada==0.5.28"]
"requirements": ["env_canada==0.5.29"]
}

View File

@@ -87,14 +87,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass, STARTUP_SCAN_TIMEOUT
)
@callback
def _async_start_background_discovery(*_: Any) -> None:
"""Run discovery in the background."""
hass.async_create_background_task(_async_discovery(), "flux_led-discovery")
async def _async_discovery(*_: Any) -> None:
async_trigger_discovery(
hass, await async_discover_devices(hass, DISCOVER_SCAN_TIMEOUT)
)
async_trigger_discovery(hass, domain_data[FLUX_LED_DISCOVERY])
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_discovery)
async_track_time_interval(hass, _async_discovery, DISCOVERY_INTERVAL)
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STARTED, _async_start_background_discovery
)
async_track_time_interval(
hass, _async_start_background_discovery, DISCOVERY_INTERVAL
)
return True

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20230224.0"]
"requirements": ["home-assistant-frontend==20230227.0"]
}

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/garages_amsterdam",
"iot_class": "cloud_polling",
"requirements": ["odp-amsterdam==5.0.1"]
"requirements": ["odp-amsterdam==5.1.0"]
}

View File

@@ -1,7 +1,6 @@
{
"domain": "hassio",
"name": "Home Assistant Supervisor",
"after_dependencies": ["panel_custom"],
"codeowners": ["@home-assistant/supervisor"],
"dependencies": ["http"],
"documentation": "https://www.home-assistant.io/integrations/hassio",

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/honeywell",
"iot_class": "cloud_polling",
"loggers": ["somecomfort"],
"requirements": ["aiosomecomfort==0.0.8"]
"requirements": ["aiosomecomfort==0.0.10"]
}

View File

@@ -7,6 +7,13 @@
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "The Honeywell integration needs to re-authenticate your account",
"data": {
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {

View File

@@ -9,5 +9,5 @@
"iot_class": "local_push",
"loggers": ["xknx"],
"quality_scale": "platinum",
"requirements": ["xknx==2.5.0"]
"requirements": ["xknx==2.6.0"]
}

View File

@@ -33,6 +33,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401
)
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.service import remove_entity_service_fields
from homeassistant.helpers.typing import ConfigType, StateType
_LOGGER = logging.getLogger(__name__)
@@ -92,7 +93,7 @@ async def _async_lock(entity: LockEntity, service_call: ServiceCall) -> None:
raise ValueError(
f"Code '{code}' for locking {entity.entity_id} doesn't match pattern {entity.code_format}"
)
await entity.async_lock(**service_call.data)
await entity.async_lock(**remove_entity_service_fields(service_call))
async def _async_unlock(entity: LockEntity, service_call: ServiceCall) -> None:
@@ -102,7 +103,7 @@ async def _async_unlock(entity: LockEntity, service_call: ServiceCall) -> None:
raise ValueError(
f"Code '{code}' for unlocking {entity.entity_id} doesn't match pattern {entity.code_format}"
)
await entity.async_unlock(**service_call.data)
await entity.async_unlock(**remove_entity_service_fields(service_call))
async def _async_open(entity: LockEntity, service_call: ServiceCall) -> None:
@@ -112,7 +113,7 @@ async def _async_open(entity: LockEntity, service_call: ServiceCall) -> None:
raise ValueError(
f"Code '{code}' for opening {entity.entity_id} doesn't match pattern {entity.code_format}"
)
await entity.async_open(**service_call.data)
await entity.async_open(**remove_entity_service_fields(service_call))
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View File

@@ -1,6 +1,7 @@
"""Matter light."""
from __future__ import annotations
from enum import IntFlag
from typing import Any
from chip.clusters import Objects as clusters
@@ -260,12 +261,16 @@ class MatterLight(MatterEntity, LightEntity):
color_temp = kwargs.get(ATTR_COLOR_TEMP)
brightness = kwargs.get(ATTR_BRIGHTNESS)
if hs_color is not None and self.supports_color:
await self._set_hs_color(hs_color)
elif xy_color is not None:
await self._set_xy_color(xy_color)
elif color_temp is not None and self.supports_color_temperature:
await self._set_color_temp(color_temp)
if self.supported_color_modes is not None:
if hs_color is not None and ColorMode.HS in self.supported_color_modes:
await self._set_hs_color(hs_color)
elif xy_color is not None and ColorMode.XY in self.supported_color_modes:
await self._set_xy_color(xy_color)
elif (
color_temp is not None
and ColorMode.COLOR_TEMP in self.supported_color_modes
):
await self._set_color_temp(color_temp)
if brightness is not None and self.supports_brightness:
await self._set_brightness(brightness)
@@ -284,7 +289,6 @@ class MatterLight(MatterEntity, LightEntity):
@callback
def _update_from_device(self) -> None:
"""Update from device."""
if self._attr_supported_color_modes is None:
# work out what (color)features are supported
supported_color_modes: set[ColorMode] = set()
@@ -297,30 +301,19 @@ class MatterLight(MatterEntity, LightEntity):
if self._entity_info.endpoint.has_attribute(
None, clusters.ColorControl.Attributes.ColorMode
):
# device has some color support, check which color modes
# are supported with the featuremap on the ColorControl cluster
color_feature_map = self.get_matter_attribute_value(
clusters.ColorControl.Attributes.FeatureMap,
capabilities = self.get_matter_attribute_value(
clusters.ColorControl.Attributes.ColorCapabilities
)
if (
color_feature_map
& clusters.ColorControl.Attributes.CurrentHue.attribute_id
):
assert capabilities is not None
if capabilities & ColorCapabilities.kHueSaturationSupported:
supported_color_modes.add(ColorMode.HS)
if (
color_feature_map
& clusters.ColorControl.Attributes.CurrentX.attribute_id
):
if capabilities & ColorCapabilities.kXYAttributesSupported:
supported_color_modes.add(ColorMode.XY)
# color temperature support detection using the featuremap is not reliable
# (temporary?) fallback to checking the value
if (
self.get_matter_attribute_value(
clusters.ColorControl.Attributes.ColorTemperatureMireds
)
is not None
):
if capabilities & ColorCapabilities.kColorTemperatureSupported:
supported_color_modes.add(ColorMode.COLOR_TEMP)
self._attr_supported_color_modes = supported_color_modes
@@ -351,11 +344,23 @@ class MatterLight(MatterEntity, LightEntity):
self._attr_brightness = self._get_brightness()
# This enum should be removed once the ColorControlCapabilities enum is added to the CHIP (Matter) library
# clusters.ColorControl.Bitmap.ColorCapabilities
class ColorCapabilities(IntFlag):
"""Color control capabilities bitmap."""
kHueSaturationSupported = 0x1
kEnhancedHueSupported = 0x2
kColorLoopSupported = 0x4
kXYAttributesSupported = 0x8
kColorTemperatureSupported = 0x10
# Discovery schema(s) to map Matter Attributes to HA entities
DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
platform=Platform.LIGHT,
entity_description=LightEntityDescription(key="ExtendedMatterLight"),
entity_description=LightEntityDescription(key="MatterLight"),
entity_class=MatterLight,
required_attributes=(clusters.OnOff.Attributes.OnOff,),
optional_attributes=(

View File

@@ -8,11 +8,11 @@ from datetime import timedelta
from functools import cached_property
from typing import Any, Generic, TypeVar
from nibe.coil import Coil
from nibe.coil import Coil, CoilData
from nibe.connection import Connection
from nibe.connection.modbus import Modbus
from nibe.connection.nibegw import NibeGW, ProductInfo
from nibe.exceptions import CoilNotFoundException, CoilReadException
from nibe.exceptions import CoilNotFoundException, ReadException
from nibe.heatpump import HeatPump, Model, Series
from homeassistant.config_entries import ConfigEntry
@@ -182,7 +182,7 @@ class ContextCoordinator(
return release_update
class Coordinator(ContextCoordinator[dict[int, Coil], int]):
class Coordinator(ContextCoordinator[dict[int, CoilData], int]):
"""Update coordinator for nibe heat pumps."""
config_entry: ConfigEntry
@@ -199,17 +199,18 @@ class Coordinator(ContextCoordinator[dict[int, Coil], int]):
)
self.data = {}
self.seed: dict[int, Coil] = {}
self.seed: dict[int, CoilData] = {}
self.connection = connection
self.heatpump = heatpump
self.task: asyncio.Task | None = None
heatpump.subscribe(heatpump.COIL_UPDATE_EVENT, self._on_coil_update)
def _on_coil_update(self, coil: Coil):
def _on_coil_update(self, data: CoilData):
"""Handle callback on coil updates."""
self.data[coil.address] = coil
self.seed[coil.address] = coil
coil = data.coil
self.data[coil.address] = data
self.seed[coil.address] = data
self.async_update_context_listeners([coil.address])
@property
@@ -246,26 +247,26 @@ class Coordinator(ContextCoordinator[dict[int, Coil], int]):
async def async_write_coil(self, coil: Coil, value: int | float | str) -> None:
"""Write coil and update state."""
coil.value = value
coil = await self.connection.write_coil(coil)
data = CoilData(coil, value)
await self.connection.write_coil(data)
self.data[coil.address] = coil
self.data[coil.address] = data
self.async_update_context_listeners([coil.address])
async def async_read_coil(self, coil: Coil) -> Coil:
async def async_read_coil(self, coil: Coil) -> CoilData:
"""Read coil and update state using callbacks."""
return await self.connection.read_coil(coil)
async def _async_update_data(self) -> dict[int, Coil]:
async def _async_update_data(self) -> dict[int, CoilData]:
self.task = asyncio.current_task()
try:
return await self._async_update_data_internal()
finally:
self.task = None
async def _async_update_data_internal(self) -> dict[int, Coil]:
result: dict[int, Coil] = {}
async def _async_update_data_internal(self) -> dict[int, CoilData]:
result: dict[int, CoilData] = {}
def _get_coils() -> Iterable[Coil]:
for address in sorted(self.context_callbacks.keys()):
@@ -282,10 +283,10 @@ class Coordinator(ContextCoordinator[dict[int, Coil], int]):
yield coil
try:
async for coil in self.connection.read_coils(_get_coils()):
result[coil.address] = coil
self.seed.pop(coil.address, None)
except CoilReadException as exception:
async for data in self.connection.read_coils(_get_coils()):
result[data.coil.address] = data
self.seed.pop(data.coil.address, None)
except ReadException as exception:
if not result:
raise UpdateFailed(f"Failed to update: {exception}") from exception
self.logger.debug(
@@ -329,7 +330,7 @@ class CoilEntity(CoordinatorEntity[Coordinator]):
self.coordinator.data or {}
)
def _async_read_coil(self, coil: Coil):
def _async_read_coil(self, data: CoilData):
"""Update state of entity based on coil data."""
async def _async_write_coil(self, value: int | float | str):
@@ -337,10 +338,9 @@ class CoilEntity(CoordinatorEntity[Coordinator]):
await self.coordinator.async_write_coil(self._coil, value)
def _handle_coordinator_update(self) -> None:
coil = self.coordinator.data.get(self._coil.address)
if coil is None:
data = self.coordinator.data.get(self._coil.address)
if data is None:
return
self._coil = coil
self._async_read_coil(coil)
self._async_read_coil(data)
self.async_write_ha_state()

View File

@@ -1,7 +1,7 @@
"""The Nibe Heat Pump binary sensors."""
from __future__ import annotations
from nibe.coil import Coil
from nibe.coil import Coil, CoilData
from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
@@ -37,5 +37,5 @@ class BinarySensor(CoilEntity, BinarySensorEntity):
"""Initialize entity."""
super().__init__(coordinator, coil, ENTITY_ID_FORMAT)
def _async_read_coil(self, coil: Coil) -> None:
self._attr_is_on = coil.value == "ON"
def _async_read_coil(self, data: CoilData) -> None:
self._attr_is_on = data.value == "ON"

View File

@@ -8,10 +8,10 @@ from nibe.connection.nibegw import NibeGW
from nibe.exceptions import (
AddressInUseException,
CoilNotFoundException,
CoilReadException,
CoilReadSendException,
CoilWriteException,
CoilWriteSendException,
ReadException,
ReadSendException,
WriteException,
)
from nibe.heatpump import HeatPump, Model
import voluptuous as vol
@@ -108,13 +108,13 @@ async def validate_nibegw_input(
try:
await connection.verify_connectivity()
except (CoilReadSendException, CoilWriteSendException) as exception:
except (ReadSendException, CoilWriteSendException) as exception:
raise FieldError(str(exception), CONF_IP_ADDRESS, "address") from exception
except CoilNotFoundException as exception:
raise FieldError("Coils not found", "base", "model") from exception
except CoilReadException as exception:
except ReadException as exception:
raise FieldError("Timeout on read from pump", "base", "read") from exception
except CoilWriteException as exception:
except WriteException as exception:
raise FieldError("Timeout on writing to pump", "base", "write") from exception
finally:
await connection.stop()
@@ -147,13 +147,13 @@ async def validate_modbus_input(
try:
await connection.verify_connectivity()
except (CoilReadSendException, CoilWriteSendException) as exception:
except (ReadSendException, CoilWriteSendException) as exception:
raise FieldError(str(exception), CONF_MODBUS_URL, "address") from exception
except CoilNotFoundException as exception:
raise FieldError("Coils not found", "base", "model") from exception
except CoilReadException as exception:
except ReadException as exception:
raise FieldError("Timeout on read from pump", "base", "read") from exception
except CoilWriteException as exception:
except WriteException as exception:
raise FieldError("Timeout on writing to pump", "base", "write") from exception
finally:
await connection.stop()

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/nibe_heatpump",
"iot_class": "local_polling",
"requirements": ["nibe==1.6.0"]
"requirements": ["nibe==2.0.0"]
}

View File

@@ -1,7 +1,7 @@
"""The Nibe Heat Pump numbers."""
from __future__ import annotations
from nibe.coil import Coil
from nibe.coil import Coil, CoilData
from homeassistant.components.number import ENTITY_ID_FORMAT, NumberEntity
from homeassistant.config_entries import ConfigEntry
@@ -58,13 +58,13 @@ class Number(CoilEntity, NumberEntity):
self._attr_native_unit_of_measurement = coil.unit
self._attr_native_value = None
def _async_read_coil(self, coil: Coil) -> None:
if coil.value is None:
def _async_read_coil(self, data: CoilData) -> None:
if data.value is None:
self._attr_native_value = None
return
try:
self._attr_native_value = float(coil.value)
self._attr_native_value = float(data.value)
except ValueError:
self._attr_native_value = None

View File

@@ -1,7 +1,7 @@
"""The Nibe Heat Pump select."""
from __future__ import annotations
from nibe.coil import Coil
from nibe.coil import Coil, CoilData
from homeassistant.components.select import ENTITY_ID_FORMAT, SelectEntity
from homeassistant.config_entries import ConfigEntry
@@ -40,12 +40,12 @@ class Select(CoilEntity, SelectEntity):
self._attr_options = list(coil.mappings.values())
self._attr_current_option = None
def _async_read_coil(self, coil: Coil) -> None:
if not isinstance(coil.value, str):
def _async_read_coil(self, data: CoilData) -> None:
if not isinstance(data.value, str):
self._attr_current_option = None
return
self._attr_current_option = coil.value
self._attr_current_option = data.value
async def async_select_option(self, option: str) -> None:
"""Support writing value."""

View File

@@ -1,7 +1,7 @@
"""The Nibe Heat Pump sensors."""
from __future__ import annotations
from nibe.coil import Coil
from nibe.coil import Coil, CoilData
from homeassistant.components.sensor import (
ENTITY_ID_FORMAT,
@@ -146,5 +146,5 @@ class Sensor(CoilEntity, SensorEntity):
self._attr_native_unit_of_measurement = coil.unit
self._attr_entity_category = EntityCategory.DIAGNOSTIC
def _async_read_coil(self, coil: Coil):
self._attr_native_value = coil.value
def _async_read_coil(self, data: CoilData):
self._attr_native_value = data.value

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from nibe.coil import Coil
from nibe.coil import Coil, CoilData
from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity
from homeassistant.config_entries import ConfigEntry
@@ -40,8 +40,8 @@ class Switch(CoilEntity, SwitchEntity):
super().__init__(coordinator, coil, ENTITY_ID_FORMAT)
self._attr_is_on = None
def _async_read_coil(self, coil: Coil) -> None:
self._attr_is_on = coil.value == "ON"
def _async_read_coil(self, data: CoilData) -> None:
self._attr_is_on = data.value == "ON"
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""

View File

@@ -17,6 +17,7 @@ from homeassistant.const import (
UnitOfPower,
UnitOfPressure,
UnitOfTemperature,
UnitOfTime,
UnitOfVolume,
)
from homeassistant.core import HomeAssistant
@@ -303,9 +304,9 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="gas_consumed_interval",
name="Gas consumed interval",
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
device_class=SensorDeviceClass.GAS,
state_class=SensorStateClass.TOTAL,
icon="mdi:meter-gas",
native_unit_of_measurement=f"{UnitOfVolume.CUBIC_METERS}/{UnitOfTime.HOURS}",
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="gas_consumed_cumulative",

View File

@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/qnap_qsw",
"iot_class": "local_polling",
"loggers": ["aioqsw"],
"requirements": ["aioqsw==0.3.1"]
"requirements": ["aioqsw==0.3.2"]
}

View File

@@ -106,9 +106,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
)
# Fetch initial data so we have data when entities subscribe
try:
# If camera WAN blocked, firmware check fails, do not prevent setup
await asyncio.gather(
device_coordinator.async_config_entry_first_refresh(),
firmware_coordinator.async_config_entry_first_refresh(),
firmware_coordinator.async_refresh(),
)
except ConfigEntryNotReady:
await host.stop()

View File

@@ -49,7 +49,7 @@ class ReolinkUpdateEntity(ReolinkBaseCoordinatorEntity, UpdateEntity):
"""Initialize a Netgear device."""
super().__init__(reolink_data, reolink_data.firmware_coordinator)
self._attr_unique_id = f"{self._host.unique_id}_update"
self._attr_unique_id = f"{self._host.unique_id}"
@property
def installed_version(self) -> str | None:

View File

@@ -196,19 +196,30 @@ class SensorEntity(Entity):
if self.unique_id is None or self.device_class is None:
return
registry = er.async_get(self.hass)
# Bail out if the entity is not yet registered
if not (
entity_id := registry.async_get_entity_id(
platform.domain, platform.platform_name, self.unique_id
)
):
# Prime _sensor_option_unit_of_measurement to ensure the correct unit
# is stored in the entity registry.
self._sensor_option_unit_of_measurement = self._get_initial_suggested_unit()
return
registry_entry = registry.async_get(entity_id)
assert registry_entry
# Prime _sensor_option_unit_of_measurement to ensure the correct unit
# is stored in the entity registry.
self.registry_entry = registry_entry
self._async_read_entity_options()
# If the sensor has 'unit_of_measurement' in its sensor options, the user has
# overridden the unit.
# If the sensor has 'sensor.private' in its entity options, it was added after
# automatic unit conversion was implemented.
# If the sensor has 'sensor.private' in its entity options, it already has a
# suggested_unit.
registry_unit = registry_entry.unit_of_measurement
if (
(
@@ -230,11 +241,14 @@ class SensorEntity(Entity):
# Set suggested_unit_of_measurement to the old unit to enable automatic
# conversion
registry.async_update_entity_options(
self.registry_entry = registry.async_update_entity_options(
entity_id,
f"{DOMAIN}.private",
{"suggested_unit_of_measurement": registry_unit},
)
# Update _sensor_option_unit_of_measurement to ensure the correct unit
# is stored in the entity registry.
self._async_read_entity_options()
async def async_internal_added_to_hass(self) -> None:
"""Call when the sensor entity is added to hass."""
@@ -305,12 +319,8 @@ class SensorEntity(Entity):
return None
def get_initial_entity_options(self) -> er.EntityOptionsType | None:
"""Return initial entity options.
These will be stored in the entity registry the first time the entity is seen,
and then never updated.
"""
def _get_initial_suggested_unit(self) -> str | UndefinedType:
"""Return the initial unit."""
# Unit suggested by the integration
suggested_unit_of_measurement = self.suggested_unit_of_measurement
@@ -321,6 +331,19 @@ class SensorEntity(Entity):
)
if suggested_unit_of_measurement is None:
return UNDEFINED
return suggested_unit_of_measurement
def get_initial_entity_options(self) -> er.EntityOptionsType | None:
"""Return initial entity options.
These will be stored in the entity registry the first time the entity is seen,
and then never updated.
"""
suggested_unit_of_measurement = self._get_initial_suggested_unit()
if suggested_unit_of_measurement is UNDEFINED:
return None
return {
@@ -416,7 +439,7 @@ class SensorEntity(Entity):
return self._sensor_option_unit_of_measurement
# Second priority, for non registered entities: unit suggested by integration
if not self.registry_entry and self.suggested_unit_of_measurement:
if not self.unique_id and self.suggested_unit_of_measurement:
return self.suggested_unit_of_measurement
# Third priority: Legacy temperature conversion, which applies

View File

@@ -588,8 +588,8 @@ def _compile_statistics( # noqa: C901
),
entity_id,
new_state,
state.last_updated.isoformat(),
fstate,
state.last_updated.isoformat(),
)
except HomeAssistantError:
continue

View File

@@ -13,16 +13,23 @@ class ThreadConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> FlowResult:
"""Set up because the user has border routers."""
await self._async_handle_discovery_without_unique_id()
return self.async_create_entry(title="Thread", data={})
async def async_step_import(
self, import_data: dict[str, str] | None = None
) -> FlowResult:
"""Set up by import from async_setup."""
await self._async_handle_discovery_without_unique_id()
return self.async_create_entry(title="Thread", data={})
async def async_step_user(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Set up by import from async_setup."""
await self._async_handle_discovery_without_unique_id()
return self.async_create_entry(title="Thread", data={})
async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> FlowResult:
"""Set up because the user has border routers."""
await self._async_handle_discovery_without_unique_id()
return self.async_create_entry(title="Thread", data={})

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/totalconnect",
"iot_class": "cloud_polling",
"loggers": ["total_connect_client"],
"requirements": ["total_connect_client==2023.1"]
"requirements": ["total_connect_client==2023.2"]
}

View File

@@ -14,6 +14,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from homeassistant.helpers.issue_registry import IssueSeverity
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_ALLOW_EA,
@@ -40,10 +41,15 @@ _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=DEFAULT_SCAN_INTERVAL)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the UniFi Protect."""
# Only start discovery once regardless of how many entries they have
async_start_discovery(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the UniFi Protect config entries."""
async_start_discovery(hass)
protect = async_create_api_client(hass, entry)
_LOGGER.debug("Connect to UniFi Protect")
data_service = ProtectData(hass, protect, SCAN_INTERVAL, entry)

View File

@@ -29,13 +29,19 @@ def async_start_discovery(hass: HomeAssistant) -> None:
return
domain_data[DISCOVERY] = True
async def _async_discovery(*_: Any) -> None:
async def _async_discovery() -> None:
async_trigger_discovery(hass, await async_discover_devices())
# Do not block startup since discovery takes 31s or more
hass.async_create_background_task(_async_discovery(), "unifiprotect-discovery")
@callback
def _async_start_background_discovery(*_: Any) -> None:
"""Run discovery in the background."""
hass.async_create_background_task(_async_discovery(), "unifiprotect-discovery")
async_track_time_interval(hass, _async_discovery, DISCOVERY_INTERVAL)
# Do not block startup since discovery takes 31s or more
_async_start_background_discovery()
async_track_time_interval(
hass, _async_start_background_discovery, DISCOVERY_INTERVAL
)
async def async_discover_devices() -> list[UnifiDevice]:

View File

@@ -12,5 +12,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
"iot_class": "local_push",
"requirements": ["yalexs-ble==2.0.3"]
"requirements": ["yalexs-ble==2.0.4"]
}

View File

@@ -2,18 +2,12 @@
from __future__ import annotations
import dataclasses
from importlib.metadata import version
from typing import Any
import bellows
import pkg_resources
import zigpy
from zigpy.config import CONF_NWK_EXTENDED_PAN_ID
from zigpy.profiles import PROFILES
from zigpy.zcl import Cluster
import zigpy_deconz
import zigpy_xbee
import zigpy_zigate
import zigpy_znp
from homeassistant.components.diagnostics.util import async_redact_data
from homeassistant.config_entries import ConfigEntry
@@ -79,13 +73,13 @@ async def async_get_config_entry_diagnostics(
"config_entry": config_entry.as_dict(),
"application_state": shallow_asdict(gateway.application_controller.state),
"versions": {
"bellows": bellows.__version__,
"zigpy": zigpy.__version__,
"zigpy_deconz": zigpy_deconz.__version__,
"zigpy_xbee": zigpy_xbee.__version__,
"zigpy_znp": zigpy_znp.__version__,
"zigpy_zigate": zigpy_zigate.__version__,
"zhaquirks": pkg_resources.get_distribution("zha-quirks").version,
"bellows": version("bellows"),
"zigpy": version("zigpy"),
"zigpy_deconz": version("zigpy-deconz"),
"zigpy_xbee": version("zigpy-xbee"),
"zigpy_znp": version("zigpy_znp"),
"zigpy_zigate": version("zigpy-zigate"),
"zhaquirks": version("zha-quirks"),
},
},
KEYS_TO_REDACT,

View File

@@ -1,13 +1,7 @@
{
"domain": "zha",
"name": "Zigbee Home Automation",
"after_dependencies": [
"onboarding",
"usb",
"zeroconf",
"homeassistant_hardware",
"homeassistant_yellow"
],
"after_dependencies": ["onboarding", "usb"],
"codeowners": ["@dmulcahey", "@adminiuga", "@puddly"],
"config_flow": true,
"dependencies": ["file_upload"],
@@ -26,15 +20,15 @@
"zigpy_znp"
],
"requirements": [
"bellows==0.34.7",
"bellows==0.34.9",
"pyserial==3.5",
"pyserial-asyncio==0.6",
"zha-quirks==0.0.93",
"zigpy-deconz==0.19.2",
"zigpy==0.53.0",
"zigpy==0.53.2",
"zigpy-xbee==0.16.2",
"zigpy-zigate==0.10.3",
"zigpy-znp==0.9.2"
"zigpy-znp==0.9.3"
],
"usb": [
{

View File

@@ -445,6 +445,10 @@ class ConfigEntry:
async def setup_again(*_: Any) -> None:
"""Run setup again."""
# Check again when we fire in case shutdown
# has started so we do not block shutdown
if hass.is_stopping:
return
self._async_cancel_retry_setup = None
await self.async_setup(hass, integration=integration, tries=tries)
@@ -459,7 +463,8 @@ class ConfigEntry:
await self._async_process_on_unload()
return
except Exception: # pylint: disable=broad-except
# pylint: disable-next=broad-except
except (asyncio.CancelledError, SystemExit, Exception):
_LOGGER.exception(
"Error setting up entry %s for %s", self.title, integration.domain
)

View File

@@ -8,7 +8,7 @@ from .backports.enum import StrEnum
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 3
PATCH_VERSION: Final = "0b2"
PATCH_VERSION: Final = "0b5"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0)

View File

@@ -38,6 +38,7 @@ from typing import (
)
from urllib.parse import urlparse
import async_timeout
from typing_extensions import Self
import voluptuous as vol
import yarl
@@ -711,6 +712,14 @@ class HomeAssistant:
"Stopping Home Assistant before startup has completed may fail"
)
# Keep holding the reference to the tasks but do not allow them
# to block shutdown. Only tasks created after this point will
# be waited for.
running_tasks = self._tasks
# Avoid clearing here since we want the remove callbacks to fire
# and remove the tasks from the original set which is now running_tasks
self._tasks = set()
# Cancel all background tasks
for task in self._background_tasks:
self._tasks.add(task)
@@ -730,6 +739,7 @@ class HomeAssistant:
"Timed out waiting for shutdown stage 1 to complete, the shutdown will"
" continue"
)
self._async_log_running_tasks(1)
# stage 2
self.state = CoreState.final_write
@@ -742,11 +752,41 @@ class HomeAssistant:
"Timed out waiting for shutdown stage 2 to complete, the shutdown will"
" continue"
)
self._async_log_running_tasks(2)
# stage 3
self.state = CoreState.not_running
self.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE)
# Make a copy of running_tasks since a task can finish
# while we are awaiting canceled tasks to get their result
# which will result in the set size changing during iteration
for task in list(running_tasks):
if task.done():
# Since we made a copy we need to check
# to see if the task finished while we
# were awaiting another task
continue
_LOGGER.warning(
"Task %s was still running after stage 2 shutdown; "
"Integrations should cancel non-critical tasks when receiving "
"the stop event to prevent delaying shutdown",
task,
)
task.cancel()
try:
async with async_timeout.timeout(0.1):
await task
except asyncio.CancelledError:
pass
except asyncio.TimeoutError:
# Task may be shielded from cancellation.
_LOGGER.exception(
"Task %s could not be canceled during stage 3 shutdown", task
)
except Exception as ex: # pylint: disable=broad-except
_LOGGER.exception("Task %s error during stage 3 shutdown: %s", task, ex)
# Prevent run_callback_threadsafe from scheduling any additional
# callbacks in the event loop as callbacks created on the futures
# it returns will never run after the final `self.async_block_till_done`
@@ -762,11 +802,18 @@ class HomeAssistant:
"Timed out waiting for shutdown stage 3 to complete, the shutdown will"
" continue"
)
self._async_log_running_tasks(3)
self.state = CoreState.stopped
if self._stopped is not None:
self._stopped.set()
def _async_log_running_tasks(self, stage: int) -> None:
"""Log all running tasks."""
for task in self._tasks:
_LOGGER.warning("Shutdown stage %s: still running: %s", stage, task)
class Context:
"""The context that triggered something."""

View File

@@ -44,7 +44,9 @@ def _async_init_flow(
# as ones in progress as it may cause additional device probing
# which can overload devices since zeroconf/ssdp updates can happen
# multiple times in the same minute
if hass.config_entries.flow.async_has_matching_flow(domain, context, data):
if hass.is_stopping or hass.config_entries.flow.async_has_matching_flow(
domain, context, data
):
return None
return hass.config_entries.flow.async_init(domain, context=context, data=data)

View File

@@ -513,6 +513,16 @@ async def async_get_all_descriptions(
return descriptions
@callback
def remove_entity_service_fields(call: ServiceCall) -> dict[Any, Any]:
"""Remove entity service fields."""
return {
key: val
for key, val in call.data.items()
if key not in cv.ENTITY_SERVICE_FIELDS
}
@callback
@bind_hass
def async_set_service_schema(
@@ -567,11 +577,7 @@ async def entity_service_call( # noqa: C901
# If the service function is a string, we'll pass it the service call data
if isinstance(func, str):
data: dict | ServiceCall = {
key: val
for key, val in call.data.items()
if key not in cv.ENTITY_SERVICE_FIELDS
}
data: dict | ServiceCall = remove_entity_service_fields(call)
# If the service function is not a string, we pass the service call
else:
data = call

View File

@@ -1,4 +1,5 @@
"""Signal handling related helpers."""
import asyncio
import logging
import signal
@@ -23,7 +24,9 @@ def async_register_signal_handling(hass: HomeAssistant) -> None:
"""
hass.loop.remove_signal_handler(signal.SIGTERM)
hass.loop.remove_signal_handler(signal.SIGINT)
hass.async_create_task(hass.async_stop(exit_code))
hass.data["homeassistant_stop"] = asyncio.create_task(
hass.async_stop(exit_code)
)
try:
hass.loop.add_signal_handler(signal.SIGTERM, async_signal_handle, 0)

View File

@@ -23,7 +23,7 @@ fnvhash==0.1.0
hass-nabucasa==0.61.0
hassil==1.0.5
home-assistant-bluetooth==1.9.3
home-assistant-frontend==20230224.0
home-assistant-frontend==20230227.0
home-assistant-intents==2023.2.22
httpx==0.23.3
ifaddr==0.1.7

View File

@@ -264,7 +264,8 @@ async def _async_setup_component(
SLOW_SETUP_MAX_WAIT,
)
return False
except Exception: # pylint: disable=broad-except
# pylint: disable-next=broad-except
except (asyncio.CancelledError, SystemExit, Exception):
_LOGGER.exception("Error during setup of component %s", domain)
async_notify_setup_error(hass, domain, integration.documentation)
return False

View File

@@ -39,7 +39,7 @@ def is_installed(package: str) -> bool:
try:
pkg_resources.get_distribution(package)
return True
except (pkg_resources.ResolutionError, pkg_resources.ExtractionError):
except (IndexError, pkg_resources.ResolutionError, pkg_resources.ExtractionError):
req = pkg_resources.Requirement.parse(package)
except ValueError:
# This is a zip file. We no longer use this in Home Assistant,

View File

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

View File

@@ -249,7 +249,7 @@ aiopvpc==4.0.1
aiopyarr==22.11.0
# homeassistant.components.qnap_qsw
aioqsw==0.3.1
aioqsw==0.3.2
# homeassistant.components.recollect_waste
aiorecollect==1.0.8
@@ -276,7 +276,7 @@ aioskybell==22.7.0
aioslimproto==2.1.1
# homeassistant.components.honeywell
aiosomecomfort==0.0.8
aiosomecomfort==0.0.10
# homeassistant.components.steamist
aiosteamist==0.3.2
@@ -422,7 +422,7 @@ beautifulsoup4==4.11.1
# beewi_smartclim==0.0.10
# homeassistant.components.zha
bellows==0.34.7
bellows==0.34.9
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.12.1
@@ -504,7 +504,7 @@ btsmarthub_devicelist==0.2.3
buienradar==1.0.5
# homeassistant.components.caldav
caldav==1.1.1
caldav==1.2.0
# homeassistant.components.circuit
circuit-webhook==1.0.1
@@ -661,7 +661,7 @@ enocean==0.50
enturclient==0.2.4
# homeassistant.components.environment_canada
env_canada==0.5.28
env_canada==0.5.29
# homeassistant.components.enphase_envoy
envoy_reader==0.20.1
@@ -907,7 +907,7 @@ hole==0.8.0
holidays==0.18.0
# homeassistant.components.frontend
home-assistant-frontend==20230224.0
home-assistant-frontend==20230227.0
# homeassistant.components.conversation
home-assistant-intents==2023.2.22
@@ -1201,7 +1201,7 @@ nextcord==2.0.0a8
nextdns==1.3.0
# homeassistant.components.nibe_heatpump
nibe==1.6.0
nibe==2.0.0
# homeassistant.components.niko_home_control
niko-home-control==0.2.1
@@ -1248,7 +1248,7 @@ oauth2client==4.1.3
objgraph==3.5.0
# homeassistant.components.garages_amsterdam
odp-amsterdam==5.0.1
odp-amsterdam==5.1.0
# homeassistant.components.oem
oemthermostat==1.1.1
@@ -2518,7 +2518,7 @@ tololib==0.1.0b4
toonapi==0.2.1
# homeassistant.components.totalconnect
total_connect_client==2023.1
total_connect_client==2023.2
# homeassistant.components.tplink_lte
tp-connected==0.0.4
@@ -2653,7 +2653,7 @@ xboxapi==2.0.1
xiaomi-ble==0.16.4
# homeassistant.components.knx
xknx==2.5.0
xknx==2.6.0
# homeassistant.components.bluesound
# homeassistant.components.fritz
@@ -2670,13 +2670,13 @@ xs1-api-client==3.0.0
yalesmartalarmclient==0.3.9
# homeassistant.components.yalexs_ble
yalexs-ble==2.0.3
yalexs-ble==2.0.4
# homeassistant.components.august
yalexs==1.2.7
# homeassistant.components.august
yalexs_ble==2.0.3
yalexs_ble==2.0.4
# homeassistant.components.yeelight
yeelight==0.7.10
@@ -2724,10 +2724,10 @@ zigpy-xbee==0.16.2
zigpy-zigate==0.10.3
# homeassistant.components.zha
zigpy-znp==0.9.2
zigpy-znp==0.9.3
# homeassistant.components.zha
zigpy==0.53.0
zigpy==0.53.2
# homeassistant.components.zoneminder
zm-py==0.5.2

View File

@@ -227,7 +227,7 @@ aiopvpc==4.0.1
aiopyarr==22.11.0
# homeassistant.components.qnap_qsw
aioqsw==0.3.1
aioqsw==0.3.2
# homeassistant.components.recollect_waste
aiorecollect==1.0.8
@@ -254,7 +254,7 @@ aioskybell==22.7.0
aioslimproto==2.1.1
# homeassistant.components.honeywell
aiosomecomfort==0.0.8
aiosomecomfort==0.0.10
# homeassistant.components.steamist
aiosteamist==0.3.2
@@ -352,7 +352,7 @@ base36==0.1.1
beautifulsoup4==4.11.1
# homeassistant.components.zha
bellows==0.34.7
bellows==0.34.9
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.12.1
@@ -405,7 +405,7 @@ bthome-ble==2.5.2
buienradar==1.0.5
# homeassistant.components.caldav
caldav==1.1.1
caldav==1.2.0
# homeassistant.components.co2signal
co2signal==0.4.2
@@ -514,7 +514,7 @@ energyzero==0.3.1
enocean==0.50
# homeassistant.components.environment_canada
env_canada==0.5.28
env_canada==0.5.29
# homeassistant.components.enphase_envoy
envoy_reader==0.20.1
@@ -690,7 +690,7 @@ hole==0.8.0
holidays==0.18.0
# homeassistant.components.frontend
home-assistant-frontend==20230224.0
home-assistant-frontend==20230227.0
# homeassistant.components.conversation
home-assistant-intents==2023.2.22
@@ -891,7 +891,7 @@ nextcord==2.0.0a8
nextdns==1.3.0
# homeassistant.components.nibe_heatpump
nibe==1.6.0
nibe==2.0.0
# homeassistant.components.nfandroidtv
notifications-android-tv==0.1.5
@@ -923,7 +923,7 @@ oauth2client==4.1.3
objgraph==3.5.0
# homeassistant.components.garages_amsterdam
odp-amsterdam==5.0.1
odp-amsterdam==5.1.0
# homeassistant.components.omnilogic
omnilogic==0.4.5
@@ -1773,7 +1773,7 @@ tololib==0.1.0b4
toonapi==0.2.1
# homeassistant.components.totalconnect
total_connect_client==2023.1
total_connect_client==2023.2
# homeassistant.components.tplink_omada
tplink-omada-client==1.1.0
@@ -1881,7 +1881,7 @@ xbox-webapi==2.0.11
xiaomi-ble==0.16.4
# homeassistant.components.knx
xknx==2.5.0
xknx==2.6.0
# homeassistant.components.bluesound
# homeassistant.components.fritz
@@ -1895,13 +1895,13 @@ xmltodict==0.13.0
yalesmartalarmclient==0.3.9
# homeassistant.components.yalexs_ble
yalexs-ble==2.0.3
yalexs-ble==2.0.4
# homeassistant.components.august
yalexs==1.2.7
# homeassistant.components.august
yalexs_ble==2.0.3
yalexs_ble==2.0.4
# homeassistant.components.yeelight
yeelight==0.7.10
@@ -1934,10 +1934,10 @@ zigpy-xbee==0.16.2
zigpy-zigate==0.10.3
# homeassistant.components.zha
zigpy-znp==0.9.2
zigpy-znp==0.9.3
# homeassistant.components.zha
zigpy==0.53.0
zigpy==0.53.2
# homeassistant.components.zwave_js
zwave-js-server-python==0.46.0

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import ast
from collections import deque
from pathlib import Path
from homeassistant.const import Platform
@@ -118,6 +119,7 @@ ALLOWED_USED_COMPONENTS = {
"input_text",
"media_source",
"onboarding",
"panel_custom",
"persistent_notification",
"person",
"script",
@@ -138,20 +140,19 @@ IGNORE_VIOLATIONS = {
# Has same requirement, gets defaults.
("sql", "recorder"),
# Sharing a base class
("openalpr_cloud", "openalpr_local"),
("lutron_caseta", "lutron"),
("ffmpeg_noise", "ffmpeg_motion"),
# Demo
("demo", "manual"),
("demo", "openalpr_local"),
# This would be a circular dep
("http", "network"),
# This would be a circular dep
("zha", "homeassistant_hardware"),
("zha", "homeassistant_yellow"),
# This should become a helper method that integrations can submit data to
("websocket_api", "lovelace"),
("websocket_api", "shopping_list"),
"logbook",
# Migration wizard from zwave to zwave_js.
"zwave_js",
}
@@ -229,6 +230,7 @@ def find_non_referenced_integrations(
def validate_dependencies(
integrations: dict[str, Integration],
integration: Integration,
check_dependencies: bool,
) -> None:
"""Validate all dependencies."""
# Some integrations are allowed to have violations.
@@ -250,12 +252,60 @@ def validate_dependencies(
"or 'after_dependencies'",
)
if check_dependencies:
_check_circular_deps(
integrations, integration.domain, integration, set(), deque()
)
def _check_circular_deps(
integrations: dict[str, Integration],
start_domain: str,
integration: Integration,
checked: set[str],
checking: deque[str],
) -> None:
"""Check for circular dependencies pointing at starting_domain."""
if integration.domain in checked or integration.domain in checking:
return
checking.append(integration.domain)
for domain in integration.manifest.get("dependencies", []):
if domain == start_domain:
integrations[start_domain].add_error(
"dependencies",
f"Found a circular dependency with {integration.domain} ({', '.join(checking)})",
)
break
_check_circular_deps(
integrations, start_domain, integrations[domain], checked, checking
)
else:
for domain in integration.manifest.get("after_dependencies", []):
if domain == start_domain:
integrations[start_domain].add_error(
"dependencies",
f"Found a circular dependency with after dependencies of {integration.domain} ({', '.join(checking)})",
)
break
_check_circular_deps(
integrations, start_domain, integrations[domain], checked, checking
)
checked.add(integration.domain)
checking.remove(integration.domain)
def validate(integrations: dict[str, Integration], config: Config) -> None:
"""Handle dependencies for integrations."""
# check for non-existing dependencies
for integration in integrations.values():
validate_dependencies(integrations, integration)
validate_dependencies(
integrations,
integration,
check_dependencies=not config.specific_integrations,
)
if config.specific_integrations:
continue

View File

@@ -166,3 +166,4 @@ async def test_step_reauth(
assert len(hass.config_entries.async_entries()) == 1
assert hass.config_entries.async_entries()[0].data[CONF_API_KEY] == new_api_key
await hass.async_block_till_done()

View File

@@ -19,7 +19,7 @@ from homeassistant.components.bluetooth.active_update_coordinator import (
_T,
ActiveBluetoothDataUpdateCoordinator,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
from homeassistant.setup import async_setup_component
@@ -395,3 +395,58 @@ async def test_polling_rejecting_the_first_time(
cancel()
unregister_listener()
async def test_no_polling_after_stop_event(
hass: HomeAssistant,
mock_bleak_scanner_start: MagicMock,
mock_bluetooth_adapters: None,
) -> None:
"""Test we do not poll after the stop event."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
needs_poll_calls = 0
def _needs_poll(
service_info: BluetoothServiceInfoBleak, seconds_since_last_poll: float | None
) -> bool:
nonlocal needs_poll_calls
needs_poll_calls += 1
return True
async def _poll_method(service_info: BluetoothServiceInfoBleak) -> dict[str, Any]:
return {"fake": "data"}
coordinator = MyCoordinator(
hass=hass,
logger=_LOGGER,
address="aa:bb:cc:dd:ee:ff",
mode=BluetoothScanningMode.ACTIVE,
needs_poll_method=_needs_poll,
poll_method=_poll_method,
)
assert coordinator.available is False # no data yet
mock_listener = MagicMock()
unregister_listener = coordinator.async_add_listener(mock_listener)
cancel = coordinator.async_start()
assert needs_poll_calls == 0
inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO)
await hass.async_block_till_done()
assert coordinator.passive_data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO.rssi}
assert coordinator.data == {"fake": "data"}
assert needs_poll_calls == 1
hass.state = CoreState.stopping
await hass.async_block_till_done()
assert needs_poll_calls == 1
# Should not generate a poll now
inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2)
await hass.async_block_till_done()
assert needs_poll_calls == 1
cancel()
unregister_listener()

View File

@@ -16,7 +16,7 @@ from homeassistant.components.bluetooth import (
from homeassistant.components.bluetooth.active_update_processor import (
ActiveBluetoothProcessorCoordinator,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
from homeassistant.setup import async_setup_component
@@ -384,3 +384,65 @@ async def test_rate_limit(
assert async_handle_update.mock_calls[-1] == call({"testdata": 1})
cancel()
async def test_no_polling_after_stop_event(
hass: HomeAssistant,
mock_bleak_scanner_start: MagicMock,
mock_bluetooth_adapters: None,
) -> None:
"""Test we do not poll after the stop event."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
needs_poll_calls = 0
def _update_method(service_info: BluetoothServiceInfoBleak):
return {"testdata": 0}
def _poll_needed(*args, **kwargs):
nonlocal needs_poll_calls
needs_poll_calls += 1
return True
async def _poll(*args, **kwargs):
return {"testdata": 1}
coordinator = ActiveBluetoothProcessorCoordinator(
hass,
_LOGGER,
address="aa:bb:cc:dd:ee:ff",
mode=BluetoothScanningMode.ACTIVE,
update_method=_update_method,
needs_poll_method=_poll_needed,
poll_method=_poll,
)
assert coordinator.available is False # no data yet
processor = MagicMock()
coordinator.async_register_processor(processor)
async_handle_update = processor.async_handle_update
cancel = coordinator.async_start()
inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO)
await hass.async_block_till_done()
assert needs_poll_calls == 1
assert coordinator.available is True
# async_handle_update should have been called twice
# The first time, it was passed the data from parsing the advertisement
# The second time, it was passed the data from polling
assert len(async_handle_update.mock_calls) == 2
assert async_handle_update.mock_calls[0] == call({"testdata": 0})
assert async_handle_update.mock_calls[1] == call({"testdata": 1})
hass.state = CoreState.stopping
await hass.async_block_till_done()
assert needs_poll_calls == 1
# Should not generate a poll now that CoreState is stopping
inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2)
await hass.async_block_till_done()
assert needs_poll_calls == 1
cancel()

View File

@@ -288,7 +288,7 @@ async def test_extended_color_light(
"turn_on",
{
"entity_id": entity_id,
"hs_color": (0, 0),
"hs_color": (236.69291338582678, 100.0),
},
blocking=True,
)
@@ -299,9 +299,9 @@ async def test_extended_color_light(
call(
node_id=1,
endpoint_id=1,
command=clusters.ColorControl.Commands.MoveToColor(
colorX=21168,
colorY=21561,
command=clusters.ColorControl.Commands.MoveToHueAndSaturation(
hue=167,
saturation=254,
transitionTime=0,
optionsMask=0,
optionsOverride=0,

View File

@@ -4,9 +4,9 @@ from contextlib import ExitStack
from typing import Any
from unittest.mock import AsyncMock, Mock, patch
from nibe.coil import Coil
from nibe.coil import Coil, CoilData
from nibe.connection import Connection
from nibe.exceptions import CoilReadException
from nibe.exceptions import ReadException
import pytest
@@ -39,12 +39,11 @@ async def fixture_coils(mock_connection):
"""Return a dict with coil data."""
coils: dict[int, Any] = {}
async def read_coil(coil: Coil, timeout: float = 0) -> Coil:
async def read_coil(coil: Coil, timeout: float = 0) -> CoilData:
nonlocal coils
if (data := coils.get(coil.address, None)) is None:
raise CoilReadException()
coil.value = data
return coil
raise ReadException()
return CoilData(coil, data)
async def read_coils(
coils: Iterable[Coil], timeout: float = 0

View File

@@ -3,7 +3,7 @@ from typing import Any
from unittest.mock import AsyncMock, patch
from freezegun.api import FrozenDateTimeFactory
from nibe.coil import Coil
from nibe.coil import CoilData
from nibe.coil_groups import UNIT_COILGROUPS
from nibe.heatpump import Model
import pytest
@@ -91,6 +91,6 @@ async def test_reset_button(
# Verify reset was written
args = mock_connection.write_coil.call_args
assert args
coil: Coil = args.args[0]
assert coil.address == unit.alarm_reset
coil: CoilData = args.args[0]
assert coil.coil.address == unit.alarm_reset
assert coil.value == 1

View File

@@ -5,9 +5,9 @@ from nibe.coil import Coil
from nibe.exceptions import (
AddressInUseException,
CoilNotFoundException,
CoilReadException,
CoilReadSendException,
CoilWriteException,
ReadException,
ReadSendException,
WriteException,
)
import pytest
@@ -169,7 +169,7 @@ async def test_read_timeout(
"""Test we handle cannot connect error."""
result = await _get_connection_form(hass, connection_type)
mock_connection.verify_connectivity.side_effect = CoilReadException()
mock_connection.verify_connectivity.side_effect = ReadException()
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data)
@@ -190,7 +190,7 @@ async def test_write_timeout(
"""Test we handle cannot connect error."""
result = await _get_connection_form(hass, connection_type)
mock_connection.verify_connectivity.side_effect = CoilWriteException()
mock_connection.verify_connectivity.side_effect = WriteException()
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data)
@@ -232,7 +232,7 @@ async def test_nibegw_invalid_host(
"""Test we handle cannot connect error."""
result = await _get_connection_form(hass, connection_type)
mock_connection.verify_connectivity.side_effect = CoilReadSendException()
mock_connection.verify_connectivity.side_effect = ReadSendException()
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data)

View File

@@ -915,6 +915,7 @@ async def test_unit_conversion_priority(
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == automatic_unit
# Assert the automatic unit conversion is stored in the registry
entry = entity_registry.async_get(entity0.entity_id)
assert entry.unit_of_measurement == automatic_unit
assert entry.options == {
"sensor.private": {"suggested_unit_of_measurement": automatic_unit}
}
@@ -930,6 +931,7 @@ async def test_unit_conversion_priority(
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit
# Assert the suggested unit is stored in the registry
entry = entity_registry.async_get(entity2.entity_id)
assert entry.unit_of_measurement == suggested_unit
assert entry.options == {
"sensor.private": {"suggested_unit_of_measurement": suggested_unit}
}
@@ -1065,6 +1067,7 @@ async def test_unit_conversion_priority_precision(
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == automatic_unit
# Assert the automatic unit conversion is stored in the registry
entry = entity_registry.async_get(entity0.entity_id)
assert entry.unit_of_measurement == automatic_unit
assert entry.options == {
"sensor": {"suggested_display_precision": 2},
"sensor.private": {"suggested_unit_of_measurement": automatic_unit},
@@ -1081,6 +1084,7 @@ async def test_unit_conversion_priority_precision(
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit
# Assert the suggested unit is stored in the registry
entry = entity_registry.async_get(entity2.entity_id)
assert entry.unit_of_measurement == suggested_unit
assert entry.options == {
"sensor": {"suggested_display_precision": 2},
"sensor.private": {"suggested_unit_of_measurement": suggested_unit},
@@ -1154,13 +1158,17 @@ async def test_unit_conversion_priority_suggested_unit_change(
platform.init(empty=True)
# Pre-register entities
entry = entity_registry.async_get_or_create("sensor", "test", "very_unique")
entry = entity_registry.async_get_or_create(
"sensor", "test", "very_unique", unit_of_measurement=original_unit
)
entity_registry.async_update_entity_options(
entry.entity_id,
"sensor.private",
{"suggested_unit_of_measurement": original_unit},
)
entry = entity_registry.async_get_or_create("sensor", "test", "very_unique_2")
entry = entity_registry.async_get_or_create(
"sensor", "test", "very_unique_2", unit_of_measurement=original_unit
)
entity_registry.async_update_entity_options(
entry.entity_id,
"sensor.private",
@@ -1193,11 +1201,124 @@ async def test_unit_conversion_priority_suggested_unit_change(
state = hass.states.get(entity0.entity_id)
assert float(state.state) == pytest.approx(float(original_value))
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == original_unit
# Assert the suggested unit is stored in the registry
entry = entity_registry.async_get(entity0.entity_id)
assert entry.unit_of_measurement == original_unit
assert entry.options == {
"sensor.private": {"suggested_unit_of_measurement": original_unit},
}
# Registered entity -> Follow suggested unit the first time the entity was seen
state = hass.states.get(entity1.entity_id)
assert float(state.state) == pytest.approx(float(original_value))
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == original_unit
# Assert the suggested unit is stored in the registry
entry = entity_registry.async_get(entity1.entity_id)
assert entry.unit_of_measurement == original_unit
assert entry.options == {
"sensor.private": {"suggested_unit_of_measurement": original_unit},
}
@pytest.mark.parametrize(
(
"native_unit_1",
"native_unit_2",
"suggested_unit",
"native_value",
"original_value",
"device_class",
),
[
# Distance
(
UnitOfLength.KILOMETERS,
UnitOfLength.METERS,
UnitOfLength.KILOMETERS,
1000000,
1000,
SensorDeviceClass.DISTANCE,
),
# Energy
(
UnitOfEnergy.KILO_WATT_HOUR,
UnitOfEnergy.WATT_HOUR,
UnitOfEnergy.KILO_WATT_HOUR,
1000000,
1000,
SensorDeviceClass.ENERGY,
),
],
)
async def test_unit_conversion_priority_suggested_unit_change_2(
hass: HomeAssistant,
enable_custom_integrations: None,
native_unit_1,
native_unit_2,
suggested_unit,
native_value,
original_value,
device_class,
) -> None:
"""Test priority of unit conversion."""
hass.config.units = METRIC_SYSTEM
entity_registry = er.async_get(hass)
platform = getattr(hass.components, "test.sensor")
platform.init(empty=True)
# Pre-register entities
entity_registry.async_get_or_create(
"sensor", "test", "very_unique", unit_of_measurement=native_unit_1
)
entity_registry.async_get_or_create(
"sensor", "test", "very_unique_2", unit_of_measurement=native_unit_1
)
platform.ENTITIES["0"] = platform.MockSensor(
name="Test",
device_class=device_class,
native_unit_of_measurement=native_unit_2,
native_value=str(native_value),
unique_id="very_unique",
)
entity0 = platform.ENTITIES["0"]
platform.ENTITIES["1"] = platform.MockSensor(
name="Test",
device_class=device_class,
native_unit_of_measurement=native_unit_2,
native_value=str(native_value),
suggested_unit_of_measurement=suggested_unit,
unique_id="very_unique_2",
)
entity1 = platform.ENTITIES["1"]
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
await hass.async_block_till_done()
# Registered entity -> Follow unit in entity registry
state = hass.states.get(entity0.entity_id)
assert float(state.state) == pytest.approx(float(original_value))
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit_1
# Assert the suggested unit is stored in the registry
entry = entity_registry.async_get(entity0.entity_id)
assert entry.unit_of_measurement == native_unit_1
assert entry.options == {
"sensor.private": {"suggested_unit_of_measurement": native_unit_1},
}
# Registered entity -> Follow unit in entity registry
state = hass.states.get(entity1.entity_id)
assert float(state.state) == pytest.approx(float(original_value))
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit_1
# Assert the suggested unit is stored in the registry
entry = entity_registry.async_get(entity0.entity_id)
assert entry.unit_of_measurement == native_unit_1
assert entry.options == {
"sensor.private": {"suggested_unit_of_measurement": native_unit_1},
}
@pytest.mark.parametrize(

View File

@@ -78,6 +78,29 @@ async def test_import_then_zeroconf(hass: HomeAssistant) -> None:
assert len(mock_setup_entry.mock_calls) == 0
async def test_user(hass: HomeAssistant) -> None:
"""Test the user flow."""
with patch(
"homeassistant.components.thread.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
thread.DOMAIN, context={"source": "user"}
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == "Thread"
assert result["data"] == {}
assert result["options"] == {}
assert len(mock_setup_entry.mock_calls) == 1
config_entry = hass.config_entries.async_entries(thread.DOMAIN)[0]
assert config_entry.data == {}
assert config_entry.options == {}
assert config_entry.title == "Thread"
assert config_entry.unique_id is None
async def test_zeroconf(hass: HomeAssistant) -> None:
"""Test the zeroconf flow."""
with patch(

View File

@@ -71,7 +71,10 @@ async def test_form(hass: HomeAssistant, nvr: NVR) -> None:
), patch(
"homeassistant.components.unifiprotect.async_setup_entry",
return_value=True,
) as mock_setup_entry:
) as mock_setup_entry, patch(
"homeassistant.components.unifiprotect.async_setup",
return_value=True,
) as mock_setup:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
@@ -93,6 +96,7 @@ async def test_form(hass: HomeAssistant, nvr: NVR) -> None:
"verify_ssl": False,
}
assert len(mock_setup_entry.mock_calls) == 1
assert len(mock_setup.mock_calls) == 1
async def test_form_version_too_old(hass: HomeAssistant, old_nvr: NVR) -> None:
@@ -214,7 +218,10 @@ async def test_form_reauth_auth(hass: HomeAssistant, nvr: NVR) -> None:
with patch(
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr",
return_value=nvr,
):
), patch(
"homeassistant.components.unifiprotect.async_setup",
return_value=True,
) as mock_setup:
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
@@ -225,6 +232,7 @@ async def test_form_reauth_auth(hass: HomeAssistant, nvr: NVR) -> None:
assert result3["type"] == FlowResultType.ABORT
assert result3["reason"] == "reauth_successful"
assert len(mock_setup.mock_calls) == 1
async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) -> None:
@@ -332,7 +340,10 @@ async def test_discovered_by_unifi_discovery_direct_connect(
), patch(
"homeassistant.components.unifiprotect.async_setup_entry",
return_value=True,
) as mock_setup_entry:
) as mock_setup_entry, patch(
"homeassistant.components.unifiprotect.async_setup",
return_value=True,
) as mock_setup:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
@@ -353,6 +364,7 @@ async def test_discovered_by_unifi_discovery_direct_connect(
"verify_ssl": True,
}
assert len(mock_setup_entry.mock_calls) == 1
assert len(mock_setup.mock_calls) == 1
async def test_discovered_by_unifi_discovery_direct_connect_updated(
@@ -515,7 +527,10 @@ async def test_discovered_by_unifi_discovery(hass: HomeAssistant, nvr: NVR) -> N
), patch(
"homeassistant.components.unifiprotect.async_setup_entry",
return_value=True,
) as mock_setup_entry:
) as mock_setup_entry, patch(
"homeassistant.components.unifiprotect.async_setup",
return_value=True,
) as mock_setup:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
@@ -536,6 +551,7 @@ async def test_discovered_by_unifi_discovery(hass: HomeAssistant, nvr: NVR) -> N
"verify_ssl": False,
}
assert len(mock_setup_entry.mock_calls) == 1
assert len(mock_setup.mock_calls) == 1
async def test_discovered_by_unifi_discovery_partial(
@@ -567,7 +583,10 @@ async def test_discovered_by_unifi_discovery_partial(
), patch(
"homeassistant.components.unifiprotect.async_setup_entry",
return_value=True,
) as mock_setup_entry:
) as mock_setup_entry, patch(
"homeassistant.components.unifiprotect.async_setup",
return_value=True,
) as mock_setup:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
@@ -588,6 +607,7 @@ async def test_discovered_by_unifi_discovery_partial(
"verify_ssl": False,
}
assert len(mock_setup_entry.mock_calls) == 1
assert len(mock_setup.mock_calls) == 1
async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface(
@@ -736,7 +756,10 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
), patch(
"homeassistant.components.unifiprotect.async_setup_entry",
return_value=True,
) as mock_setup_entry:
) as mock_setup_entry, patch(
"homeassistant.components.unifiprotect.async_setup",
return_value=True,
) as mock_setup:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
@@ -757,6 +780,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
"verify_ssl": True,
}
assert len(mock_setup_entry.mock_calls) == 1
assert len(mock_setup.mock_calls) == 1
async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface_resolver_no_result(

View File

@@ -96,3 +96,20 @@ async def test_async_create_flow_checks_existing_flows_before_startup(
data={"properties": {"id": "aa:bb:cc:dd:ee:ff"}},
)
]
async def test_async_create_flow_does_nothing_after_stop(
hass: HomeAssistant, mock_flow_init
) -> None:
"""Test we no longer create flows when hass is stopping."""
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
hass.state = CoreState.stopping
mock_flow_init.reset_mock()
discovery_flow.async_create_flow(
hass,
"hue",
{"source": config_entries.SOURCE_HOMEKIT},
{"properties": {"id": "aa:bb:cc:dd:ee:ff"}},
)
assert len(mock_flow_init.mock_calls) == 0

View File

@@ -29,6 +29,7 @@ from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.setup import async_set_domains_to_be_loaded, async_setup_component
from homeassistant.util import dt
import homeassistant.util.dt as dt_util
from .common import (
MockConfigEntry,
@@ -999,6 +1000,27 @@ async def test_setup_retrying_during_unload_before_started(hass: HomeAssistant)
)
async def test_setup_does_not_retry_during_shutdown(hass: HomeAssistant) -> None:
"""Test we do not retry when HASS is shutting down."""
entry = MockConfigEntry(domain="test")
mock_setup_entry = AsyncMock(side_effect=ConfigEntryNotReady)
mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry))
mock_entity_platform(hass, "config_flow.test", None)
await entry.async_setup(hass)
assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY
assert len(mock_setup_entry.mock_calls) == 1
hass.state = CoreState.stopping
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5))
await hass.async_block_till_done()
assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY
assert len(mock_setup_entry.mock_calls) == 1
async def test_create_entry_options(hass: HomeAssistant) -> None:
"""Test a config entry being created with options."""

View File

@@ -9,6 +9,7 @@ import gc
import logging
import os
from tempfile import TemporaryDirectory
import time
from typing import Any
from unittest.mock import MagicMock, Mock, PropertyMock, patch
@@ -2003,3 +2004,49 @@ async def test_background_task(hass: HomeAssistant) -> None:
await asyncio.sleep(0)
await hass.async_stop()
assert result.result() == ha.CoreState.stopping
async def test_shutdown_does_not_block_on_normal_tasks(
hass: HomeAssistant,
) -> None:
"""Ensure shutdown does not block on normal tasks."""
result = asyncio.Future()
unshielded_task = asyncio.sleep(10)
async def test_task():
try:
await unshielded_task
except asyncio.CancelledError:
result.set_result(hass.state)
start = time.monotonic()
task = hass.async_create_task(test_task())
await asyncio.sleep(0)
await hass.async_stop()
await asyncio.sleep(0)
assert result.done()
assert task.done()
assert time.monotonic() - start < 0.5
async def test_shutdown_does_not_block_on_shielded_tasks(
hass: HomeAssistant,
) -> None:
"""Ensure shutdown does not block on shielded tasks."""
result = asyncio.Future()
shielded_task = asyncio.shield(asyncio.sleep(10))
async def test_task():
try:
await shielded_task
except asyncio.CancelledError:
result.set_result(hass.state)
start = time.monotonic()
task = hass.async_create_task(test_task())
await asyncio.sleep(0)
await hass.async_stop()
await asyncio.sleep(0)
assert result.done()
assert task.done()
assert time.monotonic() - start < 0.5