Compare commits

...

26 Commits

Author SHA1 Message Date
Paulus Schoutsen
898af3e04c Merge pull request #68001 from home-assistant/rc 2022-03-11 17:11:03 -08:00
Diogo Gomes
3de341099f Bump pymediaroom (#68016) 2022-03-11 15:45:40 -08:00
Paulus Schoutsen
7fb76c68bb Bumped version to 2022.3.4 2022-03-11 09:25:55 -08:00
Guido Schmitz
7de5e070fb Bump pysabnzbd to 1.1.1 (#67971) 2022-03-11 09:24:50 -08:00
Tom Harris
1bfb01e0d1 Rollback pyinsteon (#67956) 2022-03-11 09:24:50 -08:00
Erik Montnemery
ca664ab5a5 Correct local import of paho-mqtt (#67944)
* Correct local import of paho-mqtt

* Remove MqttClientSetup.mqtt class attribute

* Remove reference to MqttClientSetup.mqtt
2022-03-11 09:24:49 -08:00
Franck Nijhof
5a39e63d25 Update radios to 0.1.1 (#67902) 2022-03-11 09:24:48 -08:00
Joakim Plate
c608cafebd Make sure blueprint cache is flushed on script reload (#67899) 2022-03-11 09:24:47 -08:00
Shay Levy
07e70c81b0 Fix shelly duo scene restore (#67871) 2022-03-11 09:24:46 -08:00
J. Nick Koston
cad397d6a7 Add missing callback decorator to sun (#67840) 2022-03-11 09:24:45 -08:00
Raman Gupta
c22af2c82a Bump zwave-js-server-python to 0.35.2 (#67839) 2022-03-11 09:24:45 -08:00
Richard de Boer
f5b6d93706 Support playing local "file" media on Kodi (#67832)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-03-11 09:24:44 -08:00
cheng2wei
28b3edf6b2 Fix discord embed class initialization (#67831) 2022-03-11 09:24:43 -08:00
Paulus Schoutsen
737c502e94 Merge pull request #67838 from home-assistant/rc 2022-03-07 21:51:30 -08:00
Paulus Schoutsen
a1abcbc7eb Bumped version to 2022.3.3 2022-03-07 20:45:49 -08:00
J. Nick Koston
b09ab2dafb Prevent scene from restoring unavailable states (#67836) 2022-03-07 20:45:44 -08:00
Teemu R
4e6fc3615b Bump python-miio version to 0.5.11 (#67824) 2022-03-07 20:45:43 -08:00
Bram Kragten
580c998552 Update frontend to 20220301.1 (#67812) 2022-03-07 20:45:25 -08:00
Franck Nijhof
97ba17d1ec Catch Elgato connection errors (#67799) 2022-03-07 20:44:09 -08:00
J. Nick Koston
8d7cdceb75 Handle fan_modes being set to None in homekit (#67790) 2022-03-07 20:44:08 -08:00
Simone Chemelli
dfa1c3abb3 Fix profile name update for Shelly Valve (#67778) 2022-03-07 20:44:08 -08:00
Simone Chemelli
c807c57a9b Fix internet access switch for old discovery (#67777) 2022-03-07 20:44:07 -08:00
J. Nick Koston
f4ec7e0902 Prevent polling from recreating an entity after removal (#67750) 2022-03-07 20:44:06 -08:00
G Johansson
814c96834e Fix temperature stepping in Sensibo (#67737)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-03-07 20:44:05 -08:00
muppet3000
87492e6b3e Fix timezone for growatt lastdataupdate (#67684)
* Added timezone for growatt lastdataupdate (#67646)

* Growatt lastdataupdate set to local timezone
2022-03-07 20:44:05 -08:00
Jan Bouwhuis
4aaafb0a99 Fix false positive MQTT climate deprecation warnings for defaults (#67661)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-03-07 20:44:04 -08:00
33 changed files with 463 additions and 71 deletions

View File

@@ -20,9 +20,13 @@ _LOGGER = logging.getLogger(__name__)
ATTR_EMBED = "embed"
ATTR_EMBED_AUTHOR = "author"
ATTR_EMBED_COLOR = "color"
ATTR_EMBED_DESCRIPTION = "description"
ATTR_EMBED_FIELDS = "fields"
ATTR_EMBED_FOOTER = "footer"
ATTR_EMBED_TITLE = "title"
ATTR_EMBED_THUMBNAIL = "thumbnail"
ATTR_EMBED_URL = "url"
ATTR_IMAGES = "images"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_TOKEN): cv.string})
@@ -64,10 +68,16 @@ class DiscordNotificationService(BaseNotificationService):
embeds: list[nextcord.Embed] = []
if ATTR_EMBED in data:
embedding = data[ATTR_EMBED]
title = embedding.get(ATTR_EMBED_TITLE) or nextcord.Embed.Empty
description = embedding.get(ATTR_EMBED_DESCRIPTION) or nextcord.Embed.Empty
color = embedding.get(ATTR_EMBED_COLOR) or nextcord.Embed.Empty
url = embedding.get(ATTR_EMBED_URL) or nextcord.Embed.Empty
fields = embedding.get(ATTR_EMBED_FIELDS) or []
if embedding:
embed = nextcord.Embed(**embedding)
embed = nextcord.Embed(
title=title, description=description, color=color, url=url
)
for field in fields:
embed.add_field(**field)
if ATTR_EMBED_FOOTER in embedding:

View File

@@ -1,13 +1,13 @@
"""Support for Elgato Lights."""
from typing import NamedTuple
from elgato import Elgato, Info, State
from elgato import Elgato, ElgatoConnectionError, Info, State
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
@@ -31,12 +31,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
session=session,
)
async def _async_update_data() -> State:
"""Fetch Elgato data."""
try:
return await elgato.state()
except ElgatoConnectionError as err:
raise UpdateFailed(err) from err
coordinator: DataUpdateCoordinator[State] = DataUpdateCoordinator(
hass,
LOGGER,
name=f"{DOMAIN}_{entry.data[CONF_HOST]}",
update_interval=SCAN_INTERVAL,
update_method=elgato.state,
update_method=_async_update_data,
)
await coordinator.async_config_entry_first_refresh()

View File

@@ -392,6 +392,8 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
)
self.mesh_role = MeshRoles.NONE
for mac, info in hosts.items():
if info.ip_address:
info.wan_access = self._get_wan_access(info.ip_address)
if self.manage_device_info(info, mac, consider_home):
new_device = True
self.send_signal_device_update(new_device)

View File

@@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
"home-assistant-frontend==20220301.0"
"home-assistant-frontend==20220301.1"
],
"dependencies": [
"api",
@@ -13,7 +13,8 @@
"diagnostics",
"http",
"lovelace",
"onboarding", "search",
"onboarding",
"search",
"system_log",
"websocket_api"
],

View File

@@ -222,7 +222,7 @@ class GrowattData:
date_now = dt.now().date()
last_updated_time = dt.parse_time(str(sorted_keys[-1]))
mix_detail["lastdataupdate"] = datetime.datetime.combine(
date_now, last_updated_time
date_now, last_updated_time, dt.DEFAULT_TIME_ZONE
)
# Dashboard data is largely inaccurate for mix system but it is the only call with the ability to return the combined

View File

@@ -285,20 +285,19 @@ class Thermostat(HomeAccessory):
CHAR_CURRENT_HUMIDITY, value=50
)
fan_modes = self.fan_modes = {
fan_mode.lower(): fan_mode
for fan_mode in attributes.get(ATTR_FAN_MODES, [])
}
fan_modes = {}
self.ordered_fan_speeds = []
if (
features & SUPPORT_FAN_MODE
and fan_modes
and PRE_DEFINED_FAN_MODES.intersection(fan_modes)
):
self.ordered_fan_speeds = [
speed for speed in ORDERED_FAN_SPEEDS if speed in fan_modes
]
self.fan_chars.append(CHAR_ROTATION_SPEED)
if features & SUPPORT_FAN_MODE:
fan_modes = {
fan_mode.lower(): fan_mode
for fan_mode in attributes.get(ATTR_FAN_MODES) or []
}
if fan_modes and PRE_DEFINED_FAN_MODES.intersection(fan_modes):
self.ordered_fan_speeds = [
speed for speed in ORDERED_FAN_SPEEDS if speed in fan_modes
]
self.fan_chars.append(CHAR_ROTATION_SPEED)
if FAN_AUTO in fan_modes and (FAN_ON in fan_modes or self.ordered_fan_speeds):
self.fan_chars.append(CHAR_TARGET_FAN_STATE)

View File

@@ -3,7 +3,7 @@
"name": "Insteon",
"documentation": "https://www.home-assistant.io/integrations/insteon",
"requirements": [
"pyinsteon==1.0.16"
"pyinsteon==1.0.13"
],
"codeowners": [
"@teharris1"

View File

@@ -717,6 +717,8 @@ class KodiEntity(MediaPlayerEntity):
await self._kodi.play_channel(int(media_id))
elif media_type_lower == MEDIA_TYPE_PLAYLIST:
await self._kodi.play_playlist(int(media_id))
elif media_type_lower == "file":
await self._kodi.play_file(media_id)
elif media_type_lower == "directory":
await self._kodi.play_directory(media_id)
elif media_type_lower in [

View File

@@ -2,7 +2,7 @@
"domain": "mediaroom",
"name": "Mediaroom",
"documentation": "https://www.home-assistant.io/integrations/mediaroom",
"requirements": ["pymediaroom==0.6.4.1"],
"requirements": ["pymediaroom==0.6.5.4"],
"codeowners": ["@dgomes"],
"iot_class": "local_polling",
"loggers": ["pymediaroom"]

View File

@@ -13,7 +13,7 @@ import logging
from operator import attrgetter
import ssl
import time
from typing import Any, Union, cast
from typing import TYPE_CHECKING, Any, Union, cast
import uuid
import attr
@@ -113,6 +113,11 @@ from .models import (
)
from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic
if TYPE_CHECKING:
# Only import for paho-mqtt type checking here, imports are done locally
# because integrations should be able to optionally rely on MQTT.
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
_LOGGER = logging.getLogger(__name__)
_SENTINEL = object()
@@ -759,23 +764,23 @@ class Subscription:
class MqttClientSetup:
"""Helper class to setup the paho mqtt client from config."""
# We don't import on the top because some integrations
# should be able to optionally rely on MQTT.
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
def __init__(self, config: ConfigType) -> None:
"""Initialize the MQTT client setup helper."""
# We don't import on the top because some integrations
# should be able to optionally rely on MQTT.
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
if config[CONF_PROTOCOL] == PROTOCOL_31:
proto = self.mqtt.MQTTv31
proto = mqtt.MQTTv31
else:
proto = self.mqtt.MQTTv311
proto = mqtt.MQTTv311
if (client_id := config.get(CONF_CLIENT_ID)) is None:
# PAHO MQTT relies on the MQTT server to generate random client IDs.
# However, that feature is not mandatory so we generate our own.
client_id = self.mqtt.base62(uuid.uuid4().int, padding=22)
self._client = self.mqtt.Client(client_id, protocol=proto)
client_id = mqtt.base62(uuid.uuid4().int, padding=22)
self._client = mqtt.Client(client_id, protocol=proto)
# Enable logging
self._client.enable_logger()

View File

@@ -271,7 +271,7 @@ _PLATFORM_SCHEMA_BASE = SCHEMA_BASE.extend(
vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_HOLD_LIST, default=list): cv.ensure_list,
vol.Optional(CONF_HOLD_LIST): cv.ensure_list,
vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(
@@ -298,7 +298,7 @@ _PLATFORM_SCHEMA_BASE = SCHEMA_BASE.extend(
),
vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean,
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean,
vol.Optional(CONF_SEND_IF_OFF): cv.boolean,
vol.Optional(CONF_ACTION_TEMPLATE): cv.template,
vol.Optional(CONF_ACTION_TOPIC): mqtt.valid_subscribe_topic,
# CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together
@@ -431,6 +431,12 @@ class MqttClimate(MqttEntity, ClimateEntity):
self._feature_preset_mode = False
self._optimistic_preset_mode = None
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
self._send_if_off = True
# AWAY and HOLD mode topics and templates are deprecated,
# support will be removed with release 2022.9
self._hold_list = []
MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
@staticmethod
@@ -499,6 +505,15 @@ class MqttClimate(MqttEntity, ClimateEntity):
self._command_templates = command_templates
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
if CONF_SEND_IF_OFF in config:
self._send_if_off = config[CONF_SEND_IF_OFF]
# AWAY and HOLD mode topics and templates are deprecated,
# support will be removed with release 2022.9
if CONF_HOLD_LIST in config:
self._hold_list = config[CONF_HOLD_LIST]
def _prepare_subscribe_topics(self): # noqa: C901
"""(Re)Subscribe to topics."""
topics = {}
@@ -806,7 +821,9 @@ class MqttClimate(MqttEntity, ClimateEntity):
):
presets.append(PRESET_AWAY)
presets.extend(self._config[CONF_HOLD_LIST])
# AWAY and HOLD mode topics and templates are deprecated,
# support will be removed with release 2022.9
presets.extend(self._hold_list)
if presets:
presets.insert(0, PRESET_NONE)
@@ -847,10 +864,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
setattr(self, attr, temp)
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
if (
self._config[CONF_SEND_IF_OFF]
or self._current_operation != HVAC_MODE_OFF
):
if self._send_if_off or self._current_operation != HVAC_MODE_OFF:
payload = self._command_templates[cmnd_template](temp)
await self._publish(cmnd_topic, payload)
@@ -890,7 +904,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
async def async_set_swing_mode(self, swing_mode):
"""Set new swing mode."""
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
if self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF:
if self._send_if_off or self._current_operation != HVAC_MODE_OFF:
payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE](
swing_mode
)
@@ -903,7 +917,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
async def async_set_fan_mode(self, fan_mode):
"""Set new target temperature."""
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
if self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF:
if self._send_if_off or self._current_operation != HVAC_MODE_OFF:
payload = self._command_templates[CONF_FAN_MODE_COMMAND_TEMPLATE](fan_mode)
await self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload)

View File

@@ -319,6 +319,10 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
def try_connection(hass, broker, port, username, password, protocol="3.1"):
"""Test if we can connect to an MQTT broker."""
# We don't import on the top because some integrations
# should be able to optionally rely on MQTT.
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
# Get the config from configuration.yaml
yaml_config = hass.data.get(DATA_MQTT_CONFIG, {})
entry_config = {
@@ -334,7 +338,7 @@ def try_connection(hass, broker, port, username, password, protocol="3.1"):
def on_connect(client_, userdata, flags, result_code):
"""Handle connection result."""
result.put(result_code == MqttClientSetup.mqtt.CONNACK_ACCEPTED)
result.put(result_code == mqtt.CONNACK_ACCEPTED)
client.on_connect = on_connect

View File

@@ -3,7 +3,7 @@
"name": "Radio Browser",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/radio",
"requirements": ["radios==0.1.0"],
"requirements": ["radios==0.1.1"],
"codeowners": ["@frenck"],
"iot_class": "cloud_polling"
}

View File

@@ -2,7 +2,7 @@
"domain": "sabnzbd",
"name": "SABnzbd",
"documentation": "https://www.home-assistant.io/integrations/sabnzbd",
"requirements": ["pysabnzbd==1.1.0"],
"requirements": ["pysabnzbd==1.1.1"],
"dependencies": ["configurator"],
"after_dependencies": ["discovery"],
"codeowners": [],

View File

@@ -10,7 +10,7 @@ import voluptuous as vol
from homeassistant.components.light import ATTR_TRANSITION
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON
from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON, STATE_UNAVAILABLE
from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import RestoreEntity
@@ -117,7 +117,11 @@ class Scene(RestoreEntity):
"""Call when the scene is added to hass."""
await super().async_internal_added_to_hass()
state = await self.async_get_last_state()
if state is not None and state.state is not None:
if (
state is not None
and state.state is not None
and state.state != STATE_UNAVAILABLE
):
self.__last_activated = state.state
def activate(self, **kwargs: Any) -> None:

View File

@@ -175,7 +175,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Call a service to reload scripts."""
if (conf := await component.async_prepare_reload()) is None:
return
async_get_blueprints(hass).async_reset_cache()
await _async_process_config(hass, conf, component)
async def turn_on_service(service: ServiceCall) -> None:

View File

@@ -15,6 +15,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, TIMEOUT
MAX_POSSIBLE_STEP = 1000
class SensiboDataUpdateCoordinator(DataUpdateCoordinator):
"""A Sensibo Data Update Coordinator."""
@@ -74,7 +76,11 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator):
.get("values", [0, 1])
)
if temperatures_list:
temperature_step = temperatures_list[1] - temperatures_list[0]
diff = MAX_POSSIBLE_STEP
for i in range(len(temperatures_list) - 1):
if temperatures_list[i + 1] - temperatures_list[i] < diff:
diff = temperatures_list[i + 1] - temperatures_list[i]
temperature_step = diff
active_features = list(ac_states)
full_features = set()

View File

@@ -317,4 +317,14 @@ class BlockSleepingClimate(
if self.device_block and self.block:
_LOGGER.debug("Entity %s attached to blocks", self.name)
assert self.block.channel
self._preset_modes = [
PRESET_NONE,
*self.wrapper.device.settings["thermostats"][int(self.block.channel)][
"schedule_profile_names"
],
]
self.async_write_ha_state()

View File

@@ -336,7 +336,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity):
ATTR_RGBW_COLOR
]
if ATTR_EFFECT in kwargs:
if ATTR_EFFECT in kwargs and ATTR_COLOR_TEMP not in kwargs:
# Color effect change - used only in color mode, switch device mode to color
set_mode = "color"
if self.wrapper.model == "SHBLB-1":

View File

@@ -101,6 +101,7 @@ class Sun(Entity):
self.rising = self.phase = None
self._next_change = None
@callback
def update_location(_event):
location, elevation = get_astral_location(self.hass)
if location == self.location:

View File

@@ -3,7 +3,7 @@
"name": "Xiaomi Miio",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
"requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.10"],
"requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.11"],
"codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"],
"zeroconf": ["_miio._udp.local."],
"iot_class": "local_polling",

View File

@@ -3,7 +3,7 @@
"name": "Z-Wave JS",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
"requirements": ["zwave-js-server-python==0.35.1"],
"requirements": ["zwave-js-server-python==0.35.2"],
"codeowners": ["@home-assistant/z-wave"],
"dependencies": ["usb", "http", "websocket_api"],
"iot_class": "local_push",

View File

@@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 3
PATCH_VERSION: Final = "2"
PATCH_VERSION: Final = "4"
__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

@@ -6,6 +6,7 @@ import asyncio
from collections.abc import Awaitable, Iterable, Mapping, MutableMapping
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum, auto
import functools as ft
import logging
import math
@@ -207,6 +208,19 @@ class EntityCategory(StrEnum):
SYSTEM = "system"
class EntityPlatformState(Enum):
"""The platform state of an entity."""
# Not Added: Not yet added to a platform, polling updates are written to the state machine
NOT_ADDED = auto()
# Added: Added to a platform, polling updates are written to the state machine
ADDED = auto()
# Removed: Removed from a platform, polling updates are not written to the state machine
REMOVED = auto()
def convert_to_entity_category(
value: EntityCategory | str | None, raise_report: bool = True
) -> EntityCategory | None:
@@ -294,7 +308,7 @@ class Entity(ABC):
_context_set: datetime | None = None
# If entity is added to an entity platform
_added = False
_platform_state = EntityPlatformState.NOT_ADDED
# Entity Properties
_attr_assumed_state: bool = False
@@ -553,6 +567,10 @@ class Entity(ABC):
@callback
def _async_write_ha_state(self) -> None:
"""Write the state to the state machine."""
if self._platform_state == EntityPlatformState.REMOVED:
# Polling returned after the entity has already been removed
return
if self.registry_entry and self.registry_entry.disabled_by:
if not self._disabled_reported:
self._disabled_reported = True
@@ -758,7 +776,7 @@ class Entity(ABC):
parallel_updates: asyncio.Semaphore | None,
) -> None:
"""Start adding an entity to a platform."""
if self._added:
if self._platform_state == EntityPlatformState.ADDED:
raise HomeAssistantError(
f"Entity {self.entity_id} cannot be added a second time to an entity platform"
)
@@ -766,7 +784,7 @@ class Entity(ABC):
self.hass = hass
self.platform = platform
self.parallel_updates = parallel_updates
self._added = True
self._platform_state = EntityPlatformState.ADDED
@callback
def add_to_platform_abort(self) -> None:
@@ -774,7 +792,7 @@ class Entity(ABC):
self.hass = None # type: ignore[assignment]
self.platform = None
self.parallel_updates = None
self._added = False
self._platform_state = EntityPlatformState.NOT_ADDED
async def add_to_platform_finish(self) -> None:
"""Finish adding an entity to a platform."""
@@ -792,12 +810,12 @@ class Entity(ABC):
If the entity doesn't have a non disabled entry in the entity registry,
or if force_remove=True, its state will be removed.
"""
if self.platform and not self._added:
if self.platform and self._platform_state != EntityPlatformState.ADDED:
raise HomeAssistantError(
f"Entity {self.entity_id} async_remove called twice"
)
self._added = False
self._platform_state = EntityPlatformState.REMOVED
if self._on_remove is not None:
while self._on_remove:

View File

@@ -14,7 +14,7 @@ certifi>=2021.5.30
ciso8601==2.2.0
cryptography==35.0.0
hass-nabucasa==0.54.0
home-assistant-frontend==20220301.0
home-assistant-frontend==20220301.1
httpx==0.21.3
ifaddr==0.1.7
jinja2==3.0.3

View File

@@ -843,7 +843,7 @@ hole==0.7.0
holidays==0.13
# homeassistant.components.frontend
home-assistant-frontend==20220301.0
home-assistant-frontend==20220301.1
# homeassistant.components.zwave
# homeassistant-pyozw==0.1.10
@@ -1589,7 +1589,7 @@ pyialarm==1.9.0
pyicloud==1.0.0
# homeassistant.components.insteon
pyinsteon==1.0.16
pyinsteon==1.0.13
# homeassistant.components.intesishome
pyintesishome==1.7.6
@@ -1670,7 +1670,7 @@ pymata-express==1.19
pymazda==0.3.2
# homeassistant.components.mediaroom
pymediaroom==0.6.4.1
pymediaroom==0.6.5.4
# homeassistant.components.melcloud
pymelcloud==2.5.6
@@ -1813,7 +1813,7 @@ pyrituals==0.0.6
pyruckus==0.12
# homeassistant.components.sabnzbd
pysabnzbd==1.1.0
pysabnzbd==1.1.1
# homeassistant.components.saj
pysaj==0.0.16
@@ -1952,7 +1952,7 @@ python-kasa==0.4.1
# python-lirc==1.2.3
# homeassistant.components.xiaomi_miio
python-miio==0.5.10
python-miio==0.5.11
# homeassistant.components.mpd
python-mpd2==3.0.4
@@ -2079,7 +2079,7 @@ quantum-gateway==0.0.6
rachiopy==1.0.3
# homeassistant.components.radio_browser
radios==0.1.0
radios==0.1.1
# homeassistant.components.radiotherm
radiotherm==2.1.0
@@ -2566,7 +2566,7 @@ zigpy==0.43.0
zm-py==0.5.2
# homeassistant.components.zwave_js
zwave-js-server-python==0.35.1
zwave-js-server-python==0.35.2
# homeassistant.components.zwave_me
zwave_me_ws==0.2.1

View File

@@ -553,7 +553,7 @@ hole==0.7.0
holidays==0.13
# homeassistant.components.frontend
home-assistant-frontend==20220301.0
home-assistant-frontend==20220301.1
# homeassistant.components.zwave
# homeassistant-pyozw==0.1.10
@@ -1000,7 +1000,7 @@ pyialarm==1.9.0
pyicloud==1.0.0
# homeassistant.components.insteon
pyinsteon==1.0.16
pyinsteon==1.0.13
# homeassistant.components.ipma
pyipma==2.0.5
@@ -1219,7 +1219,7 @@ python-juicenet==1.0.2
python-kasa==0.4.1
# homeassistant.components.xiaomi_miio
python-miio==0.5.10
python-miio==0.5.11
# homeassistant.components.nest
python-nest==4.2.0
@@ -1295,7 +1295,7 @@ pyzerproc==0.4.8
rachiopy==1.0.3
# homeassistant.components.radio_browser
radios==0.1.0
radios==0.1.1
# homeassistant.components.rainmachine
regenmaschine==2022.01.0
@@ -1588,7 +1588,7 @@ zigpy-znp==0.7.0
zigpy==0.43.0
# homeassistant.components.zwave_js
zwave-js-server-python==0.35.1
zwave-js-server-python==0.35.2
# homeassistant.components.zwave_me
zwave_me_ws==0.2.1

View File

@@ -1,6 +1,6 @@
[metadata]
name = homeassistant
version = 2022.3.2
version = 2022.3.4
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0

View File

@@ -15,6 +15,8 @@ from homeassistant.components.climate.const import (
ATTR_HVAC_MODES,
ATTR_MAX_TEMP,
ATTR_MIN_TEMP,
ATTR_PRESET_MODE,
ATTR_PRESET_MODES,
ATTR_SWING_MODE,
ATTR_SWING_MODES,
ATTR_TARGET_TEMP_HIGH,
@@ -74,6 +76,7 @@ from homeassistant.components.homekit.type_thermostats import (
from homeassistant.components.water_heater import DOMAIN as DOMAIN_WATER_HEATER
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
CONF_TEMPERATURE_UNIT,
@@ -2349,3 +2352,127 @@ async def test_thermostat_with_fan_modes_with_off(hass, hk_driver, events):
assert len(call_set_fan_mode) == 2
assert call_set_fan_mode[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == FAN_OFF
async def test_thermostat_with_fan_modes_set_to_none(hass, hk_driver, events):
"""Test a thermostate with fan modes set to None."""
entity_id = "climate.test"
hass.states.async_set(
entity_id,
HVAC_MODE_OFF,
{
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE
| SUPPORT_TARGET_TEMPERATURE_RANGE
| SUPPORT_FAN_MODE
| SUPPORT_SWING_MODE,
ATTR_FAN_MODES: None,
ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL],
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
ATTR_FAN_MODE: FAN_AUTO,
ATTR_SWING_MODE: SWING_BOTH,
ATTR_HVAC_MODES: [
HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_COOL,
HVAC_MODE_OFF,
HVAC_MODE_AUTO,
],
},
)
await hass.async_block_till_done()
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
await acc.run()
await hass.async_block_till_done()
assert acc.char_cooling_thresh_temp.value == 23.0
assert acc.char_heating_thresh_temp.value == 19.0
assert acc.ordered_fan_speeds == []
assert CHAR_ROTATION_SPEED not in acc.fan_chars
assert CHAR_TARGET_FAN_STATE not in acc.fan_chars
assert CHAR_SWING_MODE in acc.fan_chars
assert CHAR_CURRENT_FAN_STATE in acc.fan_chars
async def test_thermostat_with_fan_modes_set_to_none_not_supported(
hass, hk_driver, events
):
"""Test a thermostate with fan modes set to None and supported feature missing."""
entity_id = "climate.test"
hass.states.async_set(
entity_id,
HVAC_MODE_OFF,
{
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE
| SUPPORT_TARGET_TEMPERATURE_RANGE
| SUPPORT_SWING_MODE,
ATTR_FAN_MODES: None,
ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL],
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
ATTR_FAN_MODE: FAN_AUTO,
ATTR_SWING_MODE: SWING_BOTH,
ATTR_HVAC_MODES: [
HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_COOL,
HVAC_MODE_OFF,
HVAC_MODE_AUTO,
],
},
)
await hass.async_block_till_done()
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
await acc.run()
await hass.async_block_till_done()
assert acc.char_cooling_thresh_temp.value == 23.0
assert acc.char_heating_thresh_temp.value == 19.0
assert acc.ordered_fan_speeds == []
assert CHAR_ROTATION_SPEED not in acc.fan_chars
assert CHAR_TARGET_FAN_STATE not in acc.fan_chars
assert CHAR_SWING_MODE in acc.fan_chars
assert CHAR_CURRENT_FAN_STATE in acc.fan_chars
async def test_thermostat_with_supported_features_target_temp_but_fan_mode_set(
hass, hk_driver, events
):
"""Test a thermostate with fan mode and supported feature missing."""
entity_id = "climate.test"
hass.states.async_set(
entity_id,
HVAC_MODE_OFF,
{
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE,
ATTR_MIN_TEMP: 44.6,
ATTR_MAX_TEMP: 95,
ATTR_PRESET_MODES: ["home", "away"],
ATTR_TEMPERATURE: 67,
ATTR_TARGET_TEMP_HIGH: None,
ATTR_TARGET_TEMP_LOW: None,
ATTR_FAN_MODE: FAN_AUTO,
ATTR_FAN_MODES: None,
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
ATTR_FAN_MODE: FAN_AUTO,
ATTR_PRESET_MODE: "home",
ATTR_FRIENDLY_NAME: "Rec Room",
ATTR_HVAC_MODES: [
HVAC_MODE_OFF,
HVAC_MODE_HEAT,
],
},
)
await hass.async_block_till_done()
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
await acc.run()
await hass.async_block_till_done()
assert acc.ordered_fan_speeds == []
assert not acc.fan_chars

View File

@@ -333,6 +333,43 @@ async def test_set_fan_mode(hass, mqtt_mock):
assert state.attributes.get("fan_mode") == "high"
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
@pytest.mark.parametrize(
"send_if_off,assert_async_publish",
[
({}, [call("fan-mode-topic", "low", 0, False)]),
({"send_if_off": True}, [call("fan-mode-topic", "low", 0, False)]),
({"send_if_off": False}, []),
],
)
async def test_set_fan_mode_send_if_off(
hass, mqtt_mock, send_if_off, assert_async_publish
):
"""Test setting of fan mode if the hvac is off."""
config = copy.deepcopy(DEFAULT_CONFIG)
config[CLIMATE_DOMAIN].update(send_if_off)
assert await async_setup_component(hass, CLIMATE_DOMAIN, config)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_CLIMATE) is not None
# Turn on HVAC
await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE)
mqtt_mock.async_publish.reset_mock()
# Updates for fan_mode should be sent when the device is turned on
await common.async_set_fan_mode(hass, "high", ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_called_once_with("fan-mode-topic", "high", 0, False)
# Turn off HVAC
await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE)
state = hass.states.get(ENTITY_CLIMATE)
assert state.state == "off"
# Updates for fan_mode should be sent if SEND_IF_OFF is not set or is True
mqtt_mock.async_publish.reset_mock()
await common.async_set_fan_mode(hass, "low", ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_has_calls(assert_async_publish)
async def test_set_swing_mode_bad_attr(hass, mqtt_mock, caplog):
"""Test setting swing mode without required attribute."""
assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG)
@@ -385,6 +422,43 @@ async def test_set_swing(hass, mqtt_mock):
assert state.attributes.get("swing_mode") == "on"
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
@pytest.mark.parametrize(
"send_if_off,assert_async_publish",
[
({}, [call("swing-mode-topic", "on", 0, False)]),
({"send_if_off": True}, [call("swing-mode-topic", "on", 0, False)]),
({"send_if_off": False}, []),
],
)
async def test_set_swing_mode_send_if_off(
hass, mqtt_mock, send_if_off, assert_async_publish
):
"""Test setting of swing mode if the hvac is off."""
config = copy.deepcopy(DEFAULT_CONFIG)
config[CLIMATE_DOMAIN].update(send_if_off)
assert await async_setup_component(hass, CLIMATE_DOMAIN, config)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_CLIMATE) is not None
# Turn on HVAC
await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE)
mqtt_mock.async_publish.reset_mock()
# Updates for swing_mode should be sent when the device is turned on
await common.async_set_swing_mode(hass, "off", ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_called_once_with("swing-mode-topic", "off", 0, False)
# Turn off HVAC
await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE)
state = hass.states.get(ENTITY_CLIMATE)
assert state.state == "off"
# Updates for swing_mode should be sent if SEND_IF_OFF is not set or is True
mqtt_mock.async_publish.reset_mock()
await common.async_set_swing_mode(hass, "on", ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_has_calls(assert_async_publish)
async def test_set_target_temperature(hass, mqtt_mock):
"""Test setting the target temperature."""
assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG)
@@ -421,6 +495,45 @@ async def test_set_target_temperature(hass, mqtt_mock):
mqtt_mock.async_publish.reset_mock()
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
@pytest.mark.parametrize(
"send_if_off,assert_async_publish",
[
({}, [call("temperature-topic", "21.0", 0, False)]),
({"send_if_off": True}, [call("temperature-topic", "21.0", 0, False)]),
({"send_if_off": False}, []),
],
)
async def test_set_target_temperature_send_if_off(
hass, mqtt_mock, send_if_off, assert_async_publish
):
"""Test setting of target temperature if the hvac is off."""
config = copy.deepcopy(DEFAULT_CONFIG)
config[CLIMATE_DOMAIN].update(send_if_off)
assert await async_setup_component(hass, CLIMATE_DOMAIN, config)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_CLIMATE) is not None
# Turn on HVAC
await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE)
mqtt_mock.async_publish.reset_mock()
# Updates for target temperature should be sent when the device is turned on
await common.async_set_temperature(hass, 16.0, ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_called_once_with(
"temperature-topic", "16.0", 0, False
)
# Turn off HVAC
await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE)
state = hass.states.get(ENTITY_CLIMATE)
assert state.state == "off"
# Updates for target temperature sent should be if SEND_IF_OFF is not set or is True
mqtt_mock.async_publish.reset_mock()
await common.async_set_temperature(hass, 21.0, ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_has_calls(assert_async_publish)
async def test_set_target_temperature_pessimistic(hass, mqtt_mock):
"""Test setting the target temperature."""
config = copy.deepcopy(DEFAULT_CONFIG)

View File

@@ -9,6 +9,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
ENTITY_MATCH_ALL,
SERVICE_TURN_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import State
@@ -177,6 +178,34 @@ async def test_restore_state(hass, entities, enable_custom_integrations):
assert hass.states.get("scene.test").state == "2021-01-01T23:59:59+00:00"
async def test_restore_state_does_not_restore_unavailable(
hass, entities, enable_custom_integrations
):
"""Test we restore state integration but ignore unavailable."""
mock_restore_cache(hass, (State("scene.test", STATE_UNAVAILABLE),))
light_1, light_2 = await setup_lights(hass, entities)
assert await async_setup_component(
hass,
scene.DOMAIN,
{
"scene": [
{
"name": "test",
"entities": {
light_1.entity_id: "on",
light_2.entity_id: "on",
},
}
]
},
)
await hass.async_block_till_done()
assert hass.states.get("scene.test").state == STATE_UNKNOWN
async def activate(hass, entity_id=ENTITY_MATCH_ALL):
"""Activate a scene."""
data = {}

View File

@@ -545,6 +545,22 @@ async def test_async_remove_runs_callbacks(hass):
assert len(result) == 1
async def test_async_remove_ignores_in_flight_polling(hass):
"""Test in flight polling is ignored after removing."""
result = []
ent = entity.Entity()
ent.hass = hass
ent.entity_id = "test.test"
ent.async_on_remove(lambda: result.append(1))
ent.async_write_ha_state()
assert hass.states.get("test.test").state == STATE_UNKNOWN
await ent.async_remove()
assert len(result) == 1
assert hass.states.get("test.test") is None
ent.async_write_ha_state()
async def test_set_context(hass):
"""Test setting context."""
context = Context()

View File

@@ -390,6 +390,30 @@ async def test_async_remove_with_platform(hass):
assert len(hass.states.async_entity_ids()) == 0
async def test_async_remove_with_platform_update_finishes(hass):
"""Remove an entity when an update finishes after its been removed."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
entity1 = MockEntity(name="test_1")
async def _delayed_update(*args, **kwargs):
await asyncio.sleep(0.01)
entity1.async_update = _delayed_update
# Add, remove, add, remove and make sure no updates
# cause the entity to reappear after removal
for i in range(2):
await component.async_add_entities([entity1])
assert len(hass.states.async_entity_ids()) == 1
entity1.async_write_ha_state()
assert hass.states.get(entity1.entity_id) is not None
task = asyncio.create_task(entity1.async_update_ha_state(True))
await entity1.async_remove()
assert len(hass.states.async_entity_ids()) == 0
await task
assert len(hass.states.async_entity_ids()) == 0
async def test_not_adding_duplicate_entities_with_unique_id(hass, caplog):
"""Test for not adding duplicate entities."""
caplog.set_level(logging.ERROR)