mirror of
https://github.com/home-assistant/core.git
synced 2026-01-03 14:25:28 +01:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1153720c0 | ||
|
|
27d275e6f7 | ||
|
|
1191c095f8 | ||
|
|
b86d115764 | ||
|
|
479a230da7 | ||
|
|
7aecd69e3b | ||
|
|
69587dd50a | ||
|
|
6d8bd6af4d | ||
|
|
31b19e09b5 | ||
|
|
a42ba9e10a | ||
|
|
a285478cf8 | ||
|
|
c95d55e6d6 | ||
|
|
c0860931b3 | ||
|
|
898af3e04c | ||
|
|
3de341099f | ||
|
|
7fb76c68bb | ||
|
|
7de5e070fb | ||
|
|
1bfb01e0d1 | ||
|
|
ca664ab5a5 | ||
|
|
5a39e63d25 | ||
|
|
c608cafebd | ||
|
|
07e70c81b0 | ||
|
|
cad397d6a7 | ||
|
|
c22af2c82a | ||
|
|
f5b6d93706 | ||
|
|
28b3edf6b2 | ||
|
|
737c502e94 | ||
|
|
a1abcbc7eb | ||
|
|
b09ab2dafb | ||
|
|
4e6fc3615b | ||
|
|
580c998552 | ||
|
|
97ba17d1ec | ||
|
|
8d7cdceb75 | ||
|
|
dfa1c3abb3 | ||
|
|
c807c57a9b | ||
|
|
f4ec7e0902 | ||
|
|
814c96834e | ||
|
|
87492e6b3e | ||
|
|
4aaafb0a99 |
@@ -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"}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)},
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user