mirror of
https://github.com/home-assistant/core.git
synced 2026-01-03 14:25:28 +01:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be39a3cee0 | ||
|
|
826423d8ed | ||
|
|
99c0ef9ab8 | ||
|
|
4041dd61c5 | ||
|
|
fe36ff5fa9 | ||
|
|
9acd1471a0 | ||
|
|
d1a6eb55b1 | ||
|
|
112732c673 | ||
|
|
45638c78ae | ||
|
|
d5e80ed2a5 | ||
|
|
9b3c3232be | ||
|
|
feea5af3d0 | ||
|
|
be5d816fbe | ||
|
|
911de94345 | ||
|
|
53fa6c138a | ||
|
|
85e6b3950c | ||
|
|
6fd4355314 | ||
|
|
050600375d | ||
|
|
1114877062 | ||
|
|
454cb44ee8 | ||
|
|
9636435ff2 | ||
|
|
f85781dc51 | ||
|
|
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 |
@@ -88,8 +88,6 @@ class AbodeCamera(AbodeDevice, Camera):
|
||||
self, width: int | None = None, height: int | None = None
|
||||
) -> bytes | None:
|
||||
"""Get a camera image."""
|
||||
if not self.capture():
|
||||
return None
|
||||
self.refresh_image()
|
||||
|
||||
if self._response:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -19,6 +19,7 @@ from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PIN
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util.network import is_ipv6_address
|
||||
|
||||
from .const import CONF_CREDENTIALS, CONF_IDENTIFIERS, CONF_START_OFF, DOMAIN
|
||||
|
||||
@@ -166,6 +167,8 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Handle device found via zeroconf."""
|
||||
host = discovery_info.host
|
||||
if is_ipv6_address(host):
|
||||
return self.async_abort(reason="ipv6_not_supported")
|
||||
self._async_abort_entries_match({CONF_ADDRESS: host})
|
||||
service_type = discovery_info.type[:-1] # Remove leading .
|
||||
name = discovery_info.name.replace(f".{service_type}.", "")
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
},
|
||||
"abort": {
|
||||
"ipv6_not_supported": "IPv6 is not supported.",
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"device_did_not_pair": "No attempt to finish pairing process was made from the device.",
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured",
|
||||
"already_configured_device": "Device is already configured",
|
||||
"already_in_progress": "Configuration flow is already in progress",
|
||||
"backoff": "Device does not accept pairing requests at this time (you might have entered an invalid PIN code too many times), try again later.",
|
||||
"device_did_not_pair": "No attempt to finish pairing process was made from the device.",
|
||||
"device_not_found": "Device was not found during discovery, please try adding it again.",
|
||||
"inconsistent_device": "Expected protocols were not found during discovery. This normally indicates a problem with multicast DNS (Zeroconf). Please try adding the device again.",
|
||||
"invalid_config": "The configuration for this device is incomplete. Please try adding it again.",
|
||||
"ipv6_not_supported": "IPv6 is not supported.",
|
||||
"no_devices_found": "No devices found on the network",
|
||||
"reauth_successful": "Re-authentication was successful",
|
||||
"setup_failed": "Failed to set up device.",
|
||||
@@ -18,7 +17,6 @@
|
||||
"already_configured": "Device is already configured",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"no_devices_found": "No devices found on the network",
|
||||
"no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"flow_title": "{name} ({type})",
|
||||
@@ -72,6 +70,5 @@
|
||||
"description": "Configure general device settings"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Apple TV"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -37,7 +37,9 @@ from .discovery import (
|
||||
|
||||
CONF_DEVICE = "device"
|
||||
|
||||
NON_SECURE_PORT = 2101
|
||||
SECURE_PORT = 2601
|
||||
STANDARD_PORTS = {NON_SECURE_PORT, SECURE_PORT}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -48,6 +50,7 @@ PROTOCOL_MAP = {
|
||||
"serial": "serial://",
|
||||
}
|
||||
|
||||
|
||||
VALIDATE_TIMEOUT = 35
|
||||
|
||||
BASE_SCHEMA = {
|
||||
@@ -60,6 +63,11 @@ ALL_PROTOCOLS = [*SECURE_PROTOCOLS, "non-secure", "serial"]
|
||||
DEFAULT_SECURE_PROTOCOL = "secure"
|
||||
DEFAULT_NON_SECURE_PROTOCOL = "non-secure"
|
||||
|
||||
PORT_PROTOCOL_MAP = {
|
||||
NON_SECURE_PORT: DEFAULT_NON_SECURE_PROTOCOL,
|
||||
SECURE_PORT: DEFAULT_SECURE_PROTOCOL,
|
||||
}
|
||||
|
||||
|
||||
async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str]:
|
||||
"""Validate the user input allows us to connect.
|
||||
@@ -97,6 +105,13 @@ async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str
|
||||
return {"title": device_name, CONF_HOST: url, CONF_PREFIX: slugify(prefix)}
|
||||
|
||||
|
||||
def _address_from_discovery(device: ElkSystem):
|
||||
"""Append the port only if its non-standard."""
|
||||
if device.port in STANDARD_PORTS:
|
||||
return device.ip_address
|
||||
return f"{device.ip_address}:{device.port}"
|
||||
|
||||
|
||||
def _make_url_from_data(data):
|
||||
if host := data.get(CONF_HOST):
|
||||
return host
|
||||
@@ -109,7 +124,7 @@ def _make_url_from_data(data):
|
||||
def _placeholders_from_device(device: ElkSystem) -> dict[str, str]:
|
||||
return {
|
||||
"mac_address": _short_mac(device.mac_address),
|
||||
"host": f"{device.ip_address}:{device.port}",
|
||||
"host": _address_from_discovery(device),
|
||||
}
|
||||
|
||||
|
||||
@@ -166,6 +181,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
for progress in self._async_in_progress():
|
||||
if progress.get("context", {}).get(CONF_HOST) == host:
|
||||
return self.async_abort(reason="already_in_progress")
|
||||
# Handled ignored case since _async_current_entries
|
||||
# is called with include_ignore=False
|
||||
self._abort_if_unique_id_configured()
|
||||
if not device.port:
|
||||
if discovered_device := await async_discover_device(self.hass, host):
|
||||
self._discovered_device = discovered_device
|
||||
@@ -255,26 +273,26 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
device = self._discovered_device
|
||||
assert device is not None
|
||||
if user_input is not None:
|
||||
user_input[CONF_ADDRESS] = f"{device.ip_address}:{device.port}"
|
||||
user_input[CONF_ADDRESS] = _address_from_discovery(device)
|
||||
if self._async_current_entries():
|
||||
user_input[CONF_PREFIX] = _short_mac(device.mac_address)
|
||||
else:
|
||||
user_input[CONF_PREFIX] = ""
|
||||
if device.port != SECURE_PORT:
|
||||
user_input[CONF_PROTOCOL] = DEFAULT_NON_SECURE_PROTOCOL
|
||||
errors, result = await self._async_create_or_error(user_input, False)
|
||||
if not errors:
|
||||
return result
|
||||
|
||||
base_schmea = BASE_SCHEMA.copy()
|
||||
if device.port == SECURE_PORT:
|
||||
base_schmea[
|
||||
vol.Required(CONF_PROTOCOL, default=DEFAULT_SECURE_PROTOCOL)
|
||||
] = vol.In(SECURE_PROTOCOLS)
|
||||
|
||||
default_proto = PORT_PROTOCOL_MAP.get(device.port, DEFAULT_SECURE_PROTOCOL)
|
||||
return self.async_show_form(
|
||||
step_id="discovered_connection",
|
||||
data_schema=vol.Schema(base_schmea),
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
**BASE_SCHEMA,
|
||||
vol.Required(CONF_PROTOCOL, default=default_proto): vol.In(
|
||||
ALL_PROTOCOLS
|
||||
),
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=_placeholders_from_device(device),
|
||||
)
|
||||
@@ -334,7 +352,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return (await self._async_create_or_error(user_input, True))[1]
|
||||
errors, result = await self._async_create_or_error(user_input, True)
|
||||
if errors:
|
||||
return self.async_abort(reason=list(errors.values())[0])
|
||||
return result
|
||||
|
||||
def _url_already_configured(self, url):
|
||||
"""See if we already have a elkm1 matching user input configured."""
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"already_configured": "An ElkM1 with this prefix is already configured",
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
"address_already_configured": "An ElkM1 with this address is already configured",
|
||||
"already_configured": "An ElkM1 with this prefix is already configured",
|
||||
"already_in_progress": "Configuration flow is already in progress",
|
||||
"cannot_connect": "Failed to connect"
|
||||
"cannot_connect": "Failed to connect",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
@@ -37,13 +39,7 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"address": "The IP address or domain or serial port if connecting via serial.",
|
||||
"device": "Device",
|
||||
"password": "Password",
|
||||
"prefix": "A unique prefix (leave blank if you only have one ElkM1).",
|
||||
"protocol": "Protocol",
|
||||
"temperature_unit": "The temperature unit ElkM1 uses.",
|
||||
"username": "Username"
|
||||
"device": "Device"
|
||||
},
|
||||
"description": "Choose a discovered system or 'Manual Entry' if no devices have been discovered.",
|
||||
"title": "Connect to Elk-M1 Control"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "emulated_kasa",
|
||||
"name": "Emulated Kasa",
|
||||
"documentation": "https://www.home-assistant.io/integrations/emulated_kasa",
|
||||
"requirements": ["sense_energy==0.10.2"],
|
||||
"requirements": ["sense_energy==0.10.4"],
|
||||
"codeowners": ["@kbickar"],
|
||||
"quality_scale": "internal",
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": [
|
||||
"home-assistant-frontend==20220301.1"
|
||||
"home-assistant-frontend==20220301.2"
|
||||
],
|
||||
"dependencies": [
|
||||
"api",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -6,6 +6,7 @@ from collections.abc import Callable
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import client_exceptions
|
||||
from aiohue import HueBridgeV1, HueBridgeV2, LinkButtonNotPressed, Unauthorized
|
||||
from aiohue.errors import AiohueException, BridgeBusy
|
||||
@@ -14,7 +15,7 @@ import async_timeout
|
||||
from homeassistant import core
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST, Platform
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import CONF_API_VERSION, DOMAIN
|
||||
@@ -116,22 +117,23 @@ class HueBridge:
|
||||
self.authorized = True
|
||||
return True
|
||||
|
||||
async def async_request_call(
|
||||
self, task: Callable, *args, allowed_errors: list[str] | None = None, **kwargs
|
||||
) -> Any:
|
||||
"""Send request to the Hue bridge, optionally omitting error(s)."""
|
||||
async def async_request_call(self, task: Callable, *args, **kwargs) -> Any:
|
||||
"""Send request to the Hue bridge."""
|
||||
try:
|
||||
return await task(*args, **kwargs)
|
||||
except AiohueException as err:
|
||||
# The (new) Hue api can be a bit fanatic with throwing errors
|
||||
# some of which we accept in certain conditions
|
||||
# handle that here. Note that these errors are strings and do not have
|
||||
# an identifier or something.
|
||||
if allowed_errors is not None and str(err) in allowed_errors:
|
||||
# The (new) Hue api can be a bit fanatic with throwing errors so
|
||||
# we have some logic to treat some responses as warning only.
|
||||
msg = f"Request failed: {err}"
|
||||
if "may not have effect" in str(err):
|
||||
# log only
|
||||
self.logger.debug("Ignored error/warning from Hue API: %s", str(err))
|
||||
self.logger.debug(msg)
|
||||
return None
|
||||
raise err
|
||||
raise HomeAssistantError(msg) from err
|
||||
except aiohttp.ClientError as err:
|
||||
raise HomeAssistantError(
|
||||
f"Request failed due connection error: {err}"
|
||||
) from err
|
||||
|
||||
async def async_reset(self) -> bool:
|
||||
"""Reset this bridge to default state.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Philips Hue",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/hue",
|
||||
"requirements": ["aiohue==4.3.0"],
|
||||
"requirements": ["aiohue==4.4.1"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Royal Philips Electronics",
|
||||
|
||||
@@ -37,13 +37,6 @@ from .helpers import (
|
||||
normalize_hue_transition,
|
||||
)
|
||||
|
||||
ALLOWED_ERRORS = [
|
||||
"device (groupedLight) has communication issues, command (on) may not have effect",
|
||||
'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',
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -175,10 +168,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
||||
and flash is None
|
||||
):
|
||||
await self.bridge.async_request_call(
|
||||
self.controller.set_state,
|
||||
id=self.resource.id,
|
||||
on=True,
|
||||
allowed_errors=ALLOWED_ERRORS,
|
||||
self.controller.set_state, id=self.resource.id, on=True
|
||||
)
|
||||
return
|
||||
|
||||
@@ -194,7 +184,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
||||
color_xy=xy_color if light.supports_color else None,
|
||||
color_temp=color_temp if light.supports_color_temperature else None,
|
||||
transition_time=transition,
|
||||
allowed_errors=ALLOWED_ERRORS,
|
||||
)
|
||||
for light in self.controller.get_lights(self.resource.id)
|
||||
]
|
||||
@@ -214,10 +203,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
||||
# To set other features, you'll have to control the attached lights
|
||||
if transition is None:
|
||||
await self.bridge.async_request_call(
|
||||
self.controller.set_state,
|
||||
id=self.resource.id,
|
||||
on=False,
|
||||
allowed_errors=ALLOWED_ERRORS,
|
||||
self.controller.set_state, id=self.resource.id, on=False
|
||||
)
|
||||
return
|
||||
|
||||
@@ -229,7 +215,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
||||
light.id,
|
||||
on=False,
|
||||
transition_time=transition,
|
||||
allowed_errors=ALLOWED_ERRORS,
|
||||
)
|
||||
for light in self.controller.get_lights(self.resource.id)
|
||||
]
|
||||
|
||||
@@ -36,11 +36,6 @@ from .helpers import (
|
||||
normalize_hue_transition,
|
||||
)
|
||||
|
||||
ALLOWED_ERRORS = [
|
||||
"device (light) has communication issues, command (on) may not have effect",
|
||||
'device (light) is "soft off", command (on) may not have effect',
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -178,7 +173,6 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
color_xy=xy_color,
|
||||
color_temp=color_temp,
|
||||
transition_time=transition,
|
||||
allowed_errors=ALLOWED_ERRORS,
|
||||
)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
@@ -198,7 +192,6 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
id=self.resource.id,
|
||||
on=False,
|
||||
transition_time=transition,
|
||||
allowed_errors=ALLOWED_ERRORS,
|
||||
)
|
||||
|
||||
async def async_set_flash(self, flash: str) -> None:
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -135,6 +135,13 @@ DEFAULT_KEEPALIVE = 60
|
||||
DEFAULT_PROTOCOL = PROTOCOL_311
|
||||
DEFAULT_TLS_PROTOCOL = "auto"
|
||||
|
||||
DEFAULT_VALUES = {
|
||||
CONF_PORT: DEFAULT_PORT,
|
||||
CONF_WILL_MESSAGE: DEFAULT_WILL,
|
||||
CONF_BIRTH_MESSAGE: DEFAULT_BIRTH,
|
||||
CONF_DISCOVERY: DEFAULT_DISCOVERY,
|
||||
}
|
||||
|
||||
ATTR_TOPIC_TEMPLATE = "topic_template"
|
||||
ATTR_PAYLOAD_TEMPLATE = "payload_template"
|
||||
|
||||
@@ -190,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),
|
||||
@@ -207,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(
|
||||
@@ -613,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}
|
||||
|
||||
|
||||
@@ -632,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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -79,7 +79,7 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
|
||||
"""Handle dhcp discovery."""
|
||||
mac = _extract_mac_from_name(discovery_info.hostname)
|
||||
mac = format_mac(discovery_info.macaddress)
|
||||
await self.async_set_unique_id(mac)
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_IP_ADDRESS: discovery_info.ip}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"dhcp": [
|
||||
{"registered_devices": true},
|
||||
{
|
||||
"hostname": "pentair: *",
|
||||
"hostname": "pentair*",
|
||||
"macaddress": "00C033*"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_CODE, CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, DOMAIN, SENSE_TIMEOUT_EXCEPTIONS
|
||||
from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, DOMAIN, SENSE_CONNECT_EXCEPTIONS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -76,7 +76,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
await self.validate_input(user_input)
|
||||
except SenseMFARequiredException:
|
||||
return await self.async_step_validation()
|
||||
except SENSE_TIMEOUT_EXCEPTIONS:
|
||||
except SENSE_CONNECT_EXCEPTIONS:
|
||||
errors["base"] = "cannot_connect"
|
||||
except SenseAuthenticationException:
|
||||
errors["base"] = "invalid_auth"
|
||||
@@ -93,7 +93,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if user_input:
|
||||
try:
|
||||
await self._gateway.validate_mfa(user_input[CONF_CODE])
|
||||
except SENSE_TIMEOUT_EXCEPTIONS:
|
||||
except SENSE_CONNECT_EXCEPTIONS:
|
||||
errors["base"] = "cannot_connect"
|
||||
except SenseAuthenticationException:
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
import asyncio
|
||||
import socket
|
||||
|
||||
from sense_energy import SenseAPITimeoutException
|
||||
from sense_energy.sense_exceptions import SenseWebsocketException
|
||||
from sense_energy import (
|
||||
SenseAPIException,
|
||||
SenseAPITimeoutException,
|
||||
SenseWebsocketException,
|
||||
)
|
||||
|
||||
DOMAIN = "sense"
|
||||
DEFAULT_TIMEOUT = 10
|
||||
@@ -40,6 +43,11 @@ ICON = "mdi:flash"
|
||||
|
||||
SENSE_TIMEOUT_EXCEPTIONS = (asyncio.TimeoutError, SenseAPITimeoutException)
|
||||
SENSE_EXCEPTIONS = (socket.gaierror, SenseWebsocketException)
|
||||
SENSE_CONNECT_EXCEPTIONS = (
|
||||
asyncio.TimeoutError,
|
||||
SenseAPITimeoutException,
|
||||
SenseAPIException,
|
||||
)
|
||||
|
||||
MDI_ICONS = {
|
||||
"ac": "air-conditioner",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "sense",
|
||||
"name": "Sense",
|
||||
"documentation": "https://www.home-assistant.io/integrations/sense",
|
||||
"requirements": ["sense_energy==0.10.2"],
|
||||
"requirements": ["sense_energy==0.10.4"],
|
||||
"codeowners": ["@kbickar"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -9,22 +9,13 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_KEY, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN
|
||||
|
||||
|
||||
@callback
|
||||
def solaredge_entries(hass: HomeAssistant):
|
||||
"""Return the site_ids for the domain."""
|
||||
return {
|
||||
(entry.data[CONF_SITE_ID])
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
}
|
||||
|
||||
|
||||
class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow."""
|
||||
|
||||
@@ -34,9 +25,18 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Initialize the config flow."""
|
||||
self._errors = {}
|
||||
|
||||
@callback
|
||||
def _async_current_site_ids(self) -> set[str]:
|
||||
"""Return the site_ids for the domain."""
|
||||
return {
|
||||
entry.data[CONF_SITE_ID]
|
||||
for entry in self._async_current_entries(include_ignore=False)
|
||||
if CONF_SITE_ID in entry.data
|
||||
}
|
||||
|
||||
def _site_in_configuration_exists(self, site_id: str) -> bool:
|
||||
"""Return True if site_id exists in configuration."""
|
||||
return site_id in solaredge_entries(self.hass)
|
||||
return site_id in self._async_current_site_ids()
|
||||
|
||||
def _check_site(self, site_id: str, api_key: str) -> bool:
|
||||
"""Check if we can connect to the soleredge api service."""
|
||||
|
||||
@@ -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)},
|
||||
|
||||
@@ -63,6 +63,7 @@ from .media import SonosMedia
|
||||
from .statistics import ActivityStatistics, EventStatistics
|
||||
|
||||
NEVER_TIME = -1200.0
|
||||
RESUB_COOLDOWN_SECONDS = 10.0
|
||||
EVENT_CHARGING = {
|
||||
"CHARGING": True,
|
||||
"NOT_CHARGING": False,
|
||||
@@ -126,6 +127,7 @@ class SonosSpeaker:
|
||||
self._last_event_cache: dict[str, Any] = {}
|
||||
self.activity_stats: ActivityStatistics = ActivityStatistics(self.zone_name)
|
||||
self.event_stats: EventStatistics = EventStatistics(self.zone_name)
|
||||
self._resub_cooldown_expires_at: float | None = None
|
||||
|
||||
# Scheduled callback handles
|
||||
self._poll_timer: Callable | None = None
|
||||
@@ -502,6 +504,16 @@ class SonosSpeaker:
|
||||
@callback
|
||||
def speaker_activity(self, source):
|
||||
"""Track the last activity on this speaker, set availability and resubscribe."""
|
||||
if self._resub_cooldown_expires_at:
|
||||
if time.monotonic() < self._resub_cooldown_expires_at:
|
||||
_LOGGER.debug(
|
||||
"Activity on %s from %s while in cooldown, ignoring",
|
||||
self.zone_name,
|
||||
source,
|
||||
)
|
||||
return
|
||||
self._resub_cooldown_expires_at = None
|
||||
|
||||
_LOGGER.debug("Activity on %s from %s", self.zone_name, source)
|
||||
self._last_activity = time.monotonic()
|
||||
self.activity_stats.activity(source, self._last_activity)
|
||||
@@ -542,6 +554,10 @@ class SonosSpeaker:
|
||||
if not self.available:
|
||||
return
|
||||
|
||||
if self._resub_cooldown_expires_at is None and not self.hass.is_stopping:
|
||||
self._resub_cooldown_expires_at = time.monotonic() + RESUB_COOLDOWN_SECONDS
|
||||
_LOGGER.debug("Starting resubscription cooldown for %s", self.zone_name)
|
||||
|
||||
self.available = False
|
||||
self.async_write_entity_states()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "synology_dsm",
|
||||
"name": "Synology DSM",
|
||||
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
|
||||
"requirements": ["py-synologydsm-api==1.0.6"],
|
||||
"requirements": ["py-synologydsm-api==1.0.7"],
|
||||
"codeowners": ["@hacf-fr", "@Quentame", "@mib1185"],
|
||||
"config_flow": true,
|
||||
"ssdp": [
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -51,7 +51,7 @@ DEFAULT_URL = "ws://localhost:3000"
|
||||
TITLE = "Z-Wave JS"
|
||||
|
||||
ADDON_SETUP_TIMEOUT = 5
|
||||
ADDON_SETUP_TIMEOUT_ROUNDS = 4
|
||||
ADDON_SETUP_TIMEOUT_ROUNDS = 40
|
||||
CONF_EMULATE_HARDWARE = "emulate_hardware"
|
||||
CONF_LOG_LEVEL = "log_level"
|
||||
SERVER_VERSION_TIMEOUT = 10
|
||||
|
||||
@@ -7,7 +7,7 @@ from .backports.enum import StrEnum
|
||||
|
||||
MAJOR_VERSION: Final = 2022
|
||||
MINOR_VERSION: Final = 3
|
||||
PATCH_VERSION: Final = "4"
|
||||
PATCH_VERSION: Final = "8"
|
||||
__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)
|
||||
|
||||
@@ -81,7 +81,7 @@ DHCP: list[dict[str, str | bool]] = [
|
||||
{'domain': 'samsungtv', 'macaddress': '4844F7*'},
|
||||
{'domain': 'samsungtv', 'macaddress': '8CEA48*'},
|
||||
{'domain': 'screenlogic', 'registered_devices': True},
|
||||
{'domain': 'screenlogic', 'hostname': 'pentair: *', 'macaddress': '00C033*'},
|
||||
{'domain': 'screenlogic', 'hostname': 'pentair*', 'macaddress': '00C033*'},
|
||||
{'domain': 'sense', 'hostname': 'sense-*', 'macaddress': '009D6B*'},
|
||||
{'domain': 'sense', 'hostname': 'sense-*', 'macaddress': 'DCEFCA*'},
|
||||
{'domain': 'sense', 'hostname': 'sense-*', 'macaddress': 'A4D578*'},
|
||||
|
||||
@@ -218,9 +218,12 @@ def async_prepare_call_from_config(
|
||||
|
||||
if CONF_ENTITY_ID in target:
|
||||
registry = entity_registry.async_get(hass)
|
||||
target[CONF_ENTITY_ID] = entity_registry.async_resolve_entity_ids(
|
||||
registry, cv.comp_entity_ids_or_uuids(target[CONF_ENTITY_ID])
|
||||
)
|
||||
entity_ids = cv.comp_entity_ids_or_uuids(target[CONF_ENTITY_ID])
|
||||
if entity_ids not in (ENTITY_MATCH_ALL, ENTITY_MATCH_NONE):
|
||||
entity_ids = entity_registry.async_resolve_entity_ids(
|
||||
registry, entity_ids
|
||||
)
|
||||
target[CONF_ENTITY_ID] = entity_ids
|
||||
except TemplateError as ex:
|
||||
raise HomeAssistantError(
|
||||
f"Error rendering service target template: {ex}"
|
||||
|
||||
@@ -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.1
|
||||
home-assistant-frontend==20220301.2
|
||||
httpx==0.21.3
|
||||
ifaddr==0.1.7
|
||||
jinja2==3.0.3
|
||||
@@ -95,3 +95,11 @@ 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
|
||||
|
||||
# Required for compatibility with typer, used by pyunifiprotect integration
|
||||
# https://github.com/tiangolo/typer/pull/375
|
||||
click<=8.0.4
|
||||
|
||||
@@ -191,7 +191,7 @@ aiohomekit==0.7.16
|
||||
aiohttp_cors==0.7.0
|
||||
|
||||
# homeassistant.components.hue
|
||||
aiohue==4.3.0
|
||||
aiohue==4.4.1
|
||||
|
||||
# homeassistant.components.homewizard
|
||||
aiohwenergy==0.8.0
|
||||
@@ -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.1
|
||||
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
|
||||
@@ -1338,7 +1338,7 @@ py-nightscout==1.2.2
|
||||
py-schluter==0.1.7
|
||||
|
||||
# homeassistant.components.synology_dsm
|
||||
py-synologydsm-api==1.0.6
|
||||
py-synologydsm-api==1.0.7
|
||||
|
||||
# homeassistant.components.zabbix
|
||||
py-zabbix==1.1.7
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -2179,7 +2179,7 @@ sense-hat==2.2.0
|
||||
|
||||
# homeassistant.components.emulated_kasa
|
||||
# homeassistant.components.sense
|
||||
sense_energy==0.10.2
|
||||
sense_energy==0.10.4
|
||||
|
||||
# homeassistant.components.sentry
|
||||
sentry-sdk==1.5.5
|
||||
|
||||
@@ -141,7 +141,7 @@ aiohomekit==0.7.16
|
||||
aiohttp_cors==0.7.0
|
||||
|
||||
# homeassistant.components.hue
|
||||
aiohue==4.3.0
|
||||
aiohue==4.4.1
|
||||
|
||||
# homeassistant.components.homewizard
|
||||
aiohwenergy==0.8.0
|
||||
@@ -553,13 +553,13 @@ hole==0.7.0
|
||||
holidays==0.13
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20220301.1
|
||||
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
|
||||
@@ -842,7 +842,7 @@ py-melissa-climate==2.1.4
|
||||
py-nightscout==1.2.2
|
||||
|
||||
# homeassistant.components.synology_dsm
|
||||
py-synologydsm-api==1.0.6
|
||||
py-synologydsm-api==1.0.7
|
||||
|
||||
# homeassistant.components.seventeentrack
|
||||
py17track==2021.12.2
|
||||
@@ -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
|
||||
@@ -1301,7 +1301,7 @@ radios==0.1.1
|
||||
regenmaschine==2022.01.0
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.1.9
|
||||
renault-api==0.1.10
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==5.2
|
||||
@@ -1344,7 +1344,7 @@ screenlogicpy==0.5.4
|
||||
|
||||
# homeassistant.components.emulated_kasa
|
||||
# homeassistant.components.sense
|
||||
sense_energy==0.10.2
|
||||
sense_energy==0.10.4
|
||||
|
||||
# homeassistant.components.sentry
|
||||
sentry-sdk==1.5.5
|
||||
|
||||
@@ -124,6 +124,14 @@ 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
|
||||
|
||||
# Required for compatibility with typer, used by pyunifiprotect integration
|
||||
# https://github.com/tiangolo/typer/pull/375
|
||||
click<=8.0.4
|
||||
"""
|
||||
|
||||
IGNORE_PRE_COMMIT_HOOK_ID = (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = homeassistant
|
||||
version = 2022.3.4
|
||||
version = 2022.3.8
|
||||
author = The Home Assistant Authors
|
||||
author_email = hello@home-assistant.io
|
||||
license = Apache-2.0
|
||||
|
||||
@@ -1066,3 +1066,22 @@ async def test_option_start_off(hass):
|
||||
assert result2["type"] == "create_entry"
|
||||
|
||||
assert config_entry.options[CONF_START_OFF]
|
||||
|
||||
|
||||
async def test_zeroconf_rejects_ipv6(hass):
|
||||
"""Test zeroconf discovery rejects ipv6."""
|
||||
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",
|
||||
port=None,
|
||||
type="_touch-able._tcp.local.",
|
||||
name="dmapid._touch-able._tcp.local.",
|
||||
properties={"CtlN": "Apple TV"},
|
||||
),
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "ipv6_not_supported"
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -9,6 +9,7 @@ MOCK_IP_ADDRESS = "127.0.0.1"
|
||||
MOCK_MAC = "aa:bb:cc:dd:ee:ff"
|
||||
ELK_DISCOVERY = ElkSystem(MOCK_MAC, MOCK_IP_ADDRESS, 2601)
|
||||
ELK_NON_SECURE_DISCOVERY = ElkSystem(MOCK_MAC, MOCK_IP_ADDRESS, 2101)
|
||||
ELK_DISCOVERY_NON_STANDARD_PORT = ElkSystem(MOCK_MAC, MOCK_IP_ADDRESS, 444)
|
||||
|
||||
|
||||
def mock_elk(invalid_auth=None, sync_complete=None, exception=None):
|
||||
|
||||
@@ -12,6 +12,7 @@ from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM
|
||||
|
||||
from . import (
|
||||
ELK_DISCOVERY,
|
||||
ELK_DISCOVERY_NON_STANDARD_PORT,
|
||||
ELK_NON_SECURE_DISCOVERY,
|
||||
MOCK_IP_ADDRESS,
|
||||
MOCK_MAC,
|
||||
@@ -24,9 +25,32 @@ from tests.common import MockConfigEntry
|
||||
|
||||
DHCP_DISCOVERY = dhcp.DhcpServiceInfo(MOCK_IP_ADDRESS, "", MOCK_MAC)
|
||||
ELK_DISCOVERY_INFO = asdict(ELK_DISCOVERY)
|
||||
ELK_DISCOVERY_INFO_NON_STANDARD_PORT = asdict(ELK_DISCOVERY_NON_STANDARD_PORT)
|
||||
|
||||
MODULE = "homeassistant.components.elkm1"
|
||||
|
||||
|
||||
async def test_discovery_ignored_entry(hass):
|
||||
"""Test we abort on ignored entry."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_HOST: f"elks://{MOCK_IP_ADDRESS}"},
|
||||
unique_id="aa:bb:cc:dd:ee:ff",
|
||||
source=config_entries.SOURCE_IGNORE,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with _patch_discovery(), _patch_elk():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=ELK_DISCOVERY_INFO,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] == RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_form_user_with_secure_elk_no_discovery(hass):
|
||||
"""Test we can setup a secure elk."""
|
||||
|
||||
@@ -301,7 +325,7 @@ async def test_form_user_with_secure_elk_with_discovery(hass):
|
||||
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",
|
||||
@@ -801,6 +825,102 @@ async def test_form_import_device_discovered(hass):
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_import_non_secure_device_discovered(hass):
|
||||
"""Test we can import non-secure with discovery."""
|
||||
|
||||
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:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data={
|
||||
"host": "elk://127.0.0.1:2101",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"auto_configure": True,
|
||||
"prefix": "ohana",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == "ohana"
|
||||
assert result["result"].unique_id == MOCK_MAC
|
||||
assert result["data"] == {
|
||||
"auto_configure": True,
|
||||
"host": "elk://127.0.0.1:2101",
|
||||
"password": "",
|
||||
"prefix": "ohana",
|
||||
"username": "",
|
||||
}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_import_non_secure_non_stanadard_port_device_discovered(hass):
|
||||
"""Test we can import non-secure non standard port with discovery."""
|
||||
|
||||
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:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data={
|
||||
"host": "elk://127.0.0.1:444",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"auto_configure": True,
|
||||
"prefix": "ohana",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == "ohana"
|
||||
assert result["result"].unique_id == MOCK_MAC
|
||||
assert result["data"] == {
|
||||
"auto_configure": True,
|
||||
"host": "elk://127.0.0.1:444",
|
||||
"password": "",
|
||||
"prefix": "ohana",
|
||||
"username": "",
|
||||
}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_import_non_secure_device_discovered_invalid_auth(hass):
|
||||
"""Test we abort import with invalid auth."""
|
||||
|
||||
mocked_elk = mock_elk(invalid_auth=True, sync_complete=False)
|
||||
with _patch_discovery(), _patch_elk(elk=mocked_elk):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data={
|
||||
"host": "elks://127.0.0.1",
|
||||
"username": "invalid",
|
||||
"password": "",
|
||||
"auto_configure": False,
|
||||
"prefix": "ohana",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "invalid_auth"
|
||||
|
||||
|
||||
async def test_form_import_existing(hass):
|
||||
"""Test we abort on existing import."""
|
||||
config_entry = MockConfigEntry(
|
||||
@@ -978,7 +1098,52 @@ async def test_discovered_by_discovery(hass):
|
||||
assert result2["title"] == "ElkM1 ddeeff"
|
||||
assert result2["data"] == {
|
||||
"auto_configure": True,
|
||||
"host": "elks://127.0.0.1:2601",
|
||||
"host": "elks://127.0.0.1",
|
||||
"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_discovered_by_discovery_non_standard_port(hass):
|
||||
"""Test we can setup when discovered from discovery with a non-standard port."""
|
||||
|
||||
with _patch_discovery(), _patch_elk():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=ELK_DISCOVERY_INFO_NON_STANDARD_PORT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "discovered_connection"
|
||||
assert result["errors"] == {}
|
||||
|
||||
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"],
|
||||
{
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "ElkM1 ddeeff"
|
||||
assert result2["data"] == {
|
||||
"auto_configure": True,
|
||||
"host": "elks://127.0.0.1:444",
|
||||
"password": "test-password",
|
||||
"prefix": "",
|
||||
"username": "test-username",
|
||||
@@ -1042,7 +1207,7 @@ async def test_discovered_by_dhcp_udp_responds(hass):
|
||||
assert result2["title"] == "ElkM1 ddeeff"
|
||||
assert result2["data"] == {
|
||||
"auto_configure": True,
|
||||
"host": "elks://127.0.0.1:2601",
|
||||
"host": "elks://127.0.0.1",
|
||||
"password": "test-password",
|
||||
"prefix": "",
|
||||
"username": "test-username",
|
||||
@@ -1077,8 +1242,7 @@ async def test_discovered_by_dhcp_udp_responds_with_nonsecure_port(hass):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"protocol": "non-secure",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@@ -1087,10 +1251,10 @@ async def test_discovered_by_dhcp_udp_responds_with_nonsecure_port(hass):
|
||||
assert result2["title"] == "ElkM1 ddeeff"
|
||||
assert result2["data"] == {
|
||||
"auto_configure": True,
|
||||
"host": "elk://127.0.0.1:2101",
|
||||
"password": "test-password",
|
||||
"host": "elk://127.0.0.1",
|
||||
"password": "",
|
||||
"prefix": "",
|
||||
"username": "test-username",
|
||||
"username": "",
|
||||
}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
@@ -1125,10 +1289,7 @@ async def test_discovered_by_dhcp_udp_responds_existing_config_entry(hass):
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
},
|
||||
{"username": "test-username", "password": "test-password"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -1136,7 +1297,7 @@ async def test_discovered_by_dhcp_udp_responds_existing_config_entry(hass):
|
||||
assert result2["title"] == "ElkM1 ddeeff"
|
||||
assert result2["data"] == {
|
||||
"auto_configure": True,
|
||||
"host": "elks://127.0.0.1:2601",
|
||||
"host": "elks://127.0.0.1",
|
||||
"password": "test-password",
|
||||
"prefix": "ddeeff",
|
||||
"username": "test-username",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -60,7 +60,7 @@ def create_mock_bridge(hass, api_version=1):
|
||||
|
||||
bridge.async_initialize_bridge = async_initialize_bridge
|
||||
|
||||
async def async_request_call(task, *args, allowed_errors=None, **kwargs):
|
||||
async def async_request_call(task, *args, **kwargs):
|
||||
await task(*args, **kwargs)
|
||||
|
||||
bridge.async_request_call = async_request_call
|
||||
|
||||
@@ -460,7 +460,7 @@
|
||||
"model_id": "BSB002",
|
||||
"product_archetype": "bridge_v2",
|
||||
"product_name": "Philips hue",
|
||||
"software_version": "1.48.1948086000"
|
||||
"software_version": "1.50.1950111030"
|
||||
},
|
||||
"services": [
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from sense_energy import (
|
||||
SenseAPIException,
|
||||
SenseAPITimeoutException,
|
||||
SenseAuthenticationException,
|
||||
SenseMFARequiredException,
|
||||
@@ -189,7 +190,7 @@ async def test_form_mfa_required_exception(hass, mock_sense):
|
||||
assert result3["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass):
|
||||
async def test_form_timeout(hass):
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
@@ -208,6 +209,25 @@ async def test_form_cannot_connect(hass):
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass):
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"sense_energy.ASyncSenseable.authenticate",
|
||||
side_effect=SenseAPIException,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"timeout": "6", "email": "test-email", "password": "test-password"},
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_unknown_exception(hass):
|
||||
"""Test we handle unknown error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
||||
@@ -6,7 +6,7 @@ from requests.exceptions import ConnectTimeout, HTTPError
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_USER
|
||||
from homeassistant.const import CONF_API_KEY, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
@@ -66,6 +66,31 @@ async def test_abort_if_already_setup(hass: HomeAssistant, test_api: str) -> Non
|
||||
assert result.get("errors") == {CONF_SITE_ID: "already_configured"}
|
||||
|
||||
|
||||
async def test_ignored_entry_does_not_cause_error(
|
||||
hass: HomeAssistant, test_api: str
|
||||
) -> None:
|
||||
"""Test an ignored entry does not cause and error and we can still create an new entry."""
|
||||
MockConfigEntry(
|
||||
domain="solaredge",
|
||||
data={CONF_NAME: DEFAULT_NAME, CONF_API_KEY: API_KEY},
|
||||
source=SOURCE_IGNORE,
|
||||
).add_to_hass(hass)
|
||||
|
||||
# user: Should fail, same SITE_ID
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "test"
|
||||
|
||||
data = result["data"]
|
||||
assert data
|
||||
assert data[CONF_SITE_ID] == SITE_ID
|
||||
assert data[CONF_API_KEY] == "test"
|
||||
|
||||
|
||||
async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None:
|
||||
"""Test the _site_in_configuration_exists method."""
|
||||
|
||||
|
||||
@@ -397,6 +397,22 @@ async def test_service_call_entry_id(hass):
|
||||
assert dict(calls[0].data) == {"entity_id": ["hello.world"]}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target", ("all", "none"))
|
||||
async def test_service_call_all_none(hass, target):
|
||||
"""Test service call targeting all."""
|
||||
calls = async_mock_service(hass, "test_domain", "test_service")
|
||||
|
||||
config = {
|
||||
"service": "test_domain.test_service",
|
||||
"target": {"entity_id": target},
|
||||
}
|
||||
|
||||
await service.async_call_from_config(hass, config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert dict(calls[0].data) == {"entity_id": target}
|
||||
|
||||
|
||||
async def test_extract_entity_ids(hass):
|
||||
"""Test extract_entity_ids method."""
|
||||
hass.states.async_set("light.Bowl", STATE_ON)
|
||||
|
||||
Reference in New Issue
Block a user