Merge pull request #67838 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen
2022-03-07 21:51:30 -08:00
committed by GitHub
21 changed files with 415 additions and 45 deletions

View File

@@ -1,13 +1,13 @@
"""Support for Elgato Lights.""" """Support for Elgato Lights."""
from typing import NamedTuple 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.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession 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 from .const import DOMAIN, LOGGER, SCAN_INTERVAL
@@ -31,12 +31,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
session=session, 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( coordinator: DataUpdateCoordinator[State] = DataUpdateCoordinator(
hass, hass,
LOGGER, LOGGER,
name=f"{DOMAIN}_{entry.data[CONF_HOST]}", name=f"{DOMAIN}_{entry.data[CONF_HOST]}",
update_interval=SCAN_INTERVAL, update_interval=SCAN_INTERVAL,
update_method=elgato.state, update_method=_async_update_data,
) )
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()

View File

@@ -392,6 +392,8 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
) )
self.mesh_role = MeshRoles.NONE self.mesh_role = MeshRoles.NONE
for mac, info in hosts.items(): 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): if self.manage_device_info(info, mac, consider_home):
new_device = True new_device = True
self.send_signal_device_update(new_device) self.send_signal_device_update(new_device)

View File

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

View File

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

View File

@@ -285,16 +285,15 @@ class Thermostat(HomeAccessory):
CHAR_CURRENT_HUMIDITY, value=50 CHAR_CURRENT_HUMIDITY, value=50
) )
fan_modes = self.fan_modes = { fan_modes = {}
fan_mode.lower(): fan_mode
for fan_mode in attributes.get(ATTR_FAN_MODES, [])
}
self.ordered_fan_speeds = [] self.ordered_fan_speeds = []
if (
features & SUPPORT_FAN_MODE if features & SUPPORT_FAN_MODE:
and fan_modes fan_modes = {
and PRE_DEFINED_FAN_MODES.intersection(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 = [ self.ordered_fan_speeds = [
speed for speed in ORDERED_FAN_SPEEDS if speed in fan_modes speed for speed in ORDERED_FAN_SPEEDS if speed in fan_modes
] ]

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

View File

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

View File

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

View File

@@ -317,4 +317,14 @@ class BlockSleepingClimate(
if self.device_block and self.block: if self.device_block and self.block:
_LOGGER.debug("Entity %s attached to blocks", self.name) _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() self.async_write_ha_state()

View File

@@ -3,7 +3,7 @@
"name": "Xiaomi Miio", "name": "Xiaomi Miio",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", "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"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"],
"zeroconf": ["_miio._udp.local."], "zeroconf": ["_miio._udp.local."],
"iot_class": "local_polling", "iot_class": "local_polling",

View File

@@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022 MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 3 MINOR_VERSION: Final = 3
PATCH_VERSION: Final = "2" PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) 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 collections.abc import Awaitable, Iterable, Mapping, MutableMapping
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from enum import Enum, auto
import functools as ft import functools as ft
import logging import logging
import math import math
@@ -207,6 +208,19 @@ class EntityCategory(StrEnum):
SYSTEM = "system" 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( def convert_to_entity_category(
value: EntityCategory | str | None, raise_report: bool = True value: EntityCategory | str | None, raise_report: bool = True
) -> EntityCategory | None: ) -> EntityCategory | None:
@@ -294,7 +308,7 @@ class Entity(ABC):
_context_set: datetime | None = None _context_set: datetime | None = None
# If entity is added to an entity platform # If entity is added to an entity platform
_added = False _platform_state = EntityPlatformState.NOT_ADDED
# Entity Properties # Entity Properties
_attr_assumed_state: bool = False _attr_assumed_state: bool = False
@@ -553,6 +567,10 @@ class Entity(ABC):
@callback @callback
def _async_write_ha_state(self) -> None: def _async_write_ha_state(self) -> None:
"""Write the state to the state machine.""" """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 self.registry_entry and self.registry_entry.disabled_by:
if not self._disabled_reported: if not self._disabled_reported:
self._disabled_reported = True self._disabled_reported = True
@@ -758,7 +776,7 @@ class Entity(ABC):
parallel_updates: asyncio.Semaphore | None, parallel_updates: asyncio.Semaphore | None,
) -> None: ) -> None:
"""Start adding an entity to a platform.""" """Start adding an entity to a platform."""
if self._added: if self._platform_state == EntityPlatformState.ADDED:
raise HomeAssistantError( raise HomeAssistantError(
f"Entity {self.entity_id} cannot be added a second time to an entity platform" 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.hass = hass
self.platform = platform self.platform = platform
self.parallel_updates = parallel_updates self.parallel_updates = parallel_updates
self._added = True self._platform_state = EntityPlatformState.ADDED
@callback @callback
def add_to_platform_abort(self) -> None: def add_to_platform_abort(self) -> None:
@@ -774,7 +792,7 @@ class Entity(ABC):
self.hass = None # type: ignore[assignment] self.hass = None # type: ignore[assignment]
self.platform = None self.platform = None
self.parallel_updates = None self.parallel_updates = None
self._added = False self._platform_state = EntityPlatformState.NOT_ADDED
async def add_to_platform_finish(self) -> None: async def add_to_platform_finish(self) -> None:
"""Finish adding an entity to a platform.""" """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, If the entity doesn't have a non disabled entry in the entity registry,
or if force_remove=True, its state will be removed. 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( raise HomeAssistantError(
f"Entity {self.entity_id} async_remove called twice" f"Entity {self.entity_id} async_remove called twice"
) )
self._added = False self._platform_state = EntityPlatformState.REMOVED
if self._on_remove is not None: if self._on_remove is not None:
while self._on_remove: while self._on_remove:

View File

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

View File

@@ -843,7 +843,7 @@ hole==0.7.0
holidays==0.13 holidays==0.13
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20220301.0 home-assistant-frontend==20220301.1
# homeassistant.components.zwave # homeassistant.components.zwave
# homeassistant-pyozw==0.1.10 # homeassistant-pyozw==0.1.10
@@ -1952,7 +1952,7 @@ python-kasa==0.4.1
# python-lirc==1.2.3 # python-lirc==1.2.3
# homeassistant.components.xiaomi_miio # homeassistant.components.xiaomi_miio
python-miio==0.5.10 python-miio==0.5.11
# homeassistant.components.mpd # homeassistant.components.mpd
python-mpd2==3.0.4 python-mpd2==3.0.4

View File

@@ -553,7 +553,7 @@ hole==0.7.0
holidays==0.13 holidays==0.13
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20220301.0 home-assistant-frontend==20220301.1
# homeassistant.components.zwave # homeassistant.components.zwave
# homeassistant-pyozw==0.1.10 # homeassistant-pyozw==0.1.10
@@ -1219,7 +1219,7 @@ python-juicenet==1.0.2
python-kasa==0.4.1 python-kasa==0.4.1
# homeassistant.components.xiaomi_miio # homeassistant.components.xiaomi_miio
python-miio==0.5.10 python-miio==0.5.11
# homeassistant.components.nest # homeassistant.components.nest
python-nest==4.2.0 python-nest==4.2.0

View File

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

View File

@@ -15,6 +15,8 @@ from homeassistant.components.climate.const import (
ATTR_HVAC_MODES, ATTR_HVAC_MODES,
ATTR_MAX_TEMP, ATTR_MAX_TEMP,
ATTR_MIN_TEMP, ATTR_MIN_TEMP,
ATTR_PRESET_MODE,
ATTR_PRESET_MODES,
ATTR_SWING_MODE, ATTR_SWING_MODE,
ATTR_SWING_MODES, ATTR_SWING_MODES,
ATTR_TARGET_TEMP_HIGH, 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.components.water_heater import DOMAIN as DOMAIN_WATER_HEATER
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
ATTR_SUPPORTED_FEATURES, ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
CONF_TEMPERATURE_UNIT, 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 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_ENTITY_ID] == entity_id
assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == FAN_OFF 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" 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): async def test_set_swing_mode_bad_attr(hass, mqtt_mock, caplog):
"""Test setting swing mode without required attribute.""" """Test setting swing mode without required attribute."""
assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) 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" 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): async def test_set_target_temperature(hass, mqtt_mock):
"""Test setting the target temperature.""" """Test setting the target temperature."""
assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) 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() 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): async def test_set_target_temperature_pessimistic(hass, mqtt_mock):
"""Test setting the target temperature.""" """Test setting the target temperature."""
config = copy.deepcopy(DEFAULT_CONFIG) config = copy.deepcopy(DEFAULT_CONFIG)

View File

@@ -9,6 +9,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ENTITY_MATCH_ALL, ENTITY_MATCH_ALL,
SERVICE_TURN_ON, SERVICE_TURN_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
) )
from homeassistant.core import State 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" 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): async def activate(hass, entity_id=ENTITY_MATCH_ALL):
"""Activate a scene.""" """Activate a scene."""
data = {} data = {}

View File

@@ -545,6 +545,22 @@ async def test_async_remove_runs_callbacks(hass):
assert len(result) == 1 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): async def test_set_context(hass):
"""Test setting context.""" """Test setting context."""
context = 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 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): async def test_not_adding_duplicate_entities_with_unique_id(hass, caplog):
"""Test for not adding duplicate entities.""" """Test for not adding duplicate entities."""
caplog.set_level(logging.ERROR) caplog.set_level(logging.ERROR)