Merge pull request #69935 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2022-04-12 16:19:07 -07:00 committed by GitHub
commit 398c7be850
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 430 additions and 128 deletions

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -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:

View File

@ -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."""

View File

@ -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"],

View File

@ -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

View File

@ -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 = (

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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",

View File

@ -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

View File

@ -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",
)

View File

@ -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)

View File

@ -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)]
)

View File

@ -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

View File

@ -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"],

View File

@ -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

View File

@ -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:")

View File

@ -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

View File

@ -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)

View File

@ -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"

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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,
},
)

View File

@ -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

View File

@ -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 (

View File

@ -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()

View File

@ -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,

View File

@ -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", "", "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", "", "", 1, 1),
(METRIC_SYSTEM, "gas", "ft³", "", 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", "", "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", "", "", 1),
(METRIC_SYSTEM, "gas", "ft³", "", 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": [],

View File

@ -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

View File

@ -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