Compare commits

..

39 Commits

Author SHA1 Message Date
Paulus Schoutsen
b1153720c0 Merge pull request #68159 from home-assistant/rc 2022-03-15 00:11:48 -07:00
Franck Nijhof
27d275e6f7 Fix Efergy tests (#68086) 2022-03-14 23:22:59 -07:00
Paulus Schoutsen
1191c095f8 Bumped version to 2022.3.5 2022-03-14 22:46:08 -07:00
Jan Bouwhuis
b86d115764 Fix MQTT false positive deprecation warnings (#68117) 2022-03-14 22:45:53 -07:00
Frank
479a230da7 Update home_connect to 0.7.0 (#68089) 2022-03-14 22:45:52 -07:00
J. Nick Koston
7aecd69e3b Bump pyisy to 3.0.5 (#68069)
* Bump pyisy to 3.0.4

- Fixes #66003

- Changelog: https://github.com/automicus/PyISY/compare/v3.0.1...v3.0.4

* again
2022-03-14 22:45:52 -07:00
Sean Vig
69587dd50a Bump amcrest version to 1.9.7 (#68055) 2022-03-14 22:45:51 -07:00
Christopher Thornton
6d8bd6af4d Default somfy_mylink shade's _attr_is_closed to None (#68053)
The base `Cover` entity requires an explicit value for
`_attr_is_closed`.

Since the `SomfyShade` is an assumed state, we don't know
by default whether the shade is open or not, so we need to
explicitly return `None` for `_attr_is_closed`
2022-03-14 22:45:50 -07:00
Shay Levy
31b19e09b5 Fix Shelly EM/3EM invalid energy value after reboot (#68052) 2022-03-14 22:45:49 -07:00
Sean Vig
a42ba9e10a Fix turning amcrest camera on and off (#68050) 2022-03-14 22:45:49 -07:00
J. Nick Koston
a285478cf8 Filter IPv6 addresses from doorbird discovery (#68031) 2022-03-14 22:45:48 -07:00
Zack Barett
c95d55e6d6 20220301.2 (#68130) 2022-03-14 10:07:58 -07:00
epenet
c0860931b3 Fix WebSocketTimeoutException in SamsungTV (#68114)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2022-03-14 09:32:14 -07:00
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
46 changed files with 561 additions and 102 deletions

View File

@@ -52,6 +52,7 @@ from .const import (
DATA_AMCREST,
DEVICES,
DOMAIN,
RESOLUTION_LIST,
SERVICE_EVENT,
SERVICE_UPDATE,
)
@@ -76,8 +77,6 @@ RECHECK_INTERVAL = timedelta(minutes=1)
NOTIFICATION_ID = "amcrest_notification"
NOTIFICATION_TITLE = "Amcrest Camera Setup"
RESOLUTION_LIST = {"high": 0, "low": 1}
SCAN_INTERVAL = timedelta(seconds=10)
AUTHENTICATION_LIST = {"basic": "basic"}

View File

@@ -35,6 +35,7 @@ from .const import (
DATA_AMCREST,
DEVICES,
DOMAIN,
RESOLUTION_TO_STREAM,
SERVICE_UPDATE,
SNAPSHOT_TIMEOUT,
)
@@ -533,13 +534,14 @@ class AmcrestCam(Camera):
return
async def _async_get_video(self) -> bool:
stream = {0: "Main", 1: "Extra"}
return await self._api.async_is_video_enabled(
channel=0, stream=stream[self._resolution]
channel=0, stream=RESOLUTION_TO_STREAM[self._resolution]
)
async def _async_set_video(self, enable: bool) -> None:
await self._api.async_set_video_enabled(enable, channel=0)
await self._api.async_set_video_enabled(
enable, channel=0, stream=RESOLUTION_TO_STREAM[self._resolution]
)
async def _async_enable_video(self, enable: bool) -> None:
"""Enable or disable camera video stream."""
@@ -548,7 +550,7 @@ class AmcrestCam(Camera):
# recording on if video stream is being turned off.
if self.is_recording and not enable:
await self._async_enable_recording(False)
await self._async_change_setting(enable, "video", "is_streaming")
await self._async_change_setting(enable, "video", "_attr_is_streaming")
if self._control_light:
await self._async_change_light()
@@ -585,10 +587,14 @@ class AmcrestCam(Camera):
)
async def _async_get_audio(self) -> bool:
return await self._api.async_audio_enabled
return await self._api.async_is_audio_enabled(
channel=0, stream=RESOLUTION_TO_STREAM[self._resolution]
)
async def _async_set_audio(self, enable: bool) -> None:
await self._api.async_set_audio_enabled(enable)
await self._api.async_set_audio_enabled(
enable, channel=0, stream=RESOLUTION_TO_STREAM[self._resolution]
)
async def _async_enable_audio(self, enable: bool) -> None:
"""Enable or disable audio stream."""

View File

@@ -13,3 +13,6 @@ SNAPSHOT_TIMEOUT = 20
SERVICE_EVENT = "event"
SERVICE_UPDATE = "update"
RESOLUTION_LIST = {"high": 0, "low": 1}
RESOLUTION_TO_STREAM = {0: "Main", 1: "Extra"}

View File

@@ -2,7 +2,7 @@
"domain": "amcrest",
"name": "Amcrest",
"documentation": "https://www.home-assistant.io/integrations/amcrest",
"requirements": ["amcrest==1.9.4"],
"requirements": ["amcrest==1.9.7"],
"dependencies": ["ffmpeg"],
"codeowners": ["@flacjacket"],
"iot_class": "local_polling",

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

@@ -12,7 +12,7 @@ from homeassistant.components import zeroconf
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.util.network import is_link_local
from homeassistant.util.network import is_ipv4_address, is_link_local
from .const import CONF_EVENTS, DOMAIN, DOORBIRD_OUI
from .util import get_mac_address_from_doorstation_info
@@ -103,6 +103,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="not_doorbird_device")
if is_link_local(ip_address(host)):
return self.async_abort(reason="link_local_address")
if not is_ipv4_address(host):
return self.async_abort(reason="not_ipv4_address")
await self.async_set_unique_id(macaddress)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})

View File

@@ -3,7 +3,8 @@
"abort": {
"already_configured": "Device is already configured",
"link_local_address": "Link local addresses are not supported",
"not_doorbird_device": "This device is not a DoorBird"
"not_doorbird_device": "This device is not a DoorBird",
"not_ipv4_address": "Only IPv4 addresess are supported"
},
"error": {
"cannot_connect": "Failed to connect",

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.2"
],
"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

@@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/home_connect",
"dependencies": ["http"],
"codeowners": ["@DavidMStraub"],
"requirements": ["homeconnect==0.6.3"],
"requirements": ["homeconnect==0.7.0"],
"config_flow": true,
"iot_class": "cloud_push",
"loggers": ["homeconnect"]

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

@@ -2,7 +2,7 @@
"domain": "isy994",
"name": "Universal Devices ISY994",
"documentation": "https://www.home-assistant.io/integrations/isy994",
"requirements": ["pyisy==3.0.1"],
"requirements": ["pyisy==3.0.5"],
"codeowners": ["@bdraco", "@shbatm"],
"config_flow": true,
"ssdp": [

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()
@@ -130,6 +135,13 @@ DEFAULT_KEEPALIVE = 60
DEFAULT_PROTOCOL = PROTOCOL_311
DEFAULT_TLS_PROTOCOL = "auto"
DEFAULT_VALUES = {
CONF_PORT: DEFAULT_PORT,
CONF_WILL_MESSAGE: DEFAULT_WILL,
CONF_BIRTH_MESSAGE: DEFAULT_BIRTH,
CONF_DISCOVERY: DEFAULT_DISCOVERY,
}
ATTR_TOPIC_TEMPLATE = "topic_template"
ATTR_PAYLOAD_TEMPLATE = "payload_template"
@@ -185,7 +197,7 @@ CONFIG_SCHEMA_BASE = vol.Schema(
vol.Coerce(int), vol.Range(min=15)
),
vol.Optional(CONF_BROKER): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile),
@@ -202,9 +214,9 @@ CONFIG_SCHEMA_BASE = vol.Schema(
vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All(
cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])
),
vol.Optional(CONF_WILL_MESSAGE, default=DEFAULT_WILL): MQTT_WILL_BIRTH_SCHEMA,
vol.Optional(CONF_BIRTH_MESSAGE, default=DEFAULT_BIRTH): MQTT_WILL_BIRTH_SCHEMA,
vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean,
vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA,
vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA,
vol.Optional(CONF_DISCOVERY): cv.boolean,
# discovery_prefix must be a valid publish topic because if no
# state topic is specified, it will be created with the given prefix.
vol.Optional(
@@ -608,6 +620,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
def _merge_config(entry, conf):
"""Merge configuration.yaml config with config entry."""
# Base config on default values
conf = {**DEFAULT_VALUES, **conf}
return {**conf, **entry.data}
@@ -627,6 +641,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
override,
)
# Merge the configuration values from configuration.yaml
conf = _merge_config(entry, conf)
hass.data[DATA_MQTT] = MQTT(
@@ -759,23 +774,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 @@ from samsungctl import Remote
from samsungctl.exceptions import AccessDenied, ConnectionClosed, UnhandledResponse
from samsungtvws import SamsungTVWS
from samsungtvws.exceptions import ConnectionFailure, HttpApiError
from websocket import WebSocketException
from websocket import WebSocketException, WebSocketTimeoutException
from homeassistant.const import (
CONF_HOST,
@@ -318,8 +318,8 @@ class SamsungTVWSBridge(SamsungTVBridge):
def _get_app_list(self) -> dict[str, str] | None:
"""Get installed app list."""
if self._app_list is None:
if remote := self._get_remote():
if self._app_list is None and (remote := self._get_remote()):
with contextlib.suppress(WebSocketTimeoutException):
raw_app_list: list[dict[str, str]] = remote.app_list()
self._app_list = {
app["name"]: app["appId"]

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

@@ -174,6 +174,7 @@ SENSORS: Final = {
value=lambda value: round(value / 1000, 2),
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
available=lambda block: cast(int, block.energy) != -1,
),
("emeter", "energyReturned"): BlockSensorDescription(
key="emeter|energyReturned",
@@ -182,6 +183,7 @@ SENSORS: Final = {
value=lambda value: round(value / 1000, 2),
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
available=lambda block: cast(int, block.energyReturned) != -1,
),
("light", "energy"): BlockSensorDescription(
key="light|energy",

View File

@@ -79,6 +79,7 @@ class SomfyShade(RestoreEntity, CoverEntity):
self._attr_unique_id = target_id
self._attr_name = name
self._reverse = reverse
self._attr_is_closed = None
self._attr_device_class = device_class
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._target_id)},

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 = "5"
__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.2
httpx==0.21.3
ifaddr==0.1.7
jinja2==3.0.3

View File

@@ -311,7 +311,7 @@ amberelectric==1.0.3
ambiclimate==0.2.1
# homeassistant.components.amcrest
amcrest==1.9.4
amcrest==1.9.7
# homeassistant.components.androidtv
androidtv[async]==0.0.63
@@ -843,13 +843,13 @@ hole==0.7.0
holidays==0.13
# homeassistant.components.frontend
home-assistant-frontend==20220301.0
home-assistant-frontend==20220301.2
# homeassistant.components.zwave
# homeassistant-pyozw==0.1.10
# homeassistant.components.home_connect
homeconnect==0.6.3
homeconnect==0.7.0
# homeassistant.components.homematicip_cloud
homematicip==1.0.2
@@ -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
@@ -1610,7 +1610,7 @@ pyirishrail==0.0.2
pyiss==1.0.1
# homeassistant.components.isy994
pyisy==3.0.1
pyisy==3.0.5
# homeassistant.components.itach
pyitachip2ir==0.0.7
@@ -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,13 +553,13 @@ hole==0.7.0
holidays==0.13
# homeassistant.components.frontend
home-assistant-frontend==20220301.0
home-assistant-frontend==20220301.2
# homeassistant.components.zwave
# homeassistant-pyozw==0.1.10
# homeassistant.components.home_connect
homeconnect==0.6.3
homeconnect==0.7.0
# homeassistant.components.homematicip_cloud
homematicip==1.0.2
@@ -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
@@ -1015,7 +1015,7 @@ pyiqvia==2021.11.0
pyiss==1.0.1
# homeassistant.components.isy994
pyisy==3.0.1
pyisy==3.0.5
# homeassistant.components.kira
pykira==0.1.1
@@ -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.5
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0

View File

@@ -116,6 +116,54 @@ async def test_form_zeroconf_link_local_ignored(hass):
assert result["reason"] == "link_local_address"
async def test_form_zeroconf_ipv4_address(hass):
"""Test we abort and update the ip address from zeroconf with an ipv4 address."""
config_entry = MockConfigEntry(
domain=DOMAIN,
unique_id="1CCAE3AAAAAA",
data=VALID_CONFIG,
options={CONF_EVENTS: ["event1", "event2", "event3"]},
)
config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=zeroconf.ZeroconfServiceInfo(
host="4.4.4.4",
addresses=["4.4.4.4"],
hostname="mock_hostname",
name="Doorstation - abc123._axis-video._tcp.local.",
port=None,
properties={"macaddress": "1CCAE3AAAAAA"},
type="mock_type",
),
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
assert config_entry.data[CONF_HOST] == "4.4.4.4"
async def test_form_zeroconf_non_ipv4_ignored(hass):
"""Test we abort when we get a non ipv4 address via zeroconf."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=zeroconf.ZeroconfServiceInfo(
host="fd00::b27c:63bb:cc85:4ea0",
addresses=["fd00::b27c:63bb:cc85:4ea0"],
hostname="mock_hostname",
name="Doorstation - abc123._axis-video._tcp.local.",
port=None,
properties={"macaddress": "1CCAE3DOORBIRD"},
type="mock_type",
),
)
assert result["type"] == "abort"
assert result["reason"] == "not_ipv4_address"
async def test_form_zeroconf_correct_oui(hass):
"""Test we can setup from zeroconf with the correct OUI source."""
doorbirdapi = _get_mock_doorbirdapi_return_values(

View File

@@ -1,12 +1,11 @@
"""Tests for Efergy integration."""
from unittest.mock import AsyncMock, patch
from pyefergy import Efergy, exceptions
from pyefergy import exceptions
from homeassistant.components.efergy import DOMAIN
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, load_fixture
@@ -56,10 +55,6 @@ async def mock_responses(
):
"""Mock responses from Efergy."""
base_url = "https://engage.efergy.com/mobile_proxy/"
api = Efergy(
token, session=async_get_clientsession(hass), utc_offset="America/New_York"
)
assert api._utc_offset == 300
if error:
aioclient_mock.get(
f"{base_url}getInstant?token={token}",

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)