Compare commits

...

49 Commits

Author SHA1 Message Date
Franck Nijhof
be39a3cee0 Merge pull request #68825 from home-assistant/rc 2022-03-29 15:30:51 +02:00
epenet
826423d8ed Cleanup package constraints (#68833)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2022-03-29 14:42:40 +02:00
epenet
99c0ef9ab8 Pin click to fix typer issue (#68808)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2022-03-29 10:21:29 +02:00
Paulus Schoutsen
4041dd61c5 Bumped version to 2022.3.8 2022-03-29 00:11:29 -07:00
Keilin Bickar
fe36ff5fa9 Update sense library to 0.10.4 (#68816) 2022-03-29 00:11:12 -07:00
J. Nick Koston
9acd1471a0 Fix ignoring elkm1 discovery (#68750) 2022-03-29 00:11:11 -07:00
Martin Hjelmare
d1a6eb55b1 Increase zwave_js add-on start attempts before timeout (#68736) 2022-03-29 00:11:10 -07:00
J. Nick Koston
112732c673 Add option to connect to elkm1 non-secure when secure is discovered (#68735)
Co-authored-by: Glenn Waters <glenn@watrs.ca>
2022-03-29 00:11:09 -07:00
J. Nick Koston
45638c78ae Ensure solaredge can still be setup with an ignored entry (#68688) 2022-03-29 00:11:08 -07:00
J. Nick Koston
d5e80ed2a5 Fix screenlogic to get the macaddress from discovery (#68687) 2022-03-29 00:11:07 -07:00
kevdliu
9b3c3232be Revert "Take Abode camera snapshot before fetching latest image" (#68626) 2022-03-29 00:11:06 -07:00
Paulus Schoutsen
feea5af3d0 Merge pull request #68592 from home-assistant/rc 2022-03-23 21:04:33 -07:00
Michael
be5d816fbe Bump py-synologydsm-api to 1.0.7 (#68584) 2022-03-23 18:08:01 -07:00
Paulus Schoutsen
911de94345 Bumped version to 2022.3.7 2022-03-23 15:47:05 -07:00
Marcel van der Veldt
53fa6c138a Bump aiohue to version 4.4.1 (#68579) 2022-03-23 15:46:53 -07:00
Marcel van der Veldt
85e6b3950c Bump aiohue to 4.4.0 (#68556) 2022-03-23 15:46:52 -07:00
J. Nick Koston
6fd4355314 Filter IPv6 addresses from AppleTV zeroconf discovery (#68530) 2022-03-23 15:46:14 -07:00
Marcel van der Veldt
050600375d Simplify Hue error handling a bit (#68529) 2022-03-23 15:46:13 -07:00
Paulus Schoutsen
1114877062 Hue handle HTTP errors (#68396) 2022-03-23 15:46:12 -07:00
jjlawren
454cb44ee8 Add cooldown timer before Sonos resubscriptions (#68521) 2022-03-23 15:45:21 -07:00
Keilin Bickar
9636435ff2 Add support for general API exception in Sense integration (#68517) 2022-03-23 15:45:20 -07:00
Erik Montnemery
f85781dc51 Fix targeting all or none entities in service calls (#68513)
* Fix targeting all or none entities in service calls

* Add test
2022-03-23 15:45:10 -07:00
Paulus Schoutsen
49edaf2f68 Merge pull request #68493 from home-assistant/rc 2022-03-21 21:57:00 -07:00
Paulus Schoutsen
2be9798fb8 Bumped version to 2022.3.6 2022-03-21 20:42:13 -07:00
J. Nick Koston
3bf0a64e21 Fix tplink color temp conversion (#68484) 2022-03-21 20:42:09 -07:00
Paulus Schoutsen
23e9aa6ad2 Handle Hue discovery errors (#68392) 2022-03-21 20:42:08 -07:00
J. Nick Koston
a8e1f57058 Filter IPv6 addreses from enphase_envoy discovery (#68362) 2022-03-21 20:42:07 -07:00
Marcel van der Veldt
caee432901 Hue integration: update errors that should be supressed (#68337) 2022-03-21 20:42:07 -07:00
epenet
df5c09e483 Bump renault-api to 0.1.10 (#68260)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2022-03-21 20:42:06 -07:00
Franck Nijhof
38eb007f63 Update opensensemap-api to 0.2.0 (#68193) 2022-03-21 20:41:31 -07:00
Marc Mueller
7dd9bfa92f Fix point by adding authlib constraint (#68176)
* Fix point by pinning authlib

* Use constraint
2022-03-21 20:41:31 -07:00
Marc Mueller
54b7f13a54 Add missing await [velbus] (#68153) 2022-03-21 20:41:30 -07:00
Michael
774f2b9b82 Respect disable_new_entities for new device_tracker entities (#68148) 2022-03-21 20:41:29 -07:00
Antonio Larrosa
bc14385317 Fix finding matrix room that is already joined (#67967)
After some debugging, it seems room.canonical_alias contains the
room alias that matches the room_id_or_alias value but is not
contained in room.aliases (which is empty). As a result, the
matrix component thought the room wasn't alread joined, joins
again, and this replaces the previous room which had the listener.
This resulted in the component callback not being called for new
messages in the room.

This fixes #66372
2022-03-21 20:41:28 -07:00
Numa Perez
9352ed1286 Fix lyric climate (#67018)
* Fixed the issues related to auto mode

I was having the same issues as described in #63403, specifically, the error stating that Mode 7 is not valid, only Heat, Cool, Off when trying to do anything while the thermostat is set to Auto. This error originates with the way the Lyric API handles the modes. Basically, when one queries the changeableValues dict, you get a mode=Auto, as well as a heatCoolMode, which is set to either Heat, Cool, Off. Per the documentation, heatCoolMode contains the "heat cool mode when system switch is in Auto mode". It would make sense that when changing the thermostat settings, mode=Auto should be valid, but it's not. The way the API understands that the mode should be set to Auto when changing the thermostat settings is by setting the autoChangeoverActive variable to true, not the mode itself. This require changes in the async_set_hvac_mode, async_set_temperature, and async_set_preset_mode functions. Related to this issue, I got rid of the references to hasDualSetpointStatus, as it seems that it always remains false in the API, even when the mode is set to auto, so again, the key variable for this is autoChangeoverActive.

While I was working on this I also noticed another issue. The support flag SUPPORT_TARGET_TEMPERATURE_RANGE had not been included, which did not allow for the temperature range to be available, thus invalidating the target_temperature_low and target_temperature_high functions. I added this flag and sorted out which set point (heat vs cool) should be called for each of them so things work as expected in Lovelace. I have tested all of these functionalities and they all work great on my end, so I thought I'd share.

* Update climate.py

* Update climate.py

Fixed two additional issues: 1) When the system is turned off from Auto, the heatCoolMode variable becomes 'Off', so when you try to restart the system back to Auto, nothing happens. 2) I now prevent the async_set_temperature function from being called with a new set point when the system is Off.
All changes tested and functional.

* Update climate.py

* Update climate.py

Return SUPPORT_PRESET_MODE flag only for LCC models (i.e. they have the "thermostatSetpointStatus" variable defined). TCC models do not support this feature

* Update climate.py

After playing with the official Honeywell API, I realized it doesn't like to received commands with missing data, i.e., it always wants to get a mode, coolSetpoint, heatSetpoint, and autoChangeoverActive variables. This was causing some random issues with changing modes, especially from coming from off, so I modified the async_set_temperature, and async_set_hvac_mode fuctions to always send all pertinent variables.

* Update climate.py

* Update climate.py

* Update climate.py

* Update climate.py

* Clean code and test everything

Alright, sorry for the multiple commits, fixing this properly took a fair bit of testing. I went ahead and cleaned up the code and made the following big picture changes: 
1) The integration now supports the Auto mode appropriately, to include the temperature range. 
2) There's a bug that actually manifests when using the native app. When the system is 'Off' and you try to turn it on to 'Auto', it will turn on briefly but will go back to 'Off' after a few seconds. When checking the web api, this appears to be related to the fact that the heatCoolMode variable seems to continue to store 'Off', even if the mode accurately displays 'Auto', and the autoChangeoverActive=True. So to overcome that inherent limitation, when the system is 'Off' and the user turns it to 'Auto', I first turn it to Heat, wait 3 seconds, and then turn it to 'Auto', which seems to work well.

* Update climate.py

* Fixed errors

* Fixed comments that were resulting in error.

* Update climate.py

* Update homeassistant/components/lyric/climate.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/lyric/climate.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update climate.py

I removed a blank line in 268 and another one at the end of the document. I also fixed the outdents of await commands after the _LOGGER.error calls, not sure what else may be driving the flake8 and black errors. Any guidance is much appreciated @MartinHjelmare

* Update climate.py

* Update climate.py

corrected some indents that I think were the culprit of the flake8 errors

* Update climate.py

I used VS Code to fix locate the flake8 errors. I ran black on it, so I'm hoping that will fix the last lingering black error.

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-03-21 20:41:28 -07:00
epenet
f7fd781a27 Fix TypeError in SamsungTV (#68235)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2022-03-18 09:26:19 +01:00
Paulus Schoutsen
b1153720c0 Merge pull request #68159 from home-assistant/rc 2022-03-15 00:11:48 -07:00
Franck Nijhof
27d275e6f7 Fix Efergy tests (#68086) 2022-03-14 23:22:59 -07:00
Paulus Schoutsen
1191c095f8 Bumped version to 2022.3.5 2022-03-14 22:46:08 -07:00
Jan Bouwhuis
b86d115764 Fix MQTT false positive deprecation warnings (#68117) 2022-03-14 22:45:53 -07:00
Frank
479a230da7 Update home_connect to 0.7.0 (#68089) 2022-03-14 22:45:52 -07:00
J. Nick Koston
7aecd69e3b Bump pyisy to 3.0.5 (#68069)
* Bump pyisy to 3.0.4

- Fixes #66003

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

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

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

View File

@@ -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:

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}.", "")

View File

@@ -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.",

View File

@@ -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"
}
}

View File

@@ -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

View File

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

View File

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

View File

@@ -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."""

View File

@@ -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",

View File

@@ -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"

View File

@@ -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",

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

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

View File

@@ -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.

View File

@@ -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

View File

@@ -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",

View File

@@ -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)
]

View File

@@ -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:

View File

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

View File

@@ -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()

View File

@@ -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
)

View File

@@ -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(

View File

@@ -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()

View File

@@ -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"]

View File

@@ -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")

View File

@@ -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"

View File

@@ -10,7 +10,7 @@ from samsungctl import Remote
from samsungctl.exceptions import AccessDenied, ConnectionClosed, UnhandledResponse
from samsungtvws import SamsungTVWS
from samsungtvws.exceptions import ConnectionFailure, HttpApiError
from websocket import WebSocketException
from websocket import WebSocketException, WebSocketTimeoutException
from homeassistant.const import (
CONF_HOST,
@@ -318,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"])

View File

@@ -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}

View File

@@ -8,7 +8,7 @@
"dhcp": [
{"registered_devices": true},
{
"hostname": "pentair: *",
"hostname": "pentair*",
"macaddress": "00C033*"
}
],

View File

@@ -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"

View File

@@ -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",

View File

@@ -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": [

View File

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

View File

@@ -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."""

View File

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

View File

@@ -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()

View File

@@ -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": [

View File

@@ -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

View File

@@ -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])

View File

@@ -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

View File

@@ -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)

View File

@@ -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*'},

View File

@@ -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}"

View File

@@ -14,7 +14,7 @@ certifi>=2021.5.30
ciso8601==2.2.0
cryptography==35.0.0
hass-nabucasa==0.54.0
home-assistant-frontend==20220301.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

View File

@@ -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

View File

@@ -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

View File

@@ -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 = (

View File

@@ -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

View File

@@ -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"

View File

@@ -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."""

View File

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

View File

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

View File

@@ -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):

View File

@@ -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",

View File

@@ -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:

View File

@@ -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

View File

@@ -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": [
{

View File

@@ -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(

View File

@@ -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."""

View File

@@ -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)