diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 091bde53ae0..8d86edd7ebb 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -469,7 +469,8 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): # The only way we can turn the Chromecast is on is by launching an app if self._chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST: - self._chromecast.play_media(CAST_SPLASH, "image/png") + app_data = {"media_id": CAST_SPLASH, "media_type": "image/png"} + quick_play(self._chromecast, "default_media_receiver", app_data) else: self._chromecast.start_app(pychromecast.config.APP_MEDIA_RECEIVER) diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py index 6252de6e491..dd5842cd2a8 100644 --- a/homeassistant/components/climate/device_condition.py +++ b/homeassistant/components/climate/device_condition.py @@ -75,15 +75,19 @@ def async_condition_from_config( hass: HomeAssistant, config: ConfigType ) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" - if config[CONF_TYPE] == "is_hvac_mode": - attribute = const.ATTR_HVAC_MODE - else: - attribute = const.ATTR_PRESET_MODE def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: """Test if an entity is a certain state.""" - state = hass.states.get(config[ATTR_ENTITY_ID]) - return state.attributes.get(attribute) == config[attribute] if state else False + if (state := hass.states.get(config[ATTR_ENTITY_ID])) is None: + return False + + if config[CONF_TYPE] == "is_hvac_mode": + return state.state == config[const.ATTR_HVAC_MODE] + + return ( + state.attributes.get(const.ATTR_PRESET_MODE) + == config[const.ATTR_PRESET_MODE] + ) return test_is_state diff --git a/homeassistant/components/devolo_home_control/binary_sensor.py b/homeassistant/components/devolo_home_control/binary_sensor.py index 645850ee9d7..a6b18c2b312 100644 --- a/homeassistant/components/devolo_home_control/binary_sensor.py +++ b/homeassistant/components/devolo_home_control/binary_sensor.py @@ -66,9 +66,9 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity): self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str ) -> None: """Initialize a devolo binary sensor.""" - self._binary_sensor_property = device_instance.binary_sensor_property.get( + self._binary_sensor_property = device_instance.binary_sensor_property[ element_uid - ) + ] super().__init__( homecontrol=homecontrol, @@ -82,10 +82,12 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity): ) if self._attr_device_class is None: - if device_instance.binary_sensor_property.get(element_uid).sub_type != "": - self._attr_name += f" {device_instance.binary_sensor_property.get(element_uid).sub_type}" + if device_instance.binary_sensor_property[element_uid].sub_type != "": + self._attr_name += ( + f" {device_instance.binary_sensor_property[element_uid].sub_type}" + ) else: - self._attr_name += f" {device_instance.binary_sensor_property.get(element_uid).sensor_type}" + self._attr_name += f" {device_instance.binary_sensor_property[element_uid].sensor_type}" self._value = self._binary_sensor_property.state @@ -114,9 +116,9 @@ class DevoloRemoteControl(DevoloDeviceEntity, BinarySensorEntity): key: int, ) -> None: """Initialize a devolo remote control.""" - self._remote_control_property = device_instance.remote_control_property.get( + self._remote_control_property = device_instance.remote_control_property[ element_uid - ) + ] super().__init__( homecontrol=homecontrol, diff --git a/homeassistant/components/devolo_home_control/cover.py b/homeassistant/components/devolo_home_control/cover.py index b8aec44133b..bdb42969a28 100644 --- a/homeassistant/components/devolo_home_control/cover.py +++ b/homeassistant/components/devolo_home_control/cover.py @@ -63,7 +63,7 @@ class DevoloCoverDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, CoverEntity): @property def current_cover_position(self) -> int: """Return the current position. 0 is closed. 100 is open.""" - return self._value + return int(self._value) @property def is_closed(self) -> bool: diff --git a/homeassistant/components/devolo_home_control/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py index f4f2432aa6e..b3cb68098e1 100644 --- a/homeassistant/components/devolo_home_control/devolo_device.py +++ b/homeassistant/components/devolo_home_control/devolo_device.py @@ -46,7 +46,7 @@ class DevoloDeviceEntity(Entity): self.subscriber: Subscriber | None = None self.sync_callback = self._sync - self._value: int + self._value: float async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" diff --git a/homeassistant/components/devolo_home_control/manifest.json b/homeassistant/components/devolo_home_control/manifest.json index e9076e3d3da..75cca12d99e 100644 --- a/homeassistant/components/devolo_home_control/manifest.json +++ b/homeassistant/components/devolo_home_control/manifest.json @@ -2,7 +2,7 @@ "domain": "devolo_home_control", "name": "devolo Home Control", "documentation": "https://www.home-assistant.io/integrations/devolo_home_control", - "requirements": ["devolo-home-control-api==0.17.4"], + "requirements": ["devolo-home-control-api==0.18.1"], "after_dependencies": ["zeroconf"], "config_flow": true, "codeowners": ["@2Fake", "@Shutgun"], diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index 446f41a646b..2d023d23e2d 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -83,7 +83,7 @@ class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity, SensorEntity): """Abstract representation of a multi level sensor within devolo Home Control.""" @property - def native_value(self) -> int: + def native_value(self) -> float: """Return the state of the sensor.""" return self._value diff --git a/homeassistant/components/devolo_home_control/siren.py b/homeassistant/components/devolo_home_control/siren.py index ab86dc78032..36a3f82ca5f 100644 --- a/homeassistant/components/devolo_home_control/siren.py +++ b/homeassistant/components/devolo_home_control/siren.py @@ -54,8 +54,8 @@ class DevoloSirenDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, SirenEntity): ) self._attr_available_tones = [ *range( - self._multi_level_switch_property.min, - self._multi_level_switch_property.max + 1, + int(self._multi_level_switch_property.min), + int(self._multi_level_switch_property.max) + 1, ) ] self._attr_supported_features = ( diff --git a/homeassistant/components/devolo_home_control/switch.py b/homeassistant/components/devolo_home_control/switch.py index 4896d66b805..24b1d3545de 100644 --- a/homeassistant/components/devolo_home_control/switch.py +++ b/homeassistant/components/devolo_home_control/switch.py @@ -50,9 +50,9 @@ class DevoloSwitch(DevoloDeviceEntity, SwitchEntity): device_instance=device_instance, element_uid=element_uid, ) - self._binary_switch_property = self._device_instance.binary_switch_property.get( - self._attr_unique_id - ) + self._binary_switch_property = self._device_instance.binary_switch_property[ + self._attr_unique_id # type: ignore[index] + ] self._attr_is_on = self._binary_switch_property.state def turn_on(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index 4d1c039f137..e117eac8a90 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from contextlib import suppress from functools import partial from homeassistant.components.light import ( @@ -198,16 +199,21 @@ class FibaroLight(FibaroDevice, LightEntity): Dimmable and RGB lights can be on based on different properties, so we need to check here several values. + + JSON for HC2 uses always string, HC3 uses int for integers. """ props = self.fibaro_device.properties if self.current_binary_state: return True - if "brightness" in props and props.brightness != "0": - return True - if "currentProgram" in props and props.currentProgram != "0": - return True - if "currentProgramID" in props and props.currentProgramID != "0": - return True + with suppress(ValueError, TypeError): + if "brightness" in props and int(props.brightness) != 0: + return True + with suppress(ValueError, TypeError): + if "currentProgram" in props and int(props.currentProgram) != 0: + return True + with suppress(ValueError, TypeError): + if "currentProgramID" in props and int(props.currentProgramID) != 0: + return True return False diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index f6629cb3938..80c6ce2b811 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -193,9 +193,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) # Force a token refresh to fix a bug where tokens were persisted with # expires_in (relative time delta) and expires_at (absolute time) swapped. - if session.token["expires_at"] >= datetime(2070, 1, 1).timestamp(): + # A google session token typically only lasts a few days between refresh. + now = datetime.now() + if session.token["expires_at"] >= (now + timedelta(days=365)).timestamp(): session.token["expires_in"] = 0 - session.token["expires_at"] = datetime.now().timestamp() + session.token["expires_at"] = now.timestamp() try: await session.async_ensure_token_valid() except aiohttp.ClientResponseError as err: diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 5f2cc386a87..f3f6887570f 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -711,7 +711,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: dev_reg = await async_get_registry(hass) coordinator = HassioDataUpdateCoordinator(hass, entry, dev_reg) hass.data[ADDONS_COORDINATOR] = coordinator - await coordinator.async_refresh() + await coordinator.async_config_entry_first_refresh() hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -848,8 +848,8 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): new_data[DATA_KEY_ADDONS] = { addon[ATTR_SLUG]: { **addon, - **((addons_stats or {}).get(addon[ATTR_SLUG], {})), - ATTR_AUTO_UPDATE: addons_info.get(addon[ATTR_SLUG], {}).get( + **((addons_stats or {}).get(addon[ATTR_SLUG]) or {}), + ATTR_AUTO_UPDATE: (addons_info.get(addon[ATTR_SLUG]) or {}).get( ATTR_AUTO_UPDATE, False ), ATTR_CHANGELOG: (addons_changelogs or {}).get(addon[ATTR_SLUG]), @@ -952,15 +952,27 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): async def _update_addon_stats(self, slug): """Update single addon stats.""" - stats = await self.hassio.get_addon_stats(slug) - return (slug, stats) + try: + stats = await self.hassio.get_addon_stats(slug) + return (slug, stats) + except HassioAPIError as err: + _LOGGER.warning("Could not fetch stats for %s: %s", slug, err) + return (slug, None) async def _update_addon_changelog(self, slug): """Return the changelog for an add-on.""" - changelog = await self.hassio.get_addon_changelog(slug) - return (slug, changelog) + try: + changelog = await self.hassio.get_addon_changelog(slug) + return (slug, changelog) + except HassioAPIError as err: + _LOGGER.warning("Could not fetch changelog for %s: %s", slug, err) + return (slug, None) async def _update_addon_info(self, slug): """Return the info for an add-on.""" - info = await self.hassio.get_addon_info(slug) - return (slug, info) + try: + info = await self.hassio.get_addon_info(slug) + return (slug, info) + except HassioAPIError as err: + _LOGGER.warning("Could not fetch info for %s: %s", slug, err) + return (slug, None) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 63fbb170ca7..c9ec825bebc 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -296,9 +296,3 @@ class KNXClimate(KnxEntity, ClimateEntity): await super().async_added_to_hass() if self._device.mode is not None: self._device.mode.register_device_updated_cb(self.after_update_callback) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect device object when removed.""" - await super().async_will_remove_from_hass() - if self._device.mode is not None: - self._device.mode.unregister_device_updated_cb(self.after_update_callback) diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py index 5f2e14d1466..bd5ae199ccc 100644 --- a/homeassistant/components/knx/knx_entity.py +++ b/homeassistant/components/knx/knx_entity.py @@ -45,4 +45,5 @@ class KnxEntity(Entity): async def async_will_remove_from_hass(self) -> None: """Disconnect device object when removed.""" - self._device.unregister_device_updated_cb(self.after_update_callback) + # will also remove callbacks + self._device.shutdown() diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 604821ae275..0fdabcec9fb 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.20.1"], + "requirements": ["xknx==0.20.2"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver", "iot_class": "local_push", diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index e4cad5c3201..60234cd1b38 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -32,6 +32,9 @@ def async_process_play_media_url( """Update a media URL with authentication if it points at Home Assistant.""" parsed = yarl.URL(media_content_id) + if parsed.scheme and parsed.scheme not in ("http", "https"): + return media_content_id + if parsed.is_absolute(): if not is_hass_url(hass, media_content_id): return media_content_id diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 80f5b4d60c4..115a0e42e01 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -155,7 +155,7 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_SET_ABSOLUTE_POSITION, SET_ABSOLUTE_POSITION_SCHEMA, - SERVICE_SET_ABSOLUTE_POSITION, + "async_set_absolute_position", ) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index fca3e8bec0d..509584a867f 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -463,7 +463,7 @@ class MpdDevice(MediaPlayerEntity): if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_MUSIC play_item = await media_source.async_resolve_media(self.hass, media_id) - media_id = play_item.url + media_id = async_process_play_media_url(self.hass, play_item.url) if media_type == MEDIA_TYPE_PLAYLIST: _LOGGER.debug("Playing playlist: %s", media_id) @@ -476,8 +476,6 @@ class MpdDevice(MediaPlayerEntity): await self._client.load(media_id) await self._client.play() else: - media_id = async_process_play_media_url(self.hass, media_id) - await self._client.clear() self._currentplaylist = None await self._client.add(media_id) diff --git a/homeassistant/components/overkiz/select.py b/homeassistant/components/overkiz/select.py index c097b04d4eb..74d3b3ba282 100644 --- a/homeassistant/components/overkiz/select.py +++ b/homeassistant/components/overkiz/select.py @@ -39,8 +39,7 @@ def _select_option_open_closed_pedestrian( OverkizCommandParam.CLOSED: OverkizCommand.CLOSE, OverkizCommandParam.OPEN: OverkizCommand.OPEN, OverkizCommandParam.PEDESTRIAN: OverkizCommand.SET_PEDESTRIAN_POSITION, - }[OverkizCommandParam(option)], - None, + }[OverkizCommandParam(option)] ) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index f01190097df..bf4108a87c1 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -12,7 +12,7 @@ import logging import os import re from statistics import mean -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Any, Literal, overload from sqlalchemy import bindparam, func from sqlalchemy.exc import SQLAlchemyError, StatementError @@ -125,9 +125,9 @@ STATISTICS_META_BAKERY = "recorder_statistics_meta_bakery" STATISTICS_SHORT_TERM_BAKERY = "recorder_statistics_short_term_bakery" -# Convert pressure and temperature statistics from the native unit used for statistics -# to the units configured by the user -UNIT_CONVERSIONS = { +# Convert pressure, temperature and volume statistics from the normalized unit used for +# statistics to the unit configured by the user +STATISTIC_UNIT_TO_DISPLAY_UNIT_CONVERSIONS = { PRESSURE_PA: lambda x, units: pressure_util.convert( x, PRESSURE_PA, units.pressure_unit ) @@ -145,6 +145,17 @@ UNIT_CONVERSIONS = { else None, } +# Convert volume statistics from the display unit configured by the user +# to the normalized unit used for statistics +# This is used to support adjusting statistics in the display unit +DISPLAY_UNIT_TO_STATISTIC_UNIT_CONVERSIONS: dict[ + str, Callable[[float, UnitSystem], float] +] = { + VOLUME_CUBIC_FEET: lambda x, units: volume_util.convert( + x, _configured_unit(VOLUME_CUBIC_METERS, units), VOLUME_CUBIC_METERS + ), +} + _LOGGER = logging.getLogger(__name__) @@ -721,7 +732,17 @@ def get_metadata( ) +@overload +def _configured_unit(unit: None, units: UnitSystem) -> None: + ... + + +@overload def _configured_unit(unit: str, units: UnitSystem) -> str: + ... + + +def _configured_unit(unit: str | None, units: UnitSystem) -> str | None: """Return the pressure and temperature units configured by the user.""" if unit == PRESSURE_PA: return units.pressure_unit @@ -1163,7 +1184,7 @@ def _sorted_statistics_to_dict( statistic_id = metadata[meta_id]["statistic_id"] convert: Callable[[Any, Any], float | None] if convert_units: - convert = UNIT_CONVERSIONS.get(unit, lambda x, units: x) # type: ignore[arg-type,no-any-return] + convert = STATISTIC_UNIT_TO_DISPLAY_UNIT_CONVERSIONS.get(unit, lambda x, units: x) # type: ignore[arg-type,no-any-return] else: convert = no_conversion ent_results = result[meta_id] @@ -1323,17 +1344,26 @@ def adjust_statistics( if statistic_id not in metadata: return True - tables: tuple[type[Statistics | StatisticsShortTerm], ...] = ( - Statistics, + units = instance.hass.config.units + statistic_unit = metadata[statistic_id][1]["unit_of_measurement"] + display_unit = _configured_unit(statistic_unit, units) + convert = DISPLAY_UNIT_TO_STATISTIC_UNIT_CONVERSIONS.get(display_unit, lambda x, units: x) # type: ignore[arg-type] + sum_adjustment = convert(sum_adjustment, units) + + _adjust_sum_statistics( + session, StatisticsShortTerm, + metadata[statistic_id][0], + start_time, + sum_adjustment, + ) + + _adjust_sum_statistics( + session, + Statistics, + metadata[statistic_id][0], + start_time.replace(minute=0), + sum_adjustment, ) - for table in tables: - _adjust_sum_statistics( - session, - table, - metadata[statistic_id][0], - start_time, - sum_adjustment, - ) return True diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 807a694e39f..33dc8c3dc8d 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -3,7 +3,7 @@ "name": "Renault", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/renault", - "requirements": ["renault-api==0.1.10"], + "requirements": ["renault-api==0.1.11"], "codeowners": ["@epenet"], "iot_class": "cloud_polling", "loggers": ["renault_api"], diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index f37020cb029..adcec0e8b2b 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -363,9 +363,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not entry: return None entry_kw_args: dict = {} - if ( - self.unique_id - and entry.unique_id is None + if self.unique_id and ( + entry.unique_id is None or (is_unique_match and self.unique_id != entry.unique_id) ): entry_kw_args["unique_id"] = self.unique_id diff --git a/homeassistant/components/squeezebox/const.py b/homeassistant/components/squeezebox/const.py index 4664bbeaf43..d8b67504397 100644 --- a/homeassistant/components/squeezebox/const.py +++ b/homeassistant/components/squeezebox/const.py @@ -5,3 +5,4 @@ KNOWN_PLAYERS = "known_players" PLAYER_DISCOVERY_UNSUB = "player_discovery_unsub" DISCOVERY_TASK = "discovery_task" DEFAULT_PORT = 9000 +SQUEEZEBOX_SOURCE_STRINGS = ("source:", "wavin:", "spotify:") diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index d8a1c29b723..923af6c8c64 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -63,7 +63,13 @@ from .browse_media import ( library_payload, media_source_content_filter, ) -from .const import DISCOVERY_TASK, DOMAIN, KNOWN_PLAYERS, PLAYER_DISCOVERY_UNSUB +from .const import ( + DISCOVERY_TASK, + DOMAIN, + KNOWN_PLAYERS, + PLAYER_DISCOVERY_UNSUB, + SQUEEZEBOX_SOURCE_STRINGS, +) SERVICE_CALL_METHOD = "call_method" SERVICE_CALL_QUERY = "call_query" @@ -475,7 +481,9 @@ class SqueezeBoxEntity(MediaPlayerEntity): media_id = play_item.url if media_type in MEDIA_TYPE_MUSIC: - media_id = async_process_play_media_url(self.hass, media_id) + if not media_id.startswith(SQUEEZEBOX_SOURCE_STRINGS): + # do not process special squeezebox "source" media ids + media_id = async_process_play_media_url(self.hass, media_id) await self._player.async_load_url(media_id, cmd) return diff --git a/homeassistant/components/zha/diagnostics.py b/homeassistant/components/zha/diagnostics.py new file mode 100644 index 00000000000..1a775723b0b --- /dev/null +++ b/homeassistant/components/zha/diagnostics.py @@ -0,0 +1,79 @@ +"""Provides diagnostics for ZHA.""" +from __future__ import annotations + +import dataclasses +from typing import Any + +import bellows +import pkg_resources +import zigpy +from zigpy.config import CONF_NWK_EXTENDED_PAN_ID +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 +from homeassistant.const import CONF_UNIQUE_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .core.const import ATTR_IEEE, DATA_ZHA, DATA_ZHA_CONFIG, DATA_ZHA_GATEWAY +from .core.device import ZHADevice +from .core.gateway import ZHAGateway +from .core.helpers import async_get_zha_device + +KEYS_TO_REDACT = { + ATTR_IEEE, + CONF_UNIQUE_ID, + "network_key", + CONF_NWK_EXTENDED_PAN_ID, +} + + +def shallow_asdict(obj: Any) -> dict: + """Return a shallow copy of a dataclass as a dict.""" + if hasattr(obj, "__dataclass_fields__"): + result = {} + + for field in dataclasses.fields(obj): + result[field.name] = shallow_asdict(getattr(obj, field.name)) + + return result + if hasattr(obj, "as_dict"): + return obj.as_dict() + return obj + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict: + """Return diagnostics for a config entry.""" + config: dict = hass.data[DATA_ZHA][DATA_ZHA_CONFIG] + gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + return async_redact_data( + { + "config": config, + "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, + }, + }, + KEYS_TO_REDACT, + ) + + +async def async_get_device_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry +) -> dict: + """Return diagnostics for a device.""" + zha_device: ZHADevice = await async_get_zha_device(hass, device.id) + return async_redact_data(zha_device.zha_device_info, KEYS_TO_REDACT) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 14000fac875..c780bdcb16f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,9 +7,9 @@ "bellows==0.29.0", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.71", - "zigpy-deconz==0.15.0", - "zigpy==0.44.1", + "zha-quirks==0.0.72", + "zigpy-deconz==0.14.0", + "zigpy==0.44.2", "zigpy-xbee==0.14.0", "zigpy-zigate==0.8.0", "zigpy-znp==0.7.0" diff --git a/homeassistant/const.py b/homeassistant/const.py index 9007b681b7b..ee1e7a65731 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/requirements_all.txt b/requirements_all.txt index 44ede223ea9..79bab40278a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -530,7 +530,7 @@ deluge-client==1.7.1 denonavr==0.10.10 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.17.4 +devolo-home-control-api==0.18.1 # homeassistant.components.devolo_home_network devolo-plc-api==0.7.1 @@ -2043,7 +2043,7 @@ raspyrfm-client==1.2.8 regenmaschine==2022.01.0 # homeassistant.components.renault -renault-api==0.1.10 +renault-api==0.1.11 # homeassistant.components.python_script restrictedpython==5.2 @@ -2435,7 +2435,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.20.1 +xknx==0.20.2 # homeassistant.components.bluesound # homeassistant.components.fritz @@ -2473,7 +2473,7 @@ zengge==0.2 zeroconf==0.38.4 # homeassistant.components.zha -zha-quirks==0.0.71 +zha-quirks==0.0.72 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2482,7 +2482,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.15.0 +zigpy-deconz==0.14.0 # homeassistant.components.zha zigpy-xbee==0.14.0 @@ -2494,7 +2494,7 @@ zigpy-zigate==0.8.0 zigpy-znp==0.7.0 # homeassistant.components.zha -zigpy==0.44.1 +zigpy==0.44.2 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e2f826ae1e..6de908fd699 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -385,7 +385,7 @@ deluge-client==1.7.1 denonavr==0.10.10 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.17.4 +devolo-home-control-api==0.18.1 # homeassistant.components.devolo_home_network devolo-plc-api==0.7.1 @@ -1327,7 +1327,7 @@ radios==0.1.1 regenmaschine==2022.01.0 # homeassistant.components.renault -renault-api==0.1.10 +renault-api==0.1.11 # homeassistant.components.python_script restrictedpython==5.2 @@ -1575,7 +1575,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.20.1 +xknx==0.20.2 # homeassistant.components.bluesound # homeassistant.components.fritz @@ -1601,10 +1601,10 @@ youless-api==0.16 zeroconf==0.38.4 # homeassistant.components.zha -zha-quirks==0.0.71 +zha-quirks==0.0.72 # homeassistant.components.zha -zigpy-deconz==0.15.0 +zigpy-deconz==0.14.0 # homeassistant.components.zha zigpy-xbee==0.14.0 @@ -1616,7 +1616,7 @@ zigpy-zigate==0.8.0 zigpy-znp==0.7.0 # homeassistant.components.zha -zigpy==0.44.1 +zigpy==0.44.2 # homeassistant.components.zwave_js zwave-js-server-python==0.35.2 diff --git a/setup.cfg b/setup.cfg index 2959c5e4dee..b391172bd19 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.4.2 +version = 2022.4.3 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 2e6fafb0287..277acf849cf 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -1157,7 +1157,7 @@ async def test_entity_media_content_type(hass: HomeAssistant): assert state.attributes.get("media_content_type") == "movie" -async def test_entity_control(hass: HomeAssistant): +async def test_entity_control(hass: HomeAssistant, quick_play_mock): """Test various device and media controls.""" entity_id = "media_player.speaker" reg = er.async_get(hass) @@ -1200,8 +1200,13 @@ async def test_entity_control(hass: HomeAssistant): # Turn on await common.async_turn_on(hass, entity_id) - chromecast.play_media.assert_called_once_with( - "https://www.home-assistant.io/images/cast/splash.png", "image/png" + quick_play_mock.assert_called_once_with( + chromecast, + "default_media_receiver", + { + "media_id": "https://www.home-assistant.io/images/cast/splash.png", + "media_type": "image/png", + }, ) chromecast.quit_app.reset_mock() diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py index 65c1e17048b..ca3f4388c0a 100644 --- a/tests/components/climate/test_device_condition.py +++ b/tests/components/climate/test_device_condition.py @@ -92,15 +92,6 @@ async def test_get_conditions( async def test_if_state(hass, calls): """Test for turn_on and turn_off conditions.""" - hass.states.async_set( - "climate.entity", - const.HVAC_MODE_COOL, - { - const.ATTR_HVAC_MODE: const.HVAC_MODE_COOL, - const.ATTR_PRESET_MODE: const.PRESET_AWAY, - }, - ) - assert await async_setup_component( hass, automation.DOMAIN, @@ -147,6 +138,20 @@ async def test_if_state(hass, calls): ] }, ) + + # Should not fire, entity doesn't exist yet + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_COOL, + { + const.ATTR_PRESET_MODE: const.PRESET_AWAY, + }, + ) + hass.bus.async_fire("test_event1") await hass.async_block_till_done() assert len(calls) == 1 @@ -156,7 +161,6 @@ async def test_if_state(hass, calls): "climate.entity", const.HVAC_MODE_AUTO, { - const.ATTR_HVAC_MODE: const.HVAC_MODE_AUTO, const.ATTR_PRESET_MODE: const.PRESET_AWAY, }, ) @@ -176,7 +180,6 @@ async def test_if_state(hass, calls): "climate.entity", const.HVAC_MODE_AUTO, { - const.ATTR_HVAC_MODE: const.HVAC_MODE_AUTO, const.ATTR_PRESET_MODE: const.PRESET_HOME, }, ) diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index 3407c26fc2f..14d1d06ef38 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -509,3 +509,30 @@ async def test_no_os_entity(hass): # Verify that the entity does not exist assert not hass.states.get("update.home_assistant_operating_system_update") + + +async def test_setting_up_core_update_when_addon_fails(hass, caplog): + """Test setting up core update when single addon fails.""" + with patch.dict(os.environ, MOCK_ENVIRON), patch( + "homeassistant.components.hassio.HassIO.get_addon_stats", + side_effect=HassioAPIError("add-on is not running"), + ), patch( + "homeassistant.components.hassio.HassIO.get_addon_changelog", + side_effect=HassioAPIError("add-on is not running"), + ), patch( + "homeassistant.components.hassio.HassIO.get_addon_info", + side_effect=HassioAPIError("add-on is not running"), + ): + result = await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + assert result + await hass.async_block_till_done() + + # Verify that the core update entity does exist + state = hass.states.get("update.home_assistant_core_update") + assert state + assert state.state == "on" + assert "Could not fetch stats for test: add-on is not running" in caplog.text diff --git a/tests/components/media_player/test_browse_media.py b/tests/components/media_player/test_browse_media.py index 6741432024e..ea1e3b4fc36 100644 --- a/tests/components/media_player/test_browse_media.py +++ b/tests/components/media_player/test_browse_media.py @@ -36,6 +36,11 @@ async def test_process_play_media_url(hass, mock_sign_path): async_process_play_media_url(hass, "https://not-hass.com/path") == "https://not-hass.com/path" ) + # Not changing a url that is not http/https + assert ( + async_process_play_media_url(hass, "file:///tmp/test.mp3") + == "file:///tmp/test.mp3" + ) # Testing signing hass URLs assert ( diff --git a/tests/components/profiler/test_init.py b/tests/components/profiler/test_init.py index 4c88e6170c6..e4a1f5a5f1b 100644 --- a/tests/components/profiler/test_init.py +++ b/tests/components/profiler/test_init.py @@ -94,10 +94,11 @@ async def test_object_growth_logging(hass, caplog): assert hass.services.has_service(DOMAIN, SERVICE_START_LOG_OBJECTS) assert hass.services.has_service(DOMAIN, SERVICE_STOP_LOG_OBJECTS) - await hass.services.async_call( - DOMAIN, SERVICE_START_LOG_OBJECTS, {CONF_SCAN_INTERVAL: 10} - ) - await hass.async_block_till_done() + with patch("homeassistant.components.profiler.objgraph.growth"): + await hass.services.async_call( + DOMAIN, SERVICE_START_LOG_OBJECTS, {CONF_SCAN_INTERVAL: 10} + ) + await hass.async_block_till_done() assert "Growth" in caplog.text caplog.clear() diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 19169c20dd3..d2a9d10caf2 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -1451,6 +1451,31 @@ async def test_update_missing_mac_unique_id_ssdp_location_added_from_ssdp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" +@pytest.mark.usefixtures( + "remote", "remotews", "remoteencws_failing", "rest_api_failing" +) +async def test_update_zeroconf_discovery_preserved_unique_id( + hass: HomeAssistant, +) -> None: + """Test zeroconf discovery preserves unique id.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={**MOCK_OLD_ENTRY, CONF_MAC: "aa:bb:zz:ee:rr:oo"}, + unique_id="original", + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "abort" + assert result["reason"] == "not_supported" + assert entry.data[CONF_MAC] == "aa:bb:zz:ee:rr:oo" + assert entry.unique_id == "original" + + @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_added_ssdp_location_updated_from_ssdp( hass: HomeAssistant, diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 33550495589..f9f1c058626 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -325,20 +325,20 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes @pytest.mark.parametrize("state_class", ["total"]) @pytest.mark.parametrize( - "units,device_class,unit,display_unit,factor,factor2", + "units,device_class,unit,display_unit,factor", [ - (IMPERIAL_SYSTEM, "energy", "kWh", "kWh", 1, 1), - (IMPERIAL_SYSTEM, "energy", "Wh", "kWh", 1 / 1000, 1), - (IMPERIAL_SYSTEM, "monetary", "EUR", "EUR", 1, 1), - (IMPERIAL_SYSTEM, "monetary", "SEK", "SEK", 1, 1), - (IMPERIAL_SYSTEM, "gas", "m³", "ft³", 35.314666711, 35.314666711), - (IMPERIAL_SYSTEM, "gas", "ft³", "ft³", 1, 35.314666711), - (METRIC_SYSTEM, "energy", "kWh", "kWh", 1, 1), - (METRIC_SYSTEM, "energy", "Wh", "kWh", 1 / 1000, 1), - (METRIC_SYSTEM, "monetary", "EUR", "EUR", 1, 1), - (METRIC_SYSTEM, "monetary", "SEK", "SEK", 1, 1), - (METRIC_SYSTEM, "gas", "m³", "m³", 1, 1), - (METRIC_SYSTEM, "gas", "ft³", "m³", 0.0283168466, 1), + (IMPERIAL_SYSTEM, "energy", "kWh", "kWh", 1), + (IMPERIAL_SYSTEM, "energy", "Wh", "kWh", 1 / 1000), + (IMPERIAL_SYSTEM, "monetary", "EUR", "EUR", 1), + (IMPERIAL_SYSTEM, "monetary", "SEK", "SEK", 1), + (IMPERIAL_SYSTEM, "gas", "m³", "ft³", 35.314666711), + (IMPERIAL_SYSTEM, "gas", "ft³", "ft³", 1), + (METRIC_SYSTEM, "energy", "kWh", "kWh", 1), + (METRIC_SYSTEM, "energy", "Wh", "kWh", 1 / 1000), + (METRIC_SYSTEM, "monetary", "EUR", "EUR", 1), + (METRIC_SYSTEM, "monetary", "SEK", "SEK", 1), + (METRIC_SYSTEM, "gas", "m³", "m³", 1), + (METRIC_SYSTEM, "gas", "ft³", "m³", 0.0283168466), ], ) async def test_compile_hourly_sum_statistics_amount( @@ -351,7 +351,6 @@ async def test_compile_hourly_sum_statistics_amount( unit, display_unit, factor, - factor2, ): """Test compiling hourly statistics.""" period0 = dt_util.utcnow() @@ -480,8 +479,8 @@ async def test_compile_hourly_sum_statistics_amount( assert response["success"] await async_wait_recording_done_without_instance(hass) - expected_stats["sensor.test1"][1]["sum"] = approx(factor * 40.0 + factor2 * 100) - expected_stats["sensor.test1"][2]["sum"] = approx(factor * 70.0 + factor2 * 100) + expected_stats["sensor.test1"][1]["sum"] = approx(factor * 40.0 + 100) + expected_stats["sensor.test1"][2]["sum"] = approx(factor * 70.0 + 100) stats = statistics_during_period(hass, period0, period="5minute") assert stats == expected_stats @@ -499,8 +498,8 @@ async def test_compile_hourly_sum_statistics_amount( assert response["success"] await async_wait_recording_done_without_instance(hass) - expected_stats["sensor.test1"][1]["sum"] = approx(factor * 40.0 + factor2 * 100) - expected_stats["sensor.test1"][2]["sum"] = approx(factor * 70.0 - factor2 * 300) + expected_stats["sensor.test1"][1]["sum"] = approx(factor * 40.0 + 100) + expected_stats["sensor.test1"][2]["sum"] = approx(factor * 70.0 - 300) stats = statistics_during_period(hass, period0, period="5minute") assert stats == expected_stats @@ -2464,6 +2463,16 @@ def test_compile_statistics_hourly_daily_monthly_summary( }, ] + # Adjust the inserted statistics + sum_adjustment = -10 + sum_adjustement_start = zero + timedelta(minutes=65) + for i in range(13, 24): + expected_sums["sensor.test4"][i] += sum_adjustment + recorder.async_adjust_statistics( + "sensor.test4", sum_adjustement_start, sum_adjustment + ) + wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="5minute") expected_stats = { "sensor.test1": [], diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 0e969b1b0f3..cb562cd5eaa 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -11,6 +11,7 @@ from zigpy.const import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE import zigpy.device import zigpy.group import zigpy.profiles +from zigpy.state import State import zigpy.types import zigpy.zdo.types as zdo_t @@ -54,6 +55,7 @@ def zigpy_app_controller(): app.ieee.return_value = zigpy.types.EUI64.convert("00:15:8d:00:02:32:4f:32") type(app).nwk = PropertyMock(return_value=zigpy.types.NWK(0x0000)) type(app).devices = PropertyMock(return_value={}) + type(app).state = PropertyMock(return_value=State()) return app diff --git a/tests/components/zha/test_diagnostics.py b/tests/components/zha/test_diagnostics.py new file mode 100644 index 00000000000..804b6d73316 --- /dev/null +++ b/tests/components/zha/test_diagnostics.py @@ -0,0 +1,86 @@ +"""Tests for the diagnostics data provided by the ESPHome integration.""" + + +import pytest +import zigpy.profiles.zha as zha +import zigpy.zcl.clusters.security as security + +from homeassistant.components.diagnostics.const import REDACTED +from homeassistant.components.zha.core.device import ZHADevice +from homeassistant.components.zha.diagnostics import KEYS_TO_REDACT +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import async_get + +from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE + +from tests.components.diagnostics import ( + get_diagnostics_for_config_entry, + get_diagnostics_for_device, +) + +CONFIG_ENTRY_DIAGNOSTICS_KEYS = [ + "config", + "config_entry", + "application_state", + "versions", +] + + +@pytest.fixture +def zigpy_device(zigpy_device_mock): + """Device tracker zigpy device.""" + endpoints = { + 1: { + SIG_EP_INPUT: [security.IasAce.cluster_id], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zha.DeviceType.IAS_ANCILLARY_CONTROL, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + } + return zigpy_device_mock( + endpoints, node_descriptor=b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00" + ) + + +async def test_diagnostics_for_config_entry( + hass: HomeAssistant, + hass_client, + config_entry, + zha_device_joined, + zigpy_device, +): + """Test diagnostics for config entry.""" + await zha_device_joined(zigpy_device) + diagnostics_data = await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) + assert diagnostics_data + for key in CONFIG_ENTRY_DIAGNOSTICS_KEYS: + assert key in diagnostics_data + assert diagnostics_data[key] is not None + + +async def test_diagnostics_for_device( + hass: HomeAssistant, + hass_client, + config_entry, + zha_device_joined, + zigpy_device, +): + """Test diagnostics for device.""" + + zha_device: ZHADevice = await zha_device_joined(zigpy_device) + dev_reg = async_get(hass) + device = dev_reg.async_get_device({("zha", str(zha_device.ieee))}) + assert device + diagnostics_data = await get_diagnostics_for_device( + hass, hass_client, config_entry, device + ) + assert diagnostics_data + device_info: dict = zha_device.zha_device_info + for key, value in device_info.items(): + assert key in diagnostics_data + if key not in KEYS_TO_REDACT: + assert key in diagnostics_data + else: + assert diagnostics_data[key] == REDACTED