forked from home-assistant/core
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49edaf2f68 | ||
|
|
2be9798fb8 | ||
|
|
3bf0a64e21 | ||
|
|
23e9aa6ad2 | ||
|
|
a8e1f57058 | ||
|
|
caee432901 | ||
|
|
df5c09e483 | ||
|
|
38eb007f63 | ||
|
|
7dd9bfa92f | ||
|
|
54b7f13a54 | ||
|
|
774f2b9b82 | ||
|
|
bc14385317 | ||
|
|
9352ed1286 | ||
|
|
f7fd781a27 | ||
|
|
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 | ||
|
|
2aecdd3d6d | ||
|
|
76336df91a | ||
|
|
88e0380aa2 | ||
|
|
10a2c97cab | ||
|
|
92c3c08a10 | ||
|
|
4f8b69d985 | ||
|
|
f5aaf44e50 | ||
|
|
f3c85b3459 | ||
|
|
d7348718e0 | ||
|
|
2a6d5ea7bd | ||
|
|
5ae83e3c40 | ||
|
|
5657a9e6bd | ||
|
|
b290e62170 | ||
|
|
679ddbd1be | ||
|
|
b54652a849 | ||
|
|
24013ad94c | ||
|
|
9849b86a84 | ||
|
|
8bbf55c85d | ||
|
|
0541c708da | ||
|
|
ba40d62081 | ||
|
|
73765a1f29 | ||
|
|
b5b945ab4d | ||
|
|
d361643500 | ||
|
|
eff7a12557 | ||
|
|
63f8e9ee08 | ||
|
|
ee0bdaa2de | ||
|
|
48d9e9a83c |
@@ -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",
|
||||
|
||||
@@ -204,13 +204,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
# Init ip webcam
|
||||
cam = PyDroidIPCam(
|
||||
hass.loop,
|
||||
websession,
|
||||
host,
|
||||
cam_config[CONF_PORT],
|
||||
username=username,
|
||||
password=password,
|
||||
timeout=cam_config[CONF_TIMEOUT],
|
||||
ssl=False,
|
||||
)
|
||||
|
||||
if switches is None:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "android_ip_webcam",
|
||||
"name": "Android IP Webcam",
|
||||
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
|
||||
"requirements": ["pydroid-ipcam==0.8"],
|
||||
"requirements": ["pydroid-ipcam==1.3.1"],
|
||||
"codeowners": [],
|
||||
"iot_class": "local_polling"
|
||||
}
|
||||
|
||||
@@ -149,9 +149,19 @@ def _async_register_mac(
|
||||
return
|
||||
|
||||
# Make sure entity has a config entry and was disabled by the
|
||||
# default disable logic in the integration.
|
||||
# default disable logic in the integration and new entities
|
||||
# are allowed to be added.
|
||||
if (
|
||||
entity_entry.config_entry_id is None
|
||||
or (
|
||||
(
|
||||
config_entry := hass.config_entries.async_get_entry(
|
||||
entity_entry.config_entry_id
|
||||
)
|
||||
)
|
||||
is not None
|
||||
and config_entry.pref_disable_new_entities
|
||||
)
|
||||
or entity_entry.disabled_by != er.RegistryEntryDisabler.INTEGRATION
|
||||
):
|
||||
return
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -279,9 +279,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
keypad.add_callback(_element_changed)
|
||||
|
||||
try:
|
||||
if not await async_wait_for_elk_to_sync(
|
||||
elk, LOGIN_TIMEOUT, SYNC_TIMEOUT, bool(conf[CONF_USERNAME])
|
||||
):
|
||||
if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, SYNC_TIMEOUT):
|
||||
return False
|
||||
except asyncio.TimeoutError as exc:
|
||||
raise ConfigEntryNotReady(f"Timed out connecting to {conf[CONF_HOST]}") from exc
|
||||
@@ -334,7 +332,6 @@ async def async_wait_for_elk_to_sync(
|
||||
elk: elkm1.Elk,
|
||||
login_timeout: int,
|
||||
sync_timeout: int,
|
||||
password_auth: bool,
|
||||
) -> bool:
|
||||
"""Wait until the elk has finished sync. Can fail login or timeout."""
|
||||
|
||||
@@ -354,18 +351,23 @@ async def async_wait_for_elk_to_sync(
|
||||
login_event.set()
|
||||
sync_event.set()
|
||||
|
||||
def first_response(*args, **kwargs):
|
||||
_LOGGER.debug("ElkM1 received first response (VN)")
|
||||
login_event.set()
|
||||
|
||||
def sync_complete():
|
||||
sync_event.set()
|
||||
|
||||
success = True
|
||||
elk.add_handler("login", login_status)
|
||||
# VN is the first command sent for panel, when we get
|
||||
# it back we now we are logged in either with or without a password
|
||||
elk.add_handler("VN", first_response)
|
||||
elk.add_handler("sync_complete", sync_complete)
|
||||
events = []
|
||||
if password_auth:
|
||||
events.append(("login", login_event, login_timeout))
|
||||
events.append(("sync_complete", sync_event, sync_timeout))
|
||||
|
||||
for name, event, timeout in events:
|
||||
for name, event, timeout in (
|
||||
("login", login_event, login_timeout),
|
||||
("sync_complete", sync_event, sync_timeout),
|
||||
):
|
||||
_LOGGER.debug("Waiting for %s event for %s seconds", name, timeout)
|
||||
try:
|
||||
async with async_timeout.timeout(timeout):
|
||||
|
||||
@@ -81,10 +81,11 @@ async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str
|
||||
)
|
||||
elk.connect()
|
||||
|
||||
if not await async_wait_for_elk_to_sync(
|
||||
elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT, bool(userid)
|
||||
):
|
||||
raise InvalidAuth
|
||||
try:
|
||||
if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT):
|
||||
raise InvalidAuth
|
||||
finally:
|
||||
elk.disconnect()
|
||||
|
||||
short_mac = _short_mac(mac) if mac else None
|
||||
if prefix and prefix != short_mac:
|
||||
@@ -227,7 +228,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
try:
|
||||
info = await validate_input(user_input, self.unique_id)
|
||||
except asyncio.TimeoutError:
|
||||
return {CONF_HOST: "cannot_connect"}, None
|
||||
return {"base": "cannot_connect"}, None
|
||||
except InvalidAuth:
|
||||
return {CONF_PASSWORD: "invalid_auth"}, None
|
||||
except Exception: # pylint: disable=broad-except
|
||||
@@ -287,9 +288,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if device := await async_discover_device(
|
||||
self.hass, user_input[CONF_ADDRESS]
|
||||
):
|
||||
await self.async_set_unique_id(dr.format_mac(device.mac_address))
|
||||
await self.async_set_unique_id(
|
||||
dr.format_mac(device.mac_address), raise_on_progress=False
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
user_input[CONF_ADDRESS] = f"{device.ip_address}:{device.port}"
|
||||
# Ignore the port from discovery since its always going to be
|
||||
# 2601 if secure is turned on even though they may want insecure
|
||||
user_input[CONF_ADDRESS] = device.ip_address
|
||||
errors, result = await self._async_create_or_error(user_input, False)
|
||||
if not errors:
|
||||
return result
|
||||
@@ -324,7 +329,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if is_ip_address(host) and (
|
||||
device := await async_discover_device(self.hass, host)
|
||||
):
|
||||
await self.async_set_unique_id(dr.format_mac(device.mac_address))
|
||||
await self.async_set_unique_id(
|
||||
dr.format_mac(device.mac_address), raise_on_progress=False
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return (await self._async_create_or_error(user_input, True))[1]
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.util.network import is_ipv4_address
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
@@ -86,6 +87,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initialized by zeroconf discovery."""
|
||||
if not is_ipv4_address(discovery_info.host):
|
||||
return self.async_abort(reason="not_ipv4_address")
|
||||
serial = discovery_info.properties["serialnum"]
|
||||
await self.async_set_unique_id(serial)
|
||||
self.ip_address = discovery_info.host
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured",
|
||||
"reauth_successful": "Re-authentication was successful"
|
||||
"reauth_successful": "Re-authentication was successful",
|
||||
"not_ipv4_address": "Only IPv4 addresess are supported"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
|
||||
@@ -33,6 +33,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except FRITZ_EXCEPTIONS as ex:
|
||||
raise ConfigEntryNotReady from ex
|
||||
|
||||
if (
|
||||
"X_AVM-DE_UPnP1" in avm_wrapper.connection.services
|
||||
and not (await avm_wrapper.async_get_upnp_configuration())["NewEnable"]
|
||||
):
|
||||
raise ConfigEntryAuthFailed("Missing UPnP configuration")
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = avm_wrapper
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""AVM FRITZ!Box connectivity sensor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
@@ -14,8 +15,8 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .common import AvmWrapper, FritzBoxBaseEntity
|
||||
from .const import DOMAIN, MeshRoles
|
||||
from .common import AvmWrapper, ConnectionInfo, FritzBoxBaseEntity
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -24,7 +25,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class FritzBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describes Fritz sensor entity."""
|
||||
|
||||
exclude_mesh_role: MeshRoles = MeshRoles.SLAVE
|
||||
is_suitable: Callable[[ConnectionInfo], bool] = lambda info: info.wan_enabled
|
||||
|
||||
|
||||
SENSOR_TYPES: tuple[FritzBinarySensorEntityDescription, ...] = (
|
||||
@@ -45,7 +46,7 @@ SENSOR_TYPES: tuple[FritzBinarySensorEntityDescription, ...] = (
|
||||
name="Firmware Update",
|
||||
device_class=BinarySensorDeviceClass.UPDATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
exclude_mesh_role=MeshRoles.NONE,
|
||||
is_suitable=lambda info: True,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -57,10 +58,12 @@ async def async_setup_entry(
|
||||
_LOGGER.debug("Setting up FRITZ!Box binary sensors")
|
||||
avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
connection_info = await avm_wrapper.async_get_connection_info()
|
||||
|
||||
entities = [
|
||||
FritzBoxBinarySensor(avm_wrapper, entry.title, description)
|
||||
for description in SENSOR_TYPES
|
||||
if (description.exclude_mesh_role != avm_wrapper.mesh_role)
|
||||
if description.is_suitable(connection_info)
|
||||
]
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
@@ -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)
|
||||
@@ -630,6 +632,11 @@ class AvmWrapper(FritzBoxTools):
|
||||
)
|
||||
return {}
|
||||
|
||||
async def async_get_upnp_configuration(self) -> dict[str, Any]:
|
||||
"""Call X_AVM-DE_UPnP service."""
|
||||
|
||||
return await self.hass.async_add_executor_job(self.get_upnp_configuration)
|
||||
|
||||
async def async_get_wan_link_properties(self) -> dict[str, Any]:
|
||||
"""Call WANCommonInterfaceConfig service."""
|
||||
|
||||
@@ -637,6 +644,22 @@ class AvmWrapper(FritzBoxTools):
|
||||
partial(self.get_wan_link_properties)
|
||||
)
|
||||
|
||||
async def async_get_connection_info(self) -> ConnectionInfo:
|
||||
"""Return ConnectionInfo data."""
|
||||
|
||||
link_properties = await self.async_get_wan_link_properties()
|
||||
connection_info = ConnectionInfo(
|
||||
connection=link_properties.get("NewWANAccessType", "").lower(),
|
||||
mesh_role=self.mesh_role,
|
||||
wan_enabled=self.device_is_router,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"ConnectionInfo for FritzBox %s: %s",
|
||||
self.host,
|
||||
connection_info,
|
||||
)
|
||||
return connection_info
|
||||
|
||||
async def async_get_port_mapping(self, con_type: str, index: int) -> dict[str, Any]:
|
||||
"""Call GetGenericPortMappingEntry action."""
|
||||
|
||||
@@ -698,6 +721,11 @@ class AvmWrapper(FritzBoxTools):
|
||||
partial(self.set_allow_wan_access, ip_address, turn_on)
|
||||
)
|
||||
|
||||
def get_upnp_configuration(self) -> dict[str, Any]:
|
||||
"""Call X_AVM-DE_UPnP service."""
|
||||
|
||||
return self._service_call_action("X_AVM-DE_UPnP", "1", "GetInfo")
|
||||
|
||||
def get_ontel_num_deflections(self) -> dict[str, Any]:
|
||||
"""Call GetNumberOfDeflections action from X_AVM-DE_OnTel service."""
|
||||
|
||||
@@ -960,3 +988,12 @@ class FritzBoxBaseEntity:
|
||||
name=self._device_name,
|
||||
sw_version=self._avm_wrapper.current_firmware,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConnectionInfo:
|
||||
"""Fritz sensor connection information class."""
|
||||
|
||||
connection: str
|
||||
mesh_role: MeshRoles
|
||||
wan_enabled: bool
|
||||
|
||||
@@ -29,6 +29,7 @@ from .const import (
|
||||
ERROR_AUTH_INVALID,
|
||||
ERROR_CANNOT_CONNECT,
|
||||
ERROR_UNKNOWN,
|
||||
ERROR_UPNP_NOT_CONFIGURED,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -79,6 +80,12 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
return ERROR_UNKNOWN
|
||||
|
||||
if (
|
||||
"X_AVM-DE_UPnP1" in self.avm_wrapper.connection.services
|
||||
and not (await self.avm_wrapper.async_get_upnp_configuration())["NewEnable"]
|
||||
):
|
||||
return ERROR_UPNP_NOT_CONFIGURED
|
||||
|
||||
return None
|
||||
|
||||
async def async_check_configured_entry(self) -> ConfigEntry | None:
|
||||
|
||||
@@ -46,6 +46,7 @@ DEFAULT_USERNAME = ""
|
||||
|
||||
ERROR_AUTH_INVALID = "invalid_auth"
|
||||
ERROR_CANNOT_CONNECT = "cannot_connect"
|
||||
ERROR_UPNP_NOT_CONFIGURED = "upnp_not_configured"
|
||||
ERROR_UNKNOWN = "unknown_error"
|
||||
|
||||
FRITZ_SERVICES = "fritz_services"
|
||||
@@ -56,6 +57,7 @@ SERVICE_SET_GUEST_WIFI_PW = "set_guest_wifi_password"
|
||||
|
||||
SWITCH_TYPE_DEFLECTION = "CallDeflection"
|
||||
SWITCH_TYPE_PORTFORWARD = "PortForward"
|
||||
SWITCH_TYPE_PROFILE = "Profile"
|
||||
SWITCH_TYPE_WIFINETWORK = "WiFiNetwork"
|
||||
|
||||
UPTIME_DEVIATION = 5
|
||||
|
||||
@@ -22,6 +22,9 @@ async def async_get_config_entry_diagnostics(
|
||||
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
|
||||
"device_info": {
|
||||
"model": avm_wrapper.model,
|
||||
"unique_id": avm_wrapper.unique_id.replace(
|
||||
avm_wrapper.unique_id[6:11], "XX:XX"
|
||||
),
|
||||
"current_firmware": avm_wrapper.current_firmware,
|
||||
"latest_firmware": avm_wrapper.latest_firmware,
|
||||
"update_available": avm_wrapper.update_available,
|
||||
|
||||
@@ -28,8 +28,8 @@ from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .common import AvmWrapper, FritzBoxBaseEntity
|
||||
from .const import DOMAIN, DSL_CONNECTION, UPTIME_DEVIATION, MeshRoles
|
||||
from .common import AvmWrapper, ConnectionInfo, FritzBoxBaseEntity
|
||||
from .const import DOMAIN, DSL_CONNECTION, UPTIME_DEVIATION
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -134,15 +134,6 @@ def _retrieve_link_attenuation_received_state(
|
||||
return status.attenuation[1] / 10 # type: ignore[no-any-return]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConnectionInfo:
|
||||
"""Fritz sensor connection information class."""
|
||||
|
||||
connection: str
|
||||
mesh_role: MeshRoles
|
||||
wan_enabled: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class FritzRequireKeysMixin:
|
||||
"""Fritz sensor data class."""
|
||||
@@ -283,18 +274,7 @@ async def async_setup_entry(
|
||||
_LOGGER.debug("Setting up FRITZ!Box sensors")
|
||||
avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
link_properties = await avm_wrapper.async_get_wan_link_properties()
|
||||
connection_info = ConnectionInfo(
|
||||
connection=link_properties.get("NewWANAccessType", "").lower(),
|
||||
mesh_role=avm_wrapper.mesh_role,
|
||||
wan_enabled=avm_wrapper.device_is_router,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"ConnectionInfo for FritzBox %s: %s",
|
||||
avm_wrapper.host,
|
||||
connection_info,
|
||||
)
|
||||
connection_info = await avm_wrapper.async_get_connection_info()
|
||||
|
||||
entities = [
|
||||
FritzBoxSensor(avm_wrapper, entry.title, description)
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"upnp_not_configured": "Missing UPnP settings on device.",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
|
||||
@@ -30,6 +30,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
SWITCH_TYPE_DEFLECTION,
|
||||
SWITCH_TYPE_PORTFORWARD,
|
||||
SWITCH_TYPE_PROFILE,
|
||||
SWITCH_TYPE_WIFINETWORK,
|
||||
WIFI_STANDARD,
|
||||
MeshRoles,
|
||||
@@ -185,6 +186,7 @@ def profile_entities_list(
|
||||
data_fritz: FritzData,
|
||||
) -> list[FritzBoxProfileSwitch]:
|
||||
"""Add new tracker entities from the AVM device."""
|
||||
_LOGGER.debug("Setting up %s switches", SWITCH_TYPE_PROFILE)
|
||||
|
||||
new_profiles: list[FritzBoxProfileSwitch] = []
|
||||
|
||||
@@ -198,11 +200,15 @@ def profile_entities_list(
|
||||
if device_filter_out_from_trackers(
|
||||
mac, device, data_fritz.profile_switches.values()
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Skipping profile switch creation for device %s", device.hostname
|
||||
)
|
||||
continue
|
||||
|
||||
new_profiles.append(FritzBoxProfileSwitch(avm_wrapper, device))
|
||||
data_fritz.profile_switches[avm_wrapper.unique_id].add(mac)
|
||||
|
||||
_LOGGER.debug("Creating %s profile switches", len(new_profiles))
|
||||
return new_profiles
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"already_configured": "Device is already configured",
|
||||
"already_in_progress": "Configuration flow is already in progress",
|
||||
"cannot_connect": "Failed to connect",
|
||||
"invalid_auth": "Invalid authentication"
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"upnp_not_configured": "Missing UPnP settings on device."
|
||||
},
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
@@ -51,4 +52,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
@@ -59,11 +59,12 @@ SERVICE_SET = "set"
|
||||
SERVICE_REMOVE = "remove"
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.LIGHT,
|
||||
Platform.COVER,
|
||||
Platform.NOTIFY,
|
||||
Platform.FAN,
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.COVER,
|
||||
Platform.FAN,
|
||||
Platform.LIGHT,
|
||||
Platform.MEDIA_PLAYER,
|
||||
Platform.NOTIFY,
|
||||
]
|
||||
|
||||
REG_KEY = f"{DOMAIN}_registry"
|
||||
|
||||
@@ -221,12 +221,9 @@ class GrowattData:
|
||||
# Create datetime from the latest entry
|
||||
date_now = dt.now().date()
|
||||
last_updated_time = dt.parse_time(str(sorted_keys[-1]))
|
||||
combined_timestamp = datetime.datetime.combine(
|
||||
date_now, last_updated_time
|
||||
mix_detail["lastdataupdate"] = datetime.datetime.combine(
|
||||
date_now, last_updated_time, dt.DEFAULT_TIME_ZONE
|
||||
)
|
||||
# Convert datetime to UTC
|
||||
combined_timestamp_utc = dt.as_utc(combined_timestamp)
|
||||
mix_detail["lastdataupdate"] = combined_timestamp_utc.isoformat()
|
||||
|
||||
# Dashboard data is largely inaccurate for mix system but it is the only call with the ability to return the combined
|
||||
# imported from grid value that is the combination of charging AND load consumption
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -274,7 +274,7 @@ class HomeAccessory(Accessory):
|
||||
if self.config.get(ATTR_SW_VERSION) is not None:
|
||||
sw_version = format_version(self.config[ATTR_SW_VERSION])
|
||||
if sw_version is None:
|
||||
sw_version = __version__
|
||||
sw_version = format_version(__version__)
|
||||
hw_version = None
|
||||
if self.config.get(ATTR_HW_VERSION) is not None:
|
||||
hw_version = format_version(self.config[ATTR_HW_VERSION])
|
||||
@@ -289,7 +289,9 @@ class HomeAccessory(Accessory):
|
||||
serv_info = self.get_service(SERV_ACCESSORY_INFO)
|
||||
char = self.driver.loader.get_char(CHAR_HARDWARE_REVISION)
|
||||
serv_info.add_characteristic(char)
|
||||
serv_info.configure_char(CHAR_HARDWARE_REVISION, value=hw_version)
|
||||
serv_info.configure_char(
|
||||
CHAR_HARDWARE_REVISION, value=hw_version[:MAX_VERSION_LENGTH]
|
||||
)
|
||||
self.iid_manager.assign(char)
|
||||
char.broker = self
|
||||
|
||||
@@ -532,7 +534,7 @@ class HomeBridge(Bridge):
|
||||
"""Initialize a Bridge object."""
|
||||
super().__init__(driver, name)
|
||||
self.set_info_service(
|
||||
firmware_revision=__version__,
|
||||
firmware_revision=format_version(__version__),
|
||||
manufacturer=MANUFACTURER,
|
||||
model=BRIDGE_MODEL,
|
||||
serial_number=BRIDGE_SERIAL_NUMBER,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -100,6 +100,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
NUMBERS_ONLY_RE = re.compile(r"[^\d.]+")
|
||||
VERSION_RE = re.compile(r"([0-9]+)(\.[0-9]+)?(\.[0-9]+)?")
|
||||
MAX_VERSION_PART = 2**32 - 1
|
||||
|
||||
|
||||
MAX_PORT = 65535
|
||||
@@ -363,7 +364,15 @@ def convert_to_float(state):
|
||||
return None
|
||||
|
||||
|
||||
def cleanup_name_for_homekit(name: str | None) -> str | None:
|
||||
def coerce_int(state: str) -> int:
|
||||
"""Return int."""
|
||||
try:
|
||||
return int(state)
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
|
||||
|
||||
def cleanup_name_for_homekit(name: str | None) -> str:
|
||||
"""Ensure the name of the device will not crash homekit."""
|
||||
#
|
||||
# This is not a security measure.
|
||||
@@ -371,7 +380,7 @@ def cleanup_name_for_homekit(name: str | None) -> str | None:
|
||||
# UNICODE_EMOJI is also not allowed but that
|
||||
# likely isn't a problem
|
||||
if name is None:
|
||||
return None
|
||||
return "None" # None crashes apple watches
|
||||
return name.translate(HOMEKIT_CHAR_TRANSLATIONS)[:MAX_NAME_LENGTH]
|
||||
|
||||
|
||||
@@ -420,13 +429,23 @@ def get_aid_storage_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str):
|
||||
)
|
||||
|
||||
|
||||
def _format_version_part(version_part: str) -> str:
|
||||
return str(max(0, min(MAX_VERSION_PART, coerce_int(version_part))))
|
||||
|
||||
|
||||
def format_version(version):
|
||||
"""Extract the version string in a format homekit can consume."""
|
||||
split_ver = str(version).replace("-", ".")
|
||||
split_ver = str(version).replace("-", ".").replace(" ", ".")
|
||||
num_only = NUMBERS_ONLY_RE.sub("", split_ver)
|
||||
if match := VERSION_RE.search(num_only):
|
||||
return match.group(0)
|
||||
return None
|
||||
if (match := VERSION_RE.search(num_only)) is None:
|
||||
return None
|
||||
value = ".".join(map(_format_version_part, match.group(0).split(".")))
|
||||
return None if _is_zero_but_true(value) else value
|
||||
|
||||
|
||||
def _is_zero_but_true(value):
|
||||
"""Zero but true values can crash apple watches."""
|
||||
return convert_to_float(value) == 0
|
||||
|
||||
|
||||
def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str):
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "HomeKit Controller",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"requirements": ["aiohomekit==0.7.15"],
|
||||
"requirements": ["aiohomekit==0.7.16"],
|
||||
"zeroconf": ["_hap._tcp.local."],
|
||||
"after_dependencies": ["zeroconf"],
|
||||
"codeowners": ["@Jc2k", "@bdraco"],
|
||||
|
||||
@@ -6,6 +6,7 @@ import logging
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import aiohttp
|
||||
from aiohue import LinkButtonNotPressed, create_app_key
|
||||
from aiohue.discovery import DiscoveredHueBridge, discover_bridge, discover_nupnp
|
||||
from aiohue.util import normalize_bridge_id
|
||||
@@ -70,9 +71,12 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self, host: str, bridge_id: str | None = None
|
||||
) -> DiscoveredHueBridge:
|
||||
"""Return a DiscoveredHueBridge object."""
|
||||
bridge = await discover_bridge(
|
||||
host, websession=aiohttp_client.async_get_clientsession(self.hass)
|
||||
)
|
||||
try:
|
||||
bridge = await discover_bridge(
|
||||
host, websession=aiohttp_client.async_get_clientsession(self.hass)
|
||||
)
|
||||
except aiohttp.ClientError:
|
||||
return None
|
||||
if bridge_id is not None:
|
||||
bridge_id = normalize_bridge_id(bridge_id)
|
||||
assert bridge_id == bridge.id
|
||||
|
||||
@@ -42,6 +42,14 @@ ALLOWED_ERRORS = [
|
||||
'device (groupedLight) is "soft off", command (on) may not have effect',
|
||||
"device (light) has communication issues, command (on) may not have effect",
|
||||
'device (light) is "soft off", command (on) may not have effect',
|
||||
"device (grouped_light) has communication issues, command (.on) may not have effect",
|
||||
'device (grouped_light) is "soft off", command (.on) may not have effect'
|
||||
"device (grouped_light) has communication issues, command (.on.on) may not have effect",
|
||||
'device (grouped_light) is "soft off", command (.on.on) may not have effect'
|
||||
"device (light) has communication issues, command (.on) may not have effect",
|
||||
'device (light) is "soft off", command (.on) may not have effect',
|
||||
"device (light) has communication issues, command (.on.on) may not have effect",
|
||||
'device (light) is "soft off", command (.on.on) may not have effect',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -39,6 +39,10 @@ from .helpers import (
|
||||
ALLOWED_ERRORS = [
|
||||
"device (light) has communication issues, command (on) may not have effect",
|
||||
'device (light) is "soft off", command (on) may not have effect',
|
||||
"device (light) has communication issues, command (.on) may not have effect",
|
||||
'device (light) is "soft off", command (.on) may not have effect',
|
||||
"device (light) has communication issues, command (.on.on) may not have effect",
|
||||
'device (light) is "soft off", command (.on.on) may not have effect',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "LIFX",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/lifx",
|
||||
"requirements": ["aiolifx==0.7.0", "aiolifx_effects==0.2.2"],
|
||||
"requirements": ["aiolifx==0.7.1", "aiolifx_effects==0.2.2"],
|
||||
"homekit": {
|
||||
"models": ["LIFX"]
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Support for Honeywell Lyric climate platform."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from time import localtime, strftime, time
|
||||
|
||||
@@ -22,6 +23,7 @@ from homeassistant.components.climate.const import (
|
||||
HVAC_MODE_OFF,
|
||||
SUPPORT_PRESET_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE
|
||||
@@ -45,7 +47,11 @@ from .const import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
|
||||
# Only LCC models support presets
|
||||
SUPPORT_FLAGS_LCC = (
|
||||
SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE_RANGE
|
||||
)
|
||||
SUPPORT_FLAGS_TCC = SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE
|
||||
|
||||
LYRIC_HVAC_ACTION_OFF = "EquipmentOff"
|
||||
LYRIC_HVAC_ACTION_HEAT = "Heat"
|
||||
@@ -166,7 +172,11 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
if self.device.changeableValues.thermostatSetpointStatus:
|
||||
support_flags = SUPPORT_FLAGS_LCC
|
||||
else:
|
||||
support_flags = SUPPORT_FLAGS_TCC
|
||||
return support_flags
|
||||
|
||||
@property
|
||||
def temperature_unit(self) -> str:
|
||||
@@ -200,25 +210,28 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the temperature we try to reach."""
|
||||
device = self.device
|
||||
if not device.hasDualSetpointStatus:
|
||||
if (
|
||||
not device.changeableValues.autoChangeoverActive
|
||||
and HVAC_MODES[device.changeableValues.mode] != HVAC_MODE_OFF
|
||||
):
|
||||
if self.hvac_mode == HVAC_MODE_COOL:
|
||||
return device.changeableValues.coolSetpoint
|
||||
return device.changeableValues.heatSetpoint
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self) -> float | None:
|
||||
"""Return the upper bound temperature we try to reach."""
|
||||
def target_temperature_high(self) -> float | None:
|
||||
"""Return the highbound target temperature we try to reach."""
|
||||
device = self.device
|
||||
if device.hasDualSetpointStatus:
|
||||
if device.changeableValues.autoChangeoverActive:
|
||||
return device.changeableValues.coolSetpoint
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self) -> float | None:
|
||||
"""Return the upper bound temperature we try to reach."""
|
||||
def target_temperature_low(self) -> float | None:
|
||||
"""Return the lowbound target temperature we try to reach."""
|
||||
device = self.device
|
||||
if device.hasDualSetpointStatus:
|
||||
if device.changeableValues.autoChangeoverActive:
|
||||
return device.changeableValues.heatSetpoint
|
||||
return None
|
||||
|
||||
@@ -256,11 +269,11 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
|
||||
async def async_set_temperature(self, **kwargs) -> None:
|
||||
"""Set new target temperature."""
|
||||
device = self.device
|
||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
|
||||
device = self.device
|
||||
if device.hasDualSetpointStatus:
|
||||
if device.changeableValues.autoChangeoverActive:
|
||||
if target_temp_low is None or target_temp_high is None:
|
||||
raise HomeAssistantError(
|
||||
"Could not find target_temp_low and/or target_temp_high in arguments"
|
||||
@@ -270,11 +283,13 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
await self._update_thermostat(
|
||||
self.location,
|
||||
device,
|
||||
coolSetpoint=target_temp_low,
|
||||
heatSetpoint=target_temp_high,
|
||||
coolSetpoint=target_temp_high,
|
||||
heatSetpoint=target_temp_low,
|
||||
mode=HVAC_MODES[device.changeableValues.heatCoolMode],
|
||||
)
|
||||
except LYRIC_EXCEPTIONS as exception:
|
||||
_LOGGER.error(exception)
|
||||
await self.coordinator.async_refresh()
|
||||
else:
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
_LOGGER.debug("Set temperature: %s", temp)
|
||||
@@ -289,15 +304,58 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
)
|
||||
except LYRIC_EXCEPTIONS as exception:
|
||||
_LOGGER.error(exception)
|
||||
await self.coordinator.async_refresh()
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
|
||||
"""Set hvac mode."""
|
||||
_LOGGER.debug("Set hvac mode: %s", hvac_mode)
|
||||
_LOGGER.debug("HVAC mode: %s", hvac_mode)
|
||||
try:
|
||||
await self._update_thermostat(
|
||||
self.location, self.device, mode=LYRIC_HVAC_MODES[hvac_mode]
|
||||
)
|
||||
if LYRIC_HVAC_MODES[hvac_mode] == LYRIC_HVAC_MODE_HEAT_COOL:
|
||||
# If the system is off, turn it to Heat first then to Auto, otherwise it turns to
|
||||
# Auto briefly and then reverts to Off (perhaps related to heatCoolMode). This is the
|
||||
# behavior that happens with the native app as well, so likely a bug in the api itself
|
||||
|
||||
if HVAC_MODES[self.device.changeableValues.mode] == HVAC_MODE_OFF:
|
||||
_LOGGER.debug(
|
||||
"HVAC mode passed to lyric: %s",
|
||||
HVAC_MODES[LYRIC_HVAC_MODE_COOL],
|
||||
)
|
||||
await self._update_thermostat(
|
||||
self.location,
|
||||
self.device,
|
||||
mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
|
||||
autoChangeoverActive=False,
|
||||
)
|
||||
# Sleep 3 seconds before proceeding
|
||||
await asyncio.sleep(3)
|
||||
_LOGGER.debug(
|
||||
"HVAC mode passed to lyric: %s",
|
||||
HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
|
||||
)
|
||||
await self._update_thermostat(
|
||||
self.location,
|
||||
self.device,
|
||||
mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
|
||||
autoChangeoverActive=True,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"HVAC mode passed to lyric: %s",
|
||||
HVAC_MODES[self.device.changeableValues.mode],
|
||||
)
|
||||
await self._update_thermostat(
|
||||
self.location, self.device, autoChangeoverActive=True
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode]
|
||||
)
|
||||
await self._update_thermostat(
|
||||
self.location,
|
||||
self.device,
|
||||
mode=LYRIC_HVAC_MODES[hvac_mode],
|
||||
autoChangeoverActive=False,
|
||||
)
|
||||
except LYRIC_EXCEPTIONS as exception:
|
||||
_LOGGER.error(exception)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@@ -243,7 +243,10 @@ class MatrixBot:
|
||||
room.update_aliases()
|
||||
self._aliases_fetched_for.add(room.room_id)
|
||||
|
||||
if room_id_or_alias in room.aliases:
|
||||
if (
|
||||
room_id_or_alias in room.aliases
|
||||
or room_id_or_alias == room.canonical_alias
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Already in room %s (known as %s)", room.room_id, room_id_or_alias
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -75,11 +75,16 @@ from .const import (
|
||||
ATTR_TOPIC,
|
||||
CONF_BIRTH_MESSAGE,
|
||||
CONF_BROKER,
|
||||
CONF_CERTIFICATE,
|
||||
CONF_CLIENT_CERT,
|
||||
CONF_CLIENT_KEY,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_TLS_INSECURE,
|
||||
CONF_TLS_VERSION,
|
||||
CONF_TOPIC,
|
||||
CONF_WILL_MESSAGE,
|
||||
DATA_MQTT_CONFIG,
|
||||
@@ -94,6 +99,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
MQTT_CONNECTED,
|
||||
MQTT_DISCONNECTED,
|
||||
PROTOCOL_31,
|
||||
PROTOCOL_311,
|
||||
)
|
||||
from .discovery import LAST_DISCOVERY
|
||||
@@ -107,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()
|
||||
@@ -118,19 +129,19 @@ SERVICE_DUMP = "dump"
|
||||
|
||||
CONF_DISCOVERY_PREFIX = "discovery_prefix"
|
||||
CONF_KEEPALIVE = "keepalive"
|
||||
CONF_CERTIFICATE = "certificate"
|
||||
CONF_CLIENT_KEY = "client_key"
|
||||
CONF_CLIENT_CERT = "client_cert"
|
||||
CONF_TLS_INSECURE = "tls_insecure"
|
||||
CONF_TLS_VERSION = "tls_version"
|
||||
|
||||
PROTOCOL_31 = "3.1"
|
||||
|
||||
DEFAULT_PORT = 1883
|
||||
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"
|
||||
|
||||
@@ -186,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),
|
||||
@@ -203,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(
|
||||
@@ -609,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}
|
||||
|
||||
|
||||
@@ -628,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(
|
||||
@@ -757,6 +771,58 @@ class Subscription:
|
||||
encoding: str | None = attr.ib(default="utf-8")
|
||||
|
||||
|
||||
class MqttClientSetup:
|
||||
"""Helper class to setup the paho mqtt client from config."""
|
||||
|
||||
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 = mqtt.MQTTv31
|
||||
else:
|
||||
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 = mqtt.base62(uuid.uuid4().int, padding=22)
|
||||
self._client = mqtt.Client(client_id, protocol=proto)
|
||||
|
||||
# Enable logging
|
||||
self._client.enable_logger()
|
||||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
if username is not None:
|
||||
self._client.username_pw_set(username, password)
|
||||
|
||||
if (certificate := config.get(CONF_CERTIFICATE)) == "auto":
|
||||
certificate = certifi.where()
|
||||
|
||||
client_key = config.get(CONF_CLIENT_KEY)
|
||||
client_cert = config.get(CONF_CLIENT_CERT)
|
||||
tls_insecure = config.get(CONF_TLS_INSECURE)
|
||||
if certificate is not None:
|
||||
self._client.tls_set(
|
||||
certificate,
|
||||
certfile=client_cert,
|
||||
keyfile=client_key,
|
||||
tls_version=ssl.PROTOCOL_TLS,
|
||||
)
|
||||
|
||||
if tls_insecure is not None:
|
||||
self._client.tls_insecure_set(tls_insecure)
|
||||
|
||||
@property
|
||||
def client(self) -> mqtt.Client:
|
||||
"""Return the paho MQTT client."""
|
||||
return self._client
|
||||
|
||||
|
||||
class MQTT:
|
||||
"""Home Assistant MQTT client."""
|
||||
|
||||
@@ -821,46 +887,7 @@ class MQTT:
|
||||
|
||||
def init_client(self):
|
||||
"""Initialize paho client."""
|
||||
# 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 self.conf[CONF_PROTOCOL] == PROTOCOL_31:
|
||||
proto: int = mqtt.MQTTv31
|
||||
else:
|
||||
proto = mqtt.MQTTv311
|
||||
|
||||
if (client_id := self.conf.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 = mqtt.base62(uuid.uuid4().int, padding=22)
|
||||
self._mqttc = mqtt.Client(client_id, protocol=proto)
|
||||
|
||||
# Enable logging
|
||||
self._mqttc.enable_logger()
|
||||
|
||||
username = self.conf.get(CONF_USERNAME)
|
||||
password = self.conf.get(CONF_PASSWORD)
|
||||
if username is not None:
|
||||
self._mqttc.username_pw_set(username, password)
|
||||
|
||||
if (certificate := self.conf.get(CONF_CERTIFICATE)) == "auto":
|
||||
certificate = certifi.where()
|
||||
|
||||
client_key = self.conf.get(CONF_CLIENT_KEY)
|
||||
client_cert = self.conf.get(CONF_CLIENT_CERT)
|
||||
tls_insecure = self.conf.get(CONF_TLS_INSECURE)
|
||||
if certificate is not None:
|
||||
self._mqttc.tls_set(
|
||||
certificate,
|
||||
certfile=client_cert,
|
||||
keyfile=client_key,
|
||||
tls_version=ssl.PROTOCOL_TLS,
|
||||
)
|
||||
|
||||
if tls_insecure is not None:
|
||||
self._mqttc.tls_insecure_set(tls_insecure)
|
||||
|
||||
self._mqttc = MqttClientSetup(self.conf).client
|
||||
self._mqttc.on_connect = self._mqtt_on_connect
|
||||
self._mqttc.on_disconnect = self._mqtt_on_disconnect
|
||||
self._mqttc.on_message = self._mqtt_on_message
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from . import MqttClientSetup
|
||||
from .const import (
|
||||
ATTR_PAYLOAD,
|
||||
ATTR_QOS,
|
||||
@@ -62,6 +63,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
can_connect = await self.hass.async_add_executor_job(
|
||||
try_connection,
|
||||
self.hass,
|
||||
user_input[CONF_BROKER],
|
||||
user_input[CONF_PORT],
|
||||
user_input.get(CONF_USERNAME),
|
||||
@@ -102,6 +104,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
data = self._hassio_discovery
|
||||
can_connect = await self.hass.async_add_executor_job(
|
||||
try_connection,
|
||||
self.hass,
|
||||
data[CONF_HOST],
|
||||
data[CONF_PORT],
|
||||
data.get(CONF_USERNAME),
|
||||
@@ -152,6 +155,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
if user_input is not None:
|
||||
can_connect = await self.hass.async_add_executor_job(
|
||||
try_connection,
|
||||
self.hass,
|
||||
user_input[CONF_BROKER],
|
||||
user_input[CONF_PORT],
|
||||
user_input.get(CONF_USERNAME),
|
||||
@@ -313,19 +317,22 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
)
|
||||
|
||||
|
||||
def try_connection(broker, port, username, password, protocol="3.1"):
|
||||
def try_connection(hass, broker, port, username, password, protocol="3.1"):
|
||||
"""Test if we can connect to an MQTT broker."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
import paho.mqtt.client as mqtt
|
||||
# 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 protocol == "3.1":
|
||||
proto = mqtt.MQTTv31
|
||||
else:
|
||||
proto = mqtt.MQTTv311
|
||||
|
||||
client = mqtt.Client(protocol=proto)
|
||||
if username and password:
|
||||
client.username_pw_set(username, password)
|
||||
# Get the config from configuration.yaml
|
||||
yaml_config = hass.data.get(DATA_MQTT_CONFIG, {})
|
||||
entry_config = {
|
||||
CONF_BROKER: broker,
|
||||
CONF_PORT: port,
|
||||
CONF_USERNAME: username,
|
||||
CONF_PASSWORD: password,
|
||||
CONF_PROTOCOL: protocol,
|
||||
}
|
||||
client = MqttClientSetup({**yaml_config, **entry_config}).client
|
||||
|
||||
result = queue.Queue(maxsize=1)
|
||||
|
||||
|
||||
@@ -22,6 +22,12 @@ CONF_STATE_VALUE_TEMPLATE = "state_value_template"
|
||||
CONF_TOPIC = "topic"
|
||||
CONF_WILL_MESSAGE = "will_message"
|
||||
|
||||
CONF_CERTIFICATE = "certificate"
|
||||
CONF_CLIENT_KEY = "client_key"
|
||||
CONF_CLIENT_CERT = "client_cert"
|
||||
CONF_TLS_INSECURE = "tls_insecure"
|
||||
CONF_TLS_VERSION = "tls_version"
|
||||
|
||||
DATA_MQTT_CONFIG = "mqtt_config"
|
||||
DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed"
|
||||
|
||||
@@ -56,4 +62,5 @@ MQTT_DISCONNECTED = "mqtt_disconnected"
|
||||
PAYLOAD_EMPTY_JSON = "{}"
|
||||
PAYLOAD_NONE = "None"
|
||||
|
||||
PROTOCOL_31 = "3.1"
|
||||
PROTOCOL_311 = "3.1.1"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "obihai",
|
||||
"name": "Obihai",
|
||||
"documentation": "https://www.home-assistant.io/integrations/obihai",
|
||||
"requirements": ["pyobihai==1.3.1"],
|
||||
"requirements": ["pyobihai==1.3.2"],
|
||||
"codeowners": ["@dshokouhi"],
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyobihai"]
|
||||
|
||||
@@ -43,7 +43,7 @@ async def async_setup_platform(
|
||||
station_id = config[CONF_STATION_ID]
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
osm_api = OpenSenseMapData(OpenSenseMap(station_id, hass.loop, session))
|
||||
osm_api = OpenSenseMapData(OpenSenseMap(station_id, session))
|
||||
|
||||
await osm_api.async_update()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "opensensemap",
|
||||
"name": "openSenseMap",
|
||||
"documentation": "https://www.home-assistant.io/integrations/opensensemap",
|
||||
"requirements": ["opensensemap-api==0.1.5"],
|
||||
"requirements": ["opensensemap-api==0.2.0"],
|
||||
"codeowners": [],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opensensemap_api"]
|
||||
|
||||
@@ -97,6 +97,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
token_saver=token_saver,
|
||||
)
|
||||
try:
|
||||
# pylint: disable-next=fixme
|
||||
# TODO Remove authlib constraint when refactoring this code
|
||||
await session.ensure_active_token()
|
||||
except ConnectTimeout as err:
|
||||
_LOGGER.debug("Connection Timeout")
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/renault",
|
||||
"requirements": [
|
||||
"renault-api==0.1.9"
|
||||
"renault-api==0.1.10"
|
||||
],
|
||||
"codeowners": [
|
||||
"@epenet"
|
||||
|
||||
@@ -104,7 +104,7 @@ class RenaultVehicleProxy:
|
||||
coordinator = self.coordinators[key]
|
||||
if coordinator.not_supported:
|
||||
# Remove endpoint as it is not supported for this vehicle.
|
||||
LOGGER.warning(
|
||||
LOGGER.info(
|
||||
"Ignoring endpoint %s as it is not supported for this vehicle: %s",
|
||||
coordinator.name,
|
||||
coordinator.last_exception,
|
||||
@@ -112,7 +112,7 @@ class RenaultVehicleProxy:
|
||||
del self.coordinators[key]
|
||||
elif coordinator.access_denied:
|
||||
# Remove endpoint as it is denied for this vehicle.
|
||||
LOGGER.warning(
|
||||
LOGGER.info(
|
||||
"Ignoring endpoint %s as it is denied for this vehicle: %s",
|
||||
coordinator.name,
|
||||
coordinator.last_exception,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "rfxtrx",
|
||||
"name": "RFXCOM RFXtrx",
|
||||
"documentation": "https://www.home-assistant.io/integrations/rfxtrx",
|
||||
"requirements": ["pyRFXtrx==0.27.1"],
|
||||
"requirements": ["pyRFXtrx==0.28.0"],
|
||||
"codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"],
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
"""Support for Roku."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from functools import wraps
|
||||
import logging
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from rokuecp import RokuConnectionError, RokuError
|
||||
from typing_extensions import Concatenate, ParamSpec
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -16,7 +8,6 @@ from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RokuDataUpdateCoordinator
|
||||
from .entity import RokuEntity
|
||||
|
||||
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
||||
|
||||
@@ -27,10 +18,6 @@ PLATFORMS = [
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_T = TypeVar("_T", bound="RokuEntity")
|
||||
_P = ParamSpec("_P")
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -53,22 +40,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
|
||||
|
||||
def roku_exception_handler(
|
||||
func: Callable[Concatenate[_T, _P], Awaitable[None]] # type: ignore[misc]
|
||||
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: # type: ignore[misc]
|
||||
"""Decorate Roku calls to handle Roku exceptions."""
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
|
||||
try:
|
||||
await func(self, *args, **kwargs)
|
||||
except RokuConnectionError as error:
|
||||
if self.available:
|
||||
_LOGGER.error("Error communicating with API: %s", error)
|
||||
except RokuError as error:
|
||||
if self.available:
|
||||
_LOGGER.error("Invalid response from API: %s", error)
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
"""Helpers for Roku."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from functools import wraps
|
||||
import logging
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from rokuecp import RokuConnectionError, RokuConnectionTimeoutError, RokuError
|
||||
from typing_extensions import Concatenate, ParamSpec
|
||||
|
||||
from .entity import RokuEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_T = TypeVar("_T", bound=RokuEntity)
|
||||
_P = ParamSpec("_P")
|
||||
|
||||
|
||||
def format_channel_name(channel_number: str, channel_name: str | None = None) -> str:
|
||||
"""Format a Roku Channel name."""
|
||||
@@ -8,3 +23,28 @@ def format_channel_name(channel_number: str, channel_name: str | None = None) ->
|
||||
return f"{channel_name} ({channel_number})"
|
||||
|
||||
return channel_number
|
||||
|
||||
|
||||
def roku_exception_handler(ignore_timeout: bool = False) -> Callable[..., Callable]:
|
||||
"""Decorate Roku calls to handle Roku exceptions."""
|
||||
|
||||
def decorator(
|
||||
func: Callable[Concatenate[_T, _P], Awaitable[None]], # type: ignore[misc]
|
||||
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: # type: ignore[misc]
|
||||
@wraps(func)
|
||||
async def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
|
||||
try:
|
||||
await func(self, *args, **kwargs)
|
||||
except RokuConnectionTimeoutError as error:
|
||||
if not ignore_timeout and self.available:
|
||||
_LOGGER.error("Error communicating with API: %s", error)
|
||||
except RokuConnectionError as error:
|
||||
if self.available:
|
||||
_LOGGER.error("Error communicating with API: %s", error)
|
||||
except RokuError as error:
|
||||
if self.available:
|
||||
_LOGGER.error("Invalid response from API: %s", error)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "roku",
|
||||
"name": "Roku",
|
||||
"documentation": "https://www.home-assistant.io/integrations/roku",
|
||||
"requirements": ["rokuecp==0.14.1"],
|
||||
"requirements": ["rokuecp==0.15.0"],
|
||||
"homekit": {
|
||||
"models": ["3810X", "4660X", "7820X", "C105X", "C135X"]
|
||||
},
|
||||
|
||||
@@ -51,7 +51,6 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import roku_exception_handler
|
||||
from .browse_media import async_browse_media
|
||||
from .const import (
|
||||
ATTR_ARTIST_NAME,
|
||||
@@ -65,7 +64,7 @@ from .const import (
|
||||
)
|
||||
from .coordinator import RokuDataUpdateCoordinator
|
||||
from .entity import RokuEntity
|
||||
from .helpers import format_channel_name
|
||||
from .helpers import format_channel_name, roku_exception_handler
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -289,7 +288,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
||||
app.name for app in self.coordinator.data.apps if app.name is not None
|
||||
)
|
||||
|
||||
@roku_exception_handler
|
||||
@roku_exception_handler()
|
||||
async def search(self, keyword: str) -> None:
|
||||
"""Emulate opening the search screen and entering the search keyword."""
|
||||
await self.coordinator.roku.search(keyword)
|
||||
@@ -321,68 +320,68 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
||||
media_content_type,
|
||||
)
|
||||
|
||||
@roku_exception_handler
|
||||
@roku_exception_handler()
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn on the Roku."""
|
||||
await self.coordinator.roku.remote("poweron")
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@roku_exception_handler
|
||||
@roku_exception_handler(ignore_timeout=True)
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn off the Roku."""
|
||||
await self.coordinator.roku.remote("poweroff")
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@roku_exception_handler
|
||||
@roku_exception_handler()
|
||||
async def async_media_pause(self) -> None:
|
||||
"""Send pause command."""
|
||||
if self.state not in (STATE_STANDBY, STATE_PAUSED):
|
||||
await self.coordinator.roku.remote("play")
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@roku_exception_handler
|
||||
@roku_exception_handler()
|
||||
async def async_media_play(self) -> None:
|
||||
"""Send play command."""
|
||||
if self.state not in (STATE_STANDBY, STATE_PLAYING):
|
||||
await self.coordinator.roku.remote("play")
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@roku_exception_handler
|
||||
@roku_exception_handler()
|
||||
async def async_media_play_pause(self) -> None:
|
||||
"""Send play/pause command."""
|
||||
if self.state != STATE_STANDBY:
|
||||
await self.coordinator.roku.remote("play")
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@roku_exception_handler
|
||||
@roku_exception_handler()
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Send previous track command."""
|
||||
await self.coordinator.roku.remote("reverse")
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@roku_exception_handler
|
||||
@roku_exception_handler()
|
||||
async def async_media_next_track(self) -> None:
|
||||
"""Send next track command."""
|
||||
await self.coordinator.roku.remote("forward")
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@roku_exception_handler
|
||||
@roku_exception_handler()
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Mute the volume."""
|
||||
await self.coordinator.roku.remote("volume_mute")
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@roku_exception_handler
|
||||
@roku_exception_handler()
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Volume up media player."""
|
||||
await self.coordinator.roku.remote("volume_up")
|
||||
|
||||
@roku_exception_handler
|
||||
@roku_exception_handler()
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Volume down media player."""
|
||||
await self.coordinator.roku.remote("volume_down")
|
||||
|
||||
@roku_exception_handler
|
||||
@roku_exception_handler()
|
||||
async def async_play_media(
|
||||
self, media_type: str, media_id: str, **kwargs: Any
|
||||
) -> None:
|
||||
@@ -487,7 +486,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
||||
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@roku_exception_handler
|
||||
@roku_exception_handler()
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select input source."""
|
||||
if source == "Home":
|
||||
|
||||
@@ -9,10 +9,10 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import roku_exception_handler
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RokuDataUpdateCoordinator
|
||||
from .entity import RokuEntity
|
||||
from .helpers import roku_exception_handler
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -44,19 +44,19 @@ class RokuRemote(RokuEntity, RemoteEntity):
|
||||
"""Return true if device is on."""
|
||||
return not self.coordinator.data.state.standby
|
||||
|
||||
@roku_exception_handler
|
||||
@roku_exception_handler()
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
await self.coordinator.roku.remote("poweron")
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@roku_exception_handler
|
||||
@roku_exception_handler(ignore_timeout=True)
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the device off."""
|
||||
await self.coordinator.roku.remote("poweroff")
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@roku_exception_handler
|
||||
@roku_exception_handler()
|
||||
async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
|
||||
"""Send a command to one device."""
|
||||
num_repeats = kwargs[ATTR_NUM_REPEATS]
|
||||
|
||||
@@ -12,11 +12,10 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import roku_exception_handler
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RokuDataUpdateCoordinator
|
||||
from .entity import RokuEntity
|
||||
from .helpers import format_channel_name
|
||||
from .helpers import format_channel_name, roku_exception_handler
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -163,7 +162,7 @@ class RokuSelectEntity(RokuEntity, SelectEntity):
|
||||
"""Return a set of selectable options."""
|
||||
return self.entity_description.options_fn(self.coordinator.data)
|
||||
|
||||
@roku_exception_handler
|
||||
@roku_exception_handler()
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Set the option."""
|
||||
await self.entity_description.set_fn(
|
||||
|
||||
@@ -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,9 +318,10 @@ 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(TypeError, WebSocketTimeoutException):
|
||||
raw_app_list: list[dict[str, str]] = remote.app_list()
|
||||
LOGGER.debug("Received app list: %s", raw_app_list)
|
||||
self._app_list = {
|
||||
app["name"]: app["appId"]
|
||||
for app in sorted(raw_app_list, key=lambda app: app["name"])
|
||||
|
||||
@@ -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)},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Sonos",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/sonos",
|
||||
"requirements": ["soco==0.26.3"],
|
||||
"requirements": ["soco==0.26.4"],
|
||||
"dependencies": ["ssdp"],
|
||||
"after_dependencies": ["plex", "spotify", "zeroconf", "media_source"],
|
||||
"zeroconf": ["_sonos._tcp.local."],
|
||||
|
||||
@@ -172,7 +172,7 @@ class SQLSensor(SensorEntity):
|
||||
else:
|
||||
self._attr_native_value = data
|
||||
|
||||
if not data:
|
||||
if data is None:
|
||||
_LOGGER.warning("%s returned no results", self._query)
|
||||
|
||||
sess.close()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -277,8 +277,6 @@ class TemplateFan(TemplateEntity, FanEntity):
|
||||
"""Turn off the fan."""
|
||||
await self._off_script.async_run(context=self._context)
|
||||
self._state = STATE_OFF
|
||||
self._percentage = 0
|
||||
self._preset_mode = None
|
||||
|
||||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
"""Set the percentage speed of the fan."""
|
||||
|
||||
@@ -88,7 +88,10 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
||||
|
||||
# Handle turning to temp mode
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
color_tmp = mired_to_kelvin(int(kwargs[ATTR_COLOR_TEMP]))
|
||||
# Handle temp conversion mireds -> kelvin being slightly outside of valid range
|
||||
kelvin = mired_to_kelvin(int(kwargs[ATTR_COLOR_TEMP]))
|
||||
kelvin_range = self.device.valid_temperature_range
|
||||
color_tmp = max(kelvin_range.min, min(kelvin_range.max, kelvin))
|
||||
_LOGGER.debug("Changing color temp to %s", color_tmp)
|
||||
await self.device.set_color_temp(
|
||||
color_tmp, brightness=brightness, transition=transition
|
||||
|
||||
@@ -78,4 +78,4 @@ class VelbusCover(VelbusEntity, CoverEntity):
|
||||
|
||||
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||
"""Move the cover to a specific position."""
|
||||
self._channel.set_position(100 - kwargs[ATTR_POSITION])
|
||||
await self._channel.set_position(100 - kwargs[ATTR_POSITION])
|
||||
|
||||
@@ -14,7 +14,6 @@ from miio import (
|
||||
AirHumidifierMiot,
|
||||
AirHumidifierMjjsq,
|
||||
AirPurifier,
|
||||
AirPurifierMB4,
|
||||
AirPurifierMiot,
|
||||
CleaningDetails,
|
||||
CleaningSummary,
|
||||
@@ -23,10 +22,8 @@ from miio import (
|
||||
DNDStatus,
|
||||
Fan,
|
||||
Fan1C,
|
||||
FanMiot,
|
||||
FanP5,
|
||||
FanP9,
|
||||
FanP10,
|
||||
FanP11,
|
||||
FanZA5,
|
||||
RoborockVacuum,
|
||||
Timer,
|
||||
@@ -52,7 +49,6 @@ from .const import (
|
||||
KEY_DEVICE,
|
||||
MODEL_AIRFRESH_A1,
|
||||
MODEL_AIRFRESH_T2017,
|
||||
MODEL_AIRPURIFIER_3C,
|
||||
MODEL_FAN_1C,
|
||||
MODEL_FAN_P5,
|
||||
MODEL_FAN_P9,
|
||||
@@ -111,10 +107,10 @@ AIR_MONITOR_PLATFORMS = [Platform.AIR_QUALITY, Platform.SENSOR]
|
||||
|
||||
MODEL_TO_CLASS_MAP = {
|
||||
MODEL_FAN_1C: Fan1C,
|
||||
MODEL_FAN_P10: FanP10,
|
||||
MODEL_FAN_P11: FanP11,
|
||||
MODEL_FAN_P9: FanMiot,
|
||||
MODEL_FAN_P10: FanMiot,
|
||||
MODEL_FAN_P11: FanMiot,
|
||||
MODEL_FAN_P5: FanP5,
|
||||
MODEL_FAN_P9: FanP9,
|
||||
MODEL_FAN_ZA5: FanZA5,
|
||||
}
|
||||
|
||||
@@ -314,8 +310,6 @@ async def async_create_miio_device_and_coordinator(
|
||||
device = AirHumidifier(host, token, model=model)
|
||||
migrate = True
|
||||
# Airpurifiers and Airfresh
|
||||
elif model == MODEL_AIRPURIFIER_3C:
|
||||
device = AirPurifierMB4(host, token)
|
||||
elif model in MODELS_PURIFIER_MIOT:
|
||||
device = AirPurifierMiot(host, token)
|
||||
elif model.startswith("zhimi.airpurifier."):
|
||||
|
||||
@@ -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 = "0"
|
||||
PATCH_VERSION: Final = "6"
|
||||
__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
|
||||
@@ -95,3 +95,7 @@ python-socketio>=4.6.0,<5.0
|
||||
# Constrain multidict to avoid typing issues
|
||||
# https://github.com/home-assistant/core/pull/67046
|
||||
multidict>=6.0.2
|
||||
|
||||
# Required for compatibility with point integration - ensure_active_token
|
||||
# https://github.com/home-assistant/core/pull/68176
|
||||
authlib<1.0
|
||||
|
||||
@@ -149,10 +149,17 @@ async def _async_setup_component(
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
integration: loader.Integration | None = None
|
||||
|
||||
def log_error(msg: str, link: str | None = None) -> None:
|
||||
def log_error(msg: str) -> None:
|
||||
"""Log helper."""
|
||||
_LOGGER.error("Setup failed for %s: %s", domain, msg)
|
||||
if integration is None:
|
||||
custom = ""
|
||||
link = None
|
||||
else:
|
||||
custom = "" if integration.is_built_in else "custom integration "
|
||||
link = integration.documentation
|
||||
_LOGGER.error("Setup failed for %s%s: %s", custom, domain, msg)
|
||||
async_notify_setup_error(hass, domain, link)
|
||||
|
||||
try:
|
||||
@@ -174,7 +181,7 @@ async def _async_setup_component(
|
||||
try:
|
||||
await async_process_deps_reqs(hass, config, integration)
|
||||
except HomeAssistantError as err:
|
||||
log_error(str(err), integration.documentation)
|
||||
log_error(str(err))
|
||||
return False
|
||||
|
||||
# Some integrations fail on import because they call functions incorrectly.
|
||||
@@ -182,7 +189,7 @@ async def _async_setup_component(
|
||||
try:
|
||||
component = integration.get_component()
|
||||
except ImportError as err:
|
||||
log_error(f"Unable to import component: {err}", integration.documentation)
|
||||
log_error(f"Unable to import component: {err}")
|
||||
return False
|
||||
|
||||
processed_config = await conf_util.async_process_component_config(
|
||||
@@ -190,7 +197,7 @@ async def _async_setup_component(
|
||||
)
|
||||
|
||||
if processed_config is None:
|
||||
log_error("Invalid config.", integration.documentation)
|
||||
log_error("Invalid config.")
|
||||
return False
|
||||
|
||||
start = timer()
|
||||
@@ -287,6 +294,7 @@ async def async_prepare_setup_platform(
|
||||
|
||||
def log_error(msg: str) -> None:
|
||||
"""Log helper."""
|
||||
|
||||
_LOGGER.error("Unable to prepare setup for platform %s: %s", platform_path, msg)
|
||||
async_notify_setup_error(hass, platform_path)
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@ aioguardian==2021.11.0
|
||||
aioharmony==0.2.9
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==0.7.15
|
||||
aiohomekit==0.7.16
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@@ -206,7 +206,7 @@ aiokafka==0.6.0
|
||||
aiokef==0.2.16
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx==0.7.0
|
||||
aiolifx==0.7.1
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx_effects==0.2.2
|
||||
@@ -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
|
||||
@@ -1180,7 +1180,7 @@ openevsewifi==1.1.0
|
||||
openhomedevice==2.0.1
|
||||
|
||||
# homeassistant.components.opensensemap
|
||||
opensensemap-api==0.1.5
|
||||
opensensemap-api==0.2.0
|
||||
|
||||
# homeassistant.components.enigma2
|
||||
openwebifpy==3.2.7
|
||||
@@ -1360,7 +1360,7 @@ pyMetEireann==2021.8.0
|
||||
pyMetno==0.9.0
|
||||
|
||||
# homeassistant.components.rfxtrx
|
||||
pyRFXtrx==0.27.1
|
||||
pyRFXtrx==0.28.0
|
||||
|
||||
# homeassistant.components.switchmate
|
||||
# pySwitchmate==0.4.6
|
||||
@@ -1486,7 +1486,7 @@ pydispatcher==2.0.5
|
||||
pydoods==1.0.2
|
||||
|
||||
# homeassistant.components.android_ip_webcam
|
||||
pydroid-ipcam==0.8
|
||||
pydroid-ipcam==1.3.1
|
||||
|
||||
# homeassistant.components.ebox
|
||||
pyebox==1.1.4
|
||||
@@ -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
|
||||
@@ -1727,7 +1727,7 @@ pynx584==0.5
|
||||
pynzbgetapi==0.2.0
|
||||
|
||||
# homeassistant.components.obihai
|
||||
pyobihai==1.3.1
|
||||
pyobihai==1.3.2
|
||||
|
||||
# homeassistant.components.octoprint
|
||||
pyoctoprintapi==0.1.7
|
||||
@@ -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
|
||||
@@ -2097,7 +2097,7 @@ raspyrfm-client==1.2.8
|
||||
regenmaschine==2022.01.0
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.1.9
|
||||
renault-api==0.1.10
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==5.2
|
||||
@@ -2121,7 +2121,7 @@ rjpl==0.3.6
|
||||
rocketchat-API==0.6.1
|
||||
|
||||
# homeassistant.components.roku
|
||||
rokuecp==0.14.1
|
||||
rokuecp==0.15.0
|
||||
|
||||
# homeassistant.components.roomba
|
||||
roombapy==1.6.5
|
||||
@@ -2235,7 +2235,7 @@ smhi-pkg==1.0.15
|
||||
snapcast==2.1.3
|
||||
|
||||
# homeassistant.components.sonos
|
||||
soco==0.26.3
|
||||
soco==0.26.4
|
||||
|
||||
# homeassistant.components.solaredge_local
|
||||
solaredge-local==0.2.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
|
||||
|
||||
@@ -134,7 +134,7 @@ aioguardian==2021.11.0
|
||||
aioharmony==0.2.9
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==0.7.15
|
||||
aiohomekit==0.7.16
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@@ -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
|
||||
@@ -858,7 +858,7 @@ pyMetEireann==2021.8.0
|
||||
pyMetno==0.9.0
|
||||
|
||||
# homeassistant.components.rfxtrx
|
||||
pyRFXtrx==0.27.1
|
||||
pyRFXtrx==0.28.0
|
||||
|
||||
# homeassistant.components.tibber
|
||||
pyTibber==0.22.1
|
||||
@@ -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,13 +1295,13 @@ 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
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.1.9
|
||||
renault-api==0.1.10
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==5.2
|
||||
@@ -1313,7 +1313,7 @@ rflink==0.0.62
|
||||
ring_doorbell==0.7.2
|
||||
|
||||
# homeassistant.components.roku
|
||||
rokuecp==0.14.1
|
||||
rokuecp==0.15.0
|
||||
|
||||
# homeassistant.components.roomba
|
||||
roombapy==1.6.5
|
||||
@@ -1371,7 +1371,7 @@ smarthab==0.21
|
||||
smhi-pkg==1.0.15
|
||||
|
||||
# homeassistant.components.sonos
|
||||
soco==0.26.3
|
||||
soco==0.26.4
|
||||
|
||||
# homeassistant.components.solaredge
|
||||
solaredge==0.0.2
|
||||
@@ -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
|
||||
|
||||
@@ -124,6 +124,10 @@ python-socketio>=4.6.0,<5.0
|
||||
# Constrain multidict to avoid typing issues
|
||||
# https://github.com/home-assistant/core/pull/67046
|
||||
multidict>=6.0.2
|
||||
|
||||
# Required for compatibility with point integration - ensure_active_token
|
||||
# https://github.com/home-assistant/core/pull/68176
|
||||
authlib<1.0
|
||||
"""
|
||||
|
||||
IGNORE_PRE_COMMIT_HOOK_ID = (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = homeassistant
|
||||
version = 2022.3.0
|
||||
version = 2022.3.6
|
||||
author = The Home Assistant Authors
|
||||
author_email = hello@home-assistant.io
|
||||
license = Apache-2.0
|
||||
|
||||
@@ -137,6 +137,39 @@ async def test_register_mac(hass):
|
||||
assert entity_entry_1.disabled_by is None
|
||||
|
||||
|
||||
async def test_register_mac_ignored(hass):
|
||||
"""Test ignoring registering a mac."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
ent_reg = er.async_get(hass)
|
||||
|
||||
config_entry = MockConfigEntry(domain="test", pref_disable_new_entities=True)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
mac1 = "12:34:56:AB:CD:EF"
|
||||
|
||||
entity_entry_1 = ent_reg.async_get_or_create(
|
||||
"device_tracker",
|
||||
"test",
|
||||
mac1 + "yo1",
|
||||
original_name="name 1",
|
||||
config_entry=config_entry,
|
||||
disabled_by=er.RegistryEntryDisabler.INTEGRATION,
|
||||
)
|
||||
|
||||
ce._async_register_mac(hass, "test", mac1, mac1 + "yo1")
|
||||
|
||||
dev_reg.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, mac1)},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_entry_1 = ent_reg.async_get(entity_entry_1.entity_id)
|
||||
|
||||
assert entity_entry_1.disabled_by == er.RegistryEntryDisabler.INTEGRATION
|
||||
|
||||
|
||||
async def test_connected_device_registered(hass):
|
||||
"""Test dispatch on connected device being registered."""
|
||||
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -73,6 +73,155 @@ async def test_form_user_with_secure_elk_no_discovery(hass):
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_user_with_insecure_elk_skip_discovery(hass):
|
||||
"""Test we can setup a insecure elk with skipping discovery."""
|
||||
|
||||
with _patch_discovery(), _patch_elk():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with _patch_discovery(no_device=True):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {}
|
||||
assert result["step_id"] == "manual_connection"
|
||||
|
||||
mocked_elk = mock_elk(invalid_auth=False, sync_complete=True)
|
||||
|
||||
with _patch_discovery(), _patch_elk(elk=mocked_elk), patch(
|
||||
"homeassistant.components.elkm1.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.elkm1.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"protocol": "non-secure",
|
||||
"address": "1.2.3.4",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"prefix": "",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "ElkM1"
|
||||
assert result2["data"] == {
|
||||
"auto_configure": True,
|
||||
"host": "elk://1.2.3.4",
|
||||
"password": "test-password",
|
||||
"prefix": "",
|
||||
"username": "test-username",
|
||||
}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_user_with_insecure_elk_no_discovery(hass):
|
||||
"""Test we can setup a insecure elk."""
|
||||
|
||||
with _patch_discovery(), _patch_elk():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with _patch_discovery(no_device=True):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {}
|
||||
assert result["step_id"] == "manual_connection"
|
||||
|
||||
mocked_elk = mock_elk(invalid_auth=False, sync_complete=True)
|
||||
|
||||
with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch(
|
||||
"homeassistant.components.elkm1.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.elkm1.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"protocol": "non-secure",
|
||||
"address": "1.2.3.4",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"prefix": "",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "ElkM1"
|
||||
assert result2["data"] == {
|
||||
"auto_configure": True,
|
||||
"host": "elk://1.2.3.4",
|
||||
"password": "test-password",
|
||||
"prefix": "",
|
||||
"username": "test-username",
|
||||
}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_user_with_insecure_elk_times_out(hass):
|
||||
"""Test we can setup a insecure elk that times out."""
|
||||
|
||||
with _patch_discovery(), _patch_elk():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with _patch_discovery(no_device=True):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {}
|
||||
assert result["step_id"] == "manual_connection"
|
||||
|
||||
mocked_elk = mock_elk(invalid_auth=False, sync_complete=False)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.elkm1.config_flow.VALIDATE_TIMEOUT",
|
||||
0,
|
||||
), patch(
|
||||
"homeassistant.components.elkm1.config_flow.LOGIN_TIMEOUT", 0
|
||||
), _patch_discovery(), _patch_elk(
|
||||
elk=mocked_elk
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"protocol": "non-secure",
|
||||
"address": "1.2.3.4",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"prefix": "",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == RESULT_TYPE_FORM
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_user_with_secure_elk_no_discovery_ip_already_configured(hass):
|
||||
"""Test we abort when we try to configure the same ip."""
|
||||
config_entry = MockConfigEntry(
|
||||
@@ -262,7 +411,7 @@ async def test_form_user_with_secure_elk_with_discovery_pick_manual_direct_disco
|
||||
assert result3["title"] == "ElkM1 ddeeff"
|
||||
assert result3["data"] == {
|
||||
"auto_configure": True,
|
||||
"host": "elks://127.0.0.1:2601",
|
||||
"host": "elks://127.0.0.1",
|
||||
"password": "test-password",
|
||||
"prefix": "",
|
||||
"username": "test-username",
|
||||
@@ -434,7 +583,7 @@ async def test_form_cannot_connect(hass):
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {CONF_HOST: "cannot_connect"}
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_unknown_exception(hass):
|
||||
|
||||
@@ -6,6 +6,7 @@ import httpx
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.enphase_envoy.const import DOMAIN
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -312,8 +313,8 @@ async def test_zeroconf_serial_already_exists(hass: HomeAssistant) -> None:
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data=zeroconf.ZeroconfServiceInfo(
|
||||
host="1.1.1.1",
|
||||
addresses=["1.1.1.1"],
|
||||
host="4.4.4.4",
|
||||
addresses=["4.4.4.4"],
|
||||
hostname="mock_hostname",
|
||||
name="mock_name",
|
||||
port=None,
|
||||
@@ -324,6 +325,42 @@ async def test_zeroconf_serial_already_exists(hass: HomeAssistant) -> None:
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "already_configured"
|
||||
assert config_entry.data[CONF_HOST] == "4.4.4.4"
|
||||
|
||||
|
||||
async def test_zeroconf_serial_already_exists_ignores_ipv6(hass: HomeAssistant) -> None:
|
||||
"""Test serial number already exists from zeroconf but the discovery is ipv6."""
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"host": "1.1.1.1",
|
||||
"name": "Envoy",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
},
|
||||
unique_id="1234",
|
||||
title="Envoy",
|
||||
)
|
||||
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="fd00::b27c:63bb:cc85:4ea0",
|
||||
addresses=["fd00::b27c:63bb:cc85:4ea0"],
|
||||
hostname="mock_hostname",
|
||||
name="mock_name",
|
||||
port=None,
|
||||
properties={"serialnum": "1234"},
|
||||
type="mock_type",
|
||||
),
|
||||
)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "not_ipv4_address"
|
||||
assert config_entry.data[CONF_HOST] == "1.1.1.1"
|
||||
|
||||
|
||||
async def test_zeroconf_host_already_exists(hass: HomeAssistant) -> None:
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .const import MOCK_USER_DATA
|
||||
from .const import MOCK_MESH_MASTER_MAC, MOCK_USER_DATA
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
@@ -69,6 +69,7 @@ async def test_entry_diagnostics(
|
||||
"latest_firmware": None,
|
||||
"mesh_role": "master",
|
||||
"model": "FRITZ!Box 7530 AX",
|
||||
"unique_id": MOCK_MESH_MASTER_MAC.replace("6F:12", "XX:XX"),
|
||||
"update_available": False,
|
||||
"wan_link_properties": {
|
||||
"NewLayer1DownstreamMaxBitRate": 318557000,
|
||||
|
||||
@@ -42,7 +42,6 @@ from homeassistant.const import (
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
__version__,
|
||||
__version__ as hass_version,
|
||||
)
|
||||
from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS
|
||||
@@ -166,7 +165,9 @@ async def test_home_accessory(hass, hk_driver):
|
||||
serv.get_characteristic(CHAR_SERIAL_NUMBER).value
|
||||
== "light.accessory_that_exceeds_the_maximum_maximum_maximum_maximum"
|
||||
)
|
||||
assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version
|
||||
assert hass_version.startswith(
|
||||
serv.get_characteristic(CHAR_FIRMWARE_REVISION).value
|
||||
)
|
||||
|
||||
hass.states.async_set(entity_id, "on")
|
||||
await hass.async_block_till_done()
|
||||
@@ -216,7 +217,9 @@ async def test_accessory_with_missing_basic_service_info(hass, hk_driver):
|
||||
assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Home Assistant Sensor"
|
||||
assert serv.get_characteristic(CHAR_MODEL).value == "Sensor"
|
||||
assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id
|
||||
assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version
|
||||
assert hass_version.startswith(
|
||||
serv.get_characteristic(CHAR_FIRMWARE_REVISION).value
|
||||
)
|
||||
assert isinstance(acc.to_HAP(), dict)
|
||||
|
||||
|
||||
@@ -244,7 +247,9 @@ async def test_accessory_with_hardware_revision(hass, hk_driver):
|
||||
assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Home Assistant Sensor"
|
||||
assert serv.get_characteristic(CHAR_MODEL).value == "Sensor"
|
||||
assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id
|
||||
assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version
|
||||
assert hass_version.startswith(
|
||||
serv.get_characteristic(CHAR_FIRMWARE_REVISION).value
|
||||
)
|
||||
assert serv.get_characteristic(CHAR_HARDWARE_REVISION).value == "1.2.3"
|
||||
assert isinstance(acc.to_HAP(), dict)
|
||||
|
||||
@@ -687,7 +692,9 @@ def test_home_bridge(hk_driver):
|
||||
serv = bridge.services[0] # SERV_ACCESSORY_INFO
|
||||
assert serv.display_name == SERV_ACCESSORY_INFO
|
||||
assert serv.get_characteristic(CHAR_NAME).value == BRIDGE_NAME
|
||||
assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == __version__
|
||||
assert hass_version.startswith(
|
||||
serv.get_characteristic(CHAR_FIRMWARE_REVISION).value
|
||||
)
|
||||
assert serv.get_characteristic(CHAR_MANUFACTURER).value == MANUFACTURER
|
||||
assert serv.get_characteristic(CHAR_MODEL).value == BRIDGE_MODEL
|
||||
assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == BRIDGE_SERIAL_NUMBER
|
||||
|
||||
@@ -399,4 +399,4 @@ async def test_empty_name(hass, hk_driver):
|
||||
assert acc.category == 10 # Sensor
|
||||
|
||||
assert acc.char_humidity.value == 20
|
||||
assert acc.display_name is None
|
||||
assert acc.display_name == "None"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,6 +30,7 @@ from homeassistant.components.homekit.util import (
|
||||
async_port_is_available,
|
||||
async_show_setup_message,
|
||||
cleanup_name_for_homekit,
|
||||
coerce_int,
|
||||
convert_to_float,
|
||||
density_to_air_quality,
|
||||
format_version,
|
||||
@@ -349,13 +350,23 @@ async def test_format_version():
|
||||
assert format_version("undefined-undefined-1.6.8") == "1.6.8"
|
||||
assert format_version("56.0-76060") == "56.0.76060"
|
||||
assert format_version(3.6) == "3.6"
|
||||
assert format_version("AK001-ZJ100") == "001.100"
|
||||
assert format_version("AK001-ZJ100") == "1.100"
|
||||
assert format_version("HF-LPB100-") == "100"
|
||||
assert format_version("AK001-ZJ2149") == "001.2149"
|
||||
assert format_version("AK001-ZJ2149") == "1.2149"
|
||||
assert format_version("13216407885") == "4294967295" # max value
|
||||
assert format_version("000132 16407885") == "132.16407885"
|
||||
assert format_version("0.1") == "0.1"
|
||||
assert format_version("0") is None
|
||||
assert format_version("unknown") is None
|
||||
|
||||
|
||||
async def test_coerce_int():
|
||||
"""Test coerce_int method."""
|
||||
assert coerce_int("1") == 1
|
||||
assert coerce_int("") == 0
|
||||
assert coerce_int(0) == 0
|
||||
|
||||
|
||||
async def test_accessory_friendly_name():
|
||||
"""Test we provide a helpful friendly name."""
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -3,8 +3,9 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
import yaml
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant import config as hass_config, config_entries, data_entry_flow
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.components.hassio import HassioServiceInfo
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -151,7 +152,7 @@ async def test_manual_config_set(
|
||||
"discovery": True,
|
||||
}
|
||||
# Check we tried the connection, with precedence for config entry settings
|
||||
mock_try_connection.assert_called_once_with("127.0.0.1", 1883, None, None)
|
||||
mock_try_connection.assert_called_once_with(hass, "127.0.0.1", 1883, None, None)
|
||||
# Check config entry got setup
|
||||
assert len(mock_finish_setup.mock_calls) == 1
|
||||
|
||||
@@ -642,3 +643,95 @@ async def test_options_bad_will_message_fails(hass, mock_try_connection):
|
||||
mqtt.CONF_BROKER: "test-broker",
|
||||
mqtt.CONF_PORT: 1234,
|
||||
}
|
||||
|
||||
|
||||
async def test_try_connection_with_advanced_parameters(
|
||||
hass, mock_try_connection_success, tmp_path
|
||||
):
|
||||
"""Test config flow with advanced parameters from config."""
|
||||
# Mock certificate files
|
||||
certfile = tmp_path / "cert.pem"
|
||||
certfile.write_text("## mock certificate file ##")
|
||||
keyfile = tmp_path / "key.pem"
|
||||
keyfile.write_text("## mock key file ##")
|
||||
config = {
|
||||
"certificate": "auto",
|
||||
"tls_insecure": True,
|
||||
"client_cert": certfile,
|
||||
"client_key": keyfile,
|
||||
}
|
||||
new_yaml_config_file = tmp_path / "configuration.yaml"
|
||||
new_yaml_config = yaml.dump({mqtt.DOMAIN: config})
|
||||
new_yaml_config_file.write_text(new_yaml_config)
|
||||
assert new_yaml_config_file.read_text() == new_yaml_config
|
||||
|
||||
with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file):
|
||||
await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
|
||||
await hass.async_block_till_done()
|
||||
config_entry = MockConfigEntry(domain=mqtt.DOMAIN)
|
||||
config_entry.add_to_hass(hass)
|
||||
config_entry.data = {
|
||||
mqtt.CONF_BROKER: "test-broker",
|
||||
mqtt.CONF_PORT: 1234,
|
||||
mqtt.CONF_USERNAME: "user",
|
||||
mqtt.CONF_PASSWORD: "pass",
|
||||
mqtt.CONF_DISCOVERY: True,
|
||||
mqtt.CONF_BIRTH_MESSAGE: {
|
||||
mqtt.ATTR_TOPIC: "ha_state/online",
|
||||
mqtt.ATTR_PAYLOAD: "online",
|
||||
mqtt.ATTR_QOS: 1,
|
||||
mqtt.ATTR_RETAIN: True,
|
||||
},
|
||||
mqtt.CONF_WILL_MESSAGE: {
|
||||
mqtt.ATTR_TOPIC: "ha_state/offline",
|
||||
mqtt.ATTR_PAYLOAD: "offline",
|
||||
mqtt.ATTR_QOS: 2,
|
||||
mqtt.ATTR_RETAIN: False,
|
||||
},
|
||||
}
|
||||
|
||||
# Test default/suggested values from config
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "broker"
|
||||
defaults = {
|
||||
mqtt.CONF_BROKER: "test-broker",
|
||||
mqtt.CONF_PORT: 1234,
|
||||
}
|
||||
suggested = {
|
||||
mqtt.CONF_USERNAME: "user",
|
||||
mqtt.CONF_PASSWORD: "pass",
|
||||
}
|
||||
for k, v in defaults.items():
|
||||
assert get_default(result["data_schema"].schema, k) == v
|
||||
for k, v in suggested.items():
|
||||
assert get_suggested(result["data_schema"].schema, k) == v
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
mqtt.CONF_BROKER: "another-broker",
|
||||
mqtt.CONF_PORT: 2345,
|
||||
mqtt.CONF_USERNAME: "us3r",
|
||||
mqtt.CONF_PASSWORD: "p4ss",
|
||||
},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "options"
|
||||
|
||||
# check if the username and password was set from config flow and not from configuration.yaml
|
||||
assert mock_try_connection_success.username_pw_set.mock_calls[0][1] == (
|
||||
"us3r",
|
||||
"p4ss",
|
||||
)
|
||||
|
||||
# check if tls_insecure_set is called
|
||||
assert mock_try_connection_success.tls_insecure_set.mock_calls[0][1] == (True,)
|
||||
|
||||
# check if the certificate settings were set from configuration.yaml
|
||||
assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[
|
||||
"certfile"
|
||||
] == str(certfile)
|
||||
assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[
|
||||
"keyfile"
|
||||
] == str(keyfile)
|
||||
|
||||
@@ -3,7 +3,7 @@ from datetime import timedelta
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from rokuecp import RokuError
|
||||
from rokuecp import RokuConnectionError, RokuConnectionTimeoutError, RokuError
|
||||
|
||||
from homeassistant.components.media_player import MediaPlayerDeviceClass
|
||||
from homeassistant.components.media_player.const import (
|
||||
@@ -164,10 +164,15 @@ async def test_tv_setup(
|
||||
assert device_entry.suggested_area == "Living room"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"error",
|
||||
[RokuConnectionTimeoutError, RokuConnectionError, RokuError],
|
||||
)
|
||||
async def test_availability(
|
||||
hass: HomeAssistant,
|
||||
mock_roku: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
error: RokuError,
|
||||
) -> None:
|
||||
"""Test entity availability."""
|
||||
now = dt_util.utcnow()
|
||||
@@ -179,7 +184,7 @@ async def test_availability(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=future):
|
||||
mock_roku.update.side_effect = RokuError
|
||||
mock_roku.update.side_effect = error
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(MAIN_ENTITY_ID).state == STATE_UNAVAILABLE
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user