Merge pull request #62366 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2021-12-20 19:58:40 -08:00 committed by GitHub
commit 20a1bc710e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 434 additions and 167 deletions

View File

@ -155,10 +155,15 @@ jobs:
key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
steps.generate-python-key.outputs.key }}
restore-keys: |
${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-
# Temporary disabling the restore of environments when bumping
# a dependency. It seems that we are experiencing issues with
# restoring environments in GitHub Actions, although unclear why.
# First attempt: https://github.com/home-assistant/core/pull/62383
#
# restore-keys: |
# ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-
# ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-
# ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@ -517,10 +522,15 @@ jobs:
key: >-
${{ runner.os }}-${{ matrix.python-version }}-${{
steps.generate-python-key.outputs.key }}
restore-keys: |
${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}-
${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-
${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-
# Temporary disabling the restore of environments when bumping
# a dependency. It seems that we are experiencing issues with
# restoring environments in GitHub Actions, although unclear why.
# First attempt: https://github.com/home-assistant/core/pull/62383
#
# restore-keys: |
# ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}-
# ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-
# ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-
- name: Create full Python ${{ matrix.python-version }} virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |

View File

@ -2,7 +2,7 @@
"domain": "bmw_connected_drive",
"name": "BMW Connected Drive",
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"requirements": ["bimmer_connected==0.8.5"],
"requirements": ["bimmer_connected==0.8.7"],
"codeowners": ["@gerard33", "@rikroe"],
"config_flow": true,
"iot_class": "cloud_polling"

View File

@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try:
async with async_timeout.timeout(10):
things = await bapi.async_get_things(force=True)
return {thing.SERIAL: thing for thing in things}
return {thing.serial: thing for thing in things}
except ServerDisconnectedError as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
except ClientResponseError as err:

View File

@ -100,7 +100,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
self._remove_update_listener = None
self._attr_name = self._thing.NAME
self._attr_name = self._thing.name
self._attr_device_class = DEVICE_CLASS_SHADE
self._attr_supported_features = COVER_FEATURES
self._attr_attribution = ATTRIBUTION
@ -109,8 +109,8 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
name=self._attr_name,
via_device=(DOMAIN, self._entry_id),
manufacturer="Brunt",
sw_version=self._thing.FW_VERSION,
model=self._thing.MODEL,
sw_version=self._thing.fw_version,
model=self._thing.model,
)
async def async_added_to_hass(self) -> None:
@ -127,8 +127,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
None is unknown, 0 is closed, 100 is fully open.
"""
pos = self.coordinator.data[self.unique_id].currentPosition
return int(pos) if pos is not None else None
return self.coordinator.data[self.unique_id].current_position
@property
def request_cover_position(self) -> int | None:
@ -139,8 +138,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
to Brunt, at times there is a diff of 1 to current
None is unknown, 0 is closed, 100 is fully open.
"""
pos = self.coordinator.data[self.unique_id].requestPosition
return int(pos) if pos is not None else None
return self.coordinator.data[self.unique_id].request_position
@property
def move_state(self) -> int | None:
@ -149,8 +147,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
None is unknown, 0 when stopped, 1 when opening, 2 when closing
"""
mov = self.coordinator.data[self.unique_id].moveState
return int(mov) if mov is not None else None
return self.coordinator.data[self.unique_id].move_state
@property
def is_opening(self) -> bool:
@ -190,11 +187,11 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
"""Set the cover to the new position and wait for the update to be reflected."""
try:
await self._bapi.async_change_request_position(
position, thingUri=self._thing.thingUri
position, thing_uri=self._thing.thing_uri
)
except ClientResponseError as exc:
raise HomeAssistantError(
f"Unable to reposition {self._thing.NAME}"
f"Unable to reposition {self._thing.name}"
) from exc
self.coordinator.update_interval = FAST_INTERVAL
await self.coordinator.async_request_refresh()
@ -204,7 +201,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
"""Update the update interval after each refresh."""
if (
self.request_cover_position
== self._bapi.last_requested_positions[self._thing.thingUri]
== self._bapi.last_requested_positions[self._thing.thing_uri]
and self.move_state == 0
):
self.coordinator.update_interval = REGULAR_INTERVAL

View File

@ -3,7 +3,7 @@
"name": "Brunt Blind Engine",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/brunt",
"requirements": ["brunt==1.0.2"],
"requirements": ["brunt==1.1.0"],
"codeowners": ["@eavanvalkenburg"],
"iot_class": "cloud_polling"
}

View File

@ -161,6 +161,9 @@ class WebDavCalendarData:
)
event_list = []
for event in vevent_list:
if not hasattr(event.instance, "vevent"):
_LOGGER.warning("Skipped event with missing 'vevent' property")
continue
vevent = event.instance.vevent
if not self.is_matching(vevent, self.search):
continue
@ -198,6 +201,9 @@ class WebDavCalendarData:
# and they would not be properly parsed using their original start/end dates.
new_events = []
for event in results:
if not hasattr(event.instance, "vevent"):
_LOGGER.warning("Skipped event with missing 'vevent' property")
continue
vevent = event.instance.vevent
for start_dt in vevent.getrruleset() or []:
_start_of_today = start_of_today

View File

@ -3,7 +3,7 @@
"name": "Google Cast",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast",
"requirements": ["pychromecast==10.2.1"],
"requirements": ["pychromecast==10.2.2"],
"after_dependencies": [
"cloud",
"http",

View File

@ -395,11 +395,14 @@ class CastDevice(MediaPlayerEntity):
return
if self._chromecast.app_id is not None:
# Quit the previous app before starting splash screen
# Quit the previous app before starting splash screen or media player
self._chromecast.quit_app()
# The only way we can turn the Chromecast is on is by launching an app
self._chromecast.play_media(CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED)
if self._chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST:
self._chromecast.play_media(CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED)
else:
self._chromecast.start_app(pychromecast.config.APP_MEDIA_RECEIVER)
def turn_off(self):
"""Turn off the cast device."""
@ -526,7 +529,7 @@ class CastDevice(MediaPlayerEntity):
controller.play_media(media)
else:
app_data = {"media_id": media_id, "media_type": media_type, **extra}
quick_play(self._chromecast, "homeassistant_media", app_data)
quick_play(self._chromecast, "default_media_receiver", app_data)
def _media_status(self):
"""
@ -674,9 +677,9 @@ class CastDevice(MediaPlayerEntity):
support = SUPPORT_CAST
media_status = self._media_status()[0]
if (
self._chromecast
and self._chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST
if self._chromecast and self._chromecast.cast_type in (
pychromecast.const.CAST_TYPE_CHROMECAST,
pychromecast.const.CAST_TYPE_AUDIO,
):
support |= SUPPORT_TURN_ON

View File

@ -3,7 +3,7 @@
"name": "Dexcom",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dexcom",
"requirements": ["pydexcom==0.2.1"],
"requirements": ["pydexcom==0.2.2"],
"codeowners": ["@gagebenne"],
"iot_class": "cloud_polling"
}

View File

@ -3,7 +3,7 @@
"name": "DLNA Digital Media Renderer",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
"requirements": ["async-upnp-client==0.22.12"],
"requirements": ["async-upnp-client==0.23.1"],
"dependencies": ["ssdp"],
"ssdp": [
{

View File

@ -112,10 +112,11 @@ def request_app_setup(
Then come back here and hit the below button.
"""
except NoURLAvailableError:
error_msg = """Could not find a SSL enabled URL for your Home Assistant instance.
Fitbit requires that your Home Assistant instance is accessible via HTTPS.
"""
configurator.notify_errors(_CONFIGURING["fitbit"], error_msg)
_LOGGER.error(
"Could not find an SSL enabled URL for your Home Assistant instance. "
"Fitbit requires that your Home Assistant instance is accessible via HTTPS"
)
return
submit = "I have saved my Client ID and Client Secret into fitbit.conf."

View File

@ -3,7 +3,7 @@
"name": "Flux LED/MagicHome",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/flux_led",
"requirements": ["flux_led==0.26.15"],
"requirements": ["flux_led==0.27.8"],
"quality_scale": "platinum",
"codeowners": ["@icemanch"],
"iot_class": "local_push",

View File

@ -5,7 +5,7 @@ from abc import ABC, abstractmethod
from datetime import timedelta
from typing import TYPE_CHECKING, Any, Dict, TypeVar
from pyfronius import FroniusError
from pyfronius import BadStatusError, FroniusError
from homeassistant.components.sensor import SensorEntityDescription
from homeassistant.core import callback
@ -43,6 +43,8 @@ class FroniusCoordinatorBase(
error_interval: timedelta
valid_descriptions: list[SensorEntityDescription]
MAX_FAILED_UPDATES = 3
def __init__(self, *args: Any, solar_net: FroniusSolarNet, **kwargs: Any) -> None:
"""Set up the FroniusCoordinatorBase class."""
self._failed_update_count = 0
@ -62,7 +64,7 @@ class FroniusCoordinatorBase(
data = await self._update_method()
except FroniusError as err:
self._failed_update_count += 1
if self._failed_update_count == 3:
if self._failed_update_count == self.MAX_FAILED_UPDATES:
self.update_interval = self.error_interval
raise UpdateFailed(err) from err
@ -116,6 +118,8 @@ class FroniusInverterUpdateCoordinator(FroniusCoordinatorBase):
error_interval = timedelta(minutes=10)
valid_descriptions = INVERTER_ENTITY_DESCRIPTIONS
SILENT_RETRIES = 3
def __init__(
self, *args: Any, inverter_info: FroniusDeviceInfo, **kwargs: Any
) -> None:
@ -125,9 +129,19 @@ class FroniusInverterUpdateCoordinator(FroniusCoordinatorBase):
async def _update_method(self) -> dict[SolarNetId, Any]:
"""Return data per solar net id from pyfronius."""
data = await self.solar_net.fronius.current_inverter_data(
self.inverter_info.solar_net_id
)
# almost 1% of `current_inverter_data` requests on Symo devices result in
# `BadStatusError Code: 8 - LNRequestTimeout` due to flaky internal
# communication between the logger and the inverter.
for silent_retry in range(self.SILENT_RETRIES):
try:
data = await self.solar_net.fronius.current_inverter_data(
self.inverter_info.solar_net_id
)
except BadStatusError as err:
if silent_retry == (self.SILENT_RETRIES - 1):
raise err
continue
break
# wrap a single devices data in a dict with solar_net_id key for
# FroniusCoordinatorBase _async_update_data and add_entities_for_seen_keys
return {self.inverter_info.solar_net_id: data}

View File

@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
"home-assistant-frontend==20211215.0"
"home-assistant-frontend==20211220.0"
],
"dependencies": [
"api",

View File

@ -30,14 +30,13 @@ async def async_setup_entry(hass, config):
loc_id = config.data.get(CONF_LOC_ID)
dev_id = config.data.get(CONF_DEV_ID)
devices = []
devices = {}
for location in client.locations_by_id.values():
for device in location.devices_by_id.values():
if (not loc_id or location.locationid == loc_id) and (
not dev_id or device.deviceid == dev_id
):
devices.append(device)
if not loc_id or location.locationid == loc_id:
for device in location.devices_by_id.values():
if not dev_id or device.deviceid == dev_id:
devices[device.deviceid] = device
if len(devices) == 0:
_LOGGER.debug("No devices found")
@ -107,23 +106,30 @@ class HoneywellData:
if self._client is None:
return False
devices = [
refreshed_devices = [
device
for location in self._client.locations_by_id.values()
for device in location.devices_by_id.values()
]
if len(devices) == 0:
_LOGGER.error("Failed to find any devices")
if len(refreshed_devices) == 0:
_LOGGER.error("Failed to find any devices after retry")
return False
self.devices = devices
for updated_device in refreshed_devices:
if updated_device.deviceid in self.devices:
self.devices[updated_device.deviceid] = updated_device
else:
_LOGGER.info(
"New device with ID %s detected, reload the honeywell integration if you want to access it in Home Assistant"
)
await self._hass.config_entries.async_reload(self._config.entry_id)
return True
async def _refresh_devices(self):
"""Refresh each enabled device."""
for device in self.devices:
for device in self.devices.values():
await self._hass.async_add_executor_job(device.refresh)
await asyncio.sleep(UPDATE_LOOP_SLEEP_TIME)
@ -143,11 +149,16 @@ class HoneywellData:
) as exp:
retries -= 1
if retries == 0:
_LOGGER.error(
"Ran out of retry attempts (3 attempts allocated). Error: %s",
exp,
)
raise exp
result = await self._retry()
if not result:
_LOGGER.error("Retry result was empty. Error: %s", exp)
raise exp
_LOGGER.error("SomeComfort update failed, Retrying - Error: %s", exp)
_LOGGER.info("SomeComfort update failed, retrying. Error: %s", exp)

View File

@ -122,7 +122,7 @@ async def async_setup_entry(hass, config, async_add_entities, discovery_info=Non
async_add_entities(
[
HoneywellUSThermostat(data, device, cool_away_temp, heat_away_temp)
for device in data.devices
for device in data.devices.values()
]
)

View File

@ -3,7 +3,7 @@
"name": "Philips Hue",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hue",
"requirements": ["aiohue==3.0.6"],
"requirements": ["aiohue==3.0.7"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",

View File

@ -47,20 +47,9 @@ class HueBaseEntity(Entity):
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.device.id)},
)
# some (3th party) Hue lights report their connection status incorrectly
# causing the zigbee availability to report as disconnected while in fact
# it can be controlled. Although this is in fact something the device manufacturer
# should fix, we work around it here. If the light is reported unavailable at
# startup, we ignore the availability status of the zigbee connection
self._ignore_availability = False
if self.device is None:
return
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
self._ignore_availability = (
# Official Hue lights are reliable
self.device.product_data.manufacturer_name != "Signify Netherlands B.V."
and zigbee.status != ConnectivityServiceStatus.CONNECTED
)
# used for availability workaround
self._ignore_availability = None
self._last_state = None
@property
def name(self) -> str:
@ -82,6 +71,7 @@ class HueBaseEntity(Entity):
async def async_added_to_hass(self) -> None:
"""Call when entity is added."""
self._check_availability_workaround()
# Add value_changed callbacks.
self.async_on_remove(
self.controller.subscribe(
@ -140,5 +130,50 @@ class HueBaseEntity(Entity):
ent_reg.async_remove(self.entity_id)
else:
self.logger.debug("Received status update for %s", self.entity_id)
self._check_availability_workaround()
self.on_update()
self.async_write_ha_state()
@callback
def _check_availability_workaround(self):
"""Check availability of the device."""
if self.resource.type != ResourceTypes.LIGHT:
return
if self._ignore_availability is not None:
# already processed
return
cur_state = self.resource.on.on
if self._last_state is None:
self._last_state = cur_state
return
# some (3th party) Hue lights report their connection status incorrectly
# causing the zigbee availability to report as disconnected while in fact
# it can be controlled. Although this is in fact something the device manufacturer
# should fix, we work around it here. If the light is reported unavailable
# by the zigbee connectivity but the state changesm its considered as a
# malfunctioning device and we report it.
# while the user should actually fix this issue instead of ignoring it, we
# ignore the availability for this light from this point.
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
if (
self._last_state != cur_state
and zigbee.status != ConnectivityServiceStatus.CONNECTED
):
# the device state changed from on->off or off->on
# while it was reported as not connected!
self.logger.warning(
"Light %s changed state while reported as disconnected. "
"This is an indicator that routing is not working properly for this device. "
"Home Assistant will ignore availability for this light from now on. "
"Device details: %s - %s (%s) fw: %s",
self.name,
self.device.product_data.manufacturer_name,
self.device.product_data.product_name,
self.device.product_data.model_id,
self.device.product_data.software_version,
)
# do we want to store this in some persistent storage?
self._ignore_availability = True
else:
self._ignore_availability = False
self._last_state = cur_state

View File

@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/knx",
"requirements": [
"xknx==0.18.13"
"xknx==0.18.14"
],
"codeowners": [
"@Julius2342",

View File

@ -2,6 +2,7 @@
from __future__ import annotations
from datetime import timedelta
from http import HTTPStatus
import logging
from aiohttp.client_exceptions import ClientResponseError
@ -30,7 +31,11 @@ from homeassistant.helpers.update_coordinator import (
UpdateFailed,
)
from .api import ConfigEntryLyricClient, LyricLocalOAuth2Implementation
from .api import (
ConfigEntryLyricClient,
LyricLocalOAuth2Implementation,
OAuth2SessionLyric,
)
from .config_flow import OAuth2FlowHandler
from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN
@ -84,21 +89,35 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
session = aiohttp_client.async_get_clientsession(hass)
oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
oauth_session = OAuth2SessionLyric(hass, entry, implementation)
client = ConfigEntryLyricClient(session, oauth_session)
client_id = hass.data[DOMAIN][CONF_CLIENT_ID]
lyric = Lyric(client, client_id)
async def async_update_data() -> Lyric:
async def async_update_data(force_refresh_token: bool = False) -> Lyric:
"""Fetch data from Lyric."""
await oauth_session.async_ensure_token_valid()
try:
if not force_refresh_token:
await oauth_session.async_ensure_token_valid()
else:
await oauth_session.force_refresh_token()
except ClientResponseError as exception:
if exception.status in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN):
raise ConfigEntryAuthFailed from exception
raise UpdateFailed(exception) from exception
try:
async with async_timeout.timeout(60):
await lyric.get_locations()
return lyric
except LyricAuthenticationException as exception:
# Attempt to refresh the token before failing.
# Honeywell appear to have issues keeping tokens saved.
_LOGGER.debug("Authentication failed. Attempting to refresh token")
if not force_refresh_token:
return await async_update_data(force_refresh_token=True)
raise ConfigEntryAuthFailed from exception
except (LyricException, ClientResponseError) as exception:
raise UpdateFailed(exception) from exception

View File

@ -8,6 +8,18 @@ from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.aiohttp_client import async_get_clientsession
class OAuth2SessionLyric(config_entry_oauth2_flow.OAuth2Session):
"""OAuth2Session for Lyric."""
async def force_refresh_token(self) -> None:
"""Force a token refresh."""
new_token = await self.implementation.async_refresh_token(self.token)
self.hass.config_entries.async_update_entry(
self.config_entry, data={**self.config_entry.data, "token": new_token}
)
class ConfigEntryLyricClient(LyricClient):
"""Provide Honeywell Lyric authentication tied to an OAuth2 based config entry."""

View File

@ -2,7 +2,7 @@
"domain": "netgear",
"name": "NETGEAR",
"documentation": "https://www.home-assistant.io/integrations/netgear",
"requirements": ["pynetgear==0.7.0"],
"requirements": ["pynetgear==0.8.0"],
"codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"],
"iot_class": "local_polling",
"config_flow": true,

View File

@ -11,6 +11,7 @@ from nexia.const import (
SYSTEM_STATUS_IDLE,
UNIT_FAHRENHEIT,
)
from nexia.util import find_humidity_setpoint
import voluptuous as vol
from homeassistant.components.climate import ClimateEntity
@ -58,6 +59,8 @@ from .coordinator import NexiaDataUpdateCoordinator
from .entity import NexiaThermostatZoneEntity
from .util import percent_conv
PARALLEL_UPDATES = 1 # keep data in sync with only one connection at a time
SERVICE_SET_AIRCLEANER_MODE = "set_aircleaner_mode"
SERVICE_SET_HUMIDIFY_SETPOINT = "set_humidify_setpoint"
SERVICE_SET_HVAC_RUN_MODE = "set_hvac_run_mode"
@ -231,9 +234,9 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
def set_humidity(self, humidity):
"""Dehumidify target."""
if self._thermostat.has_dehumidify_support():
self._thermostat.set_dehumidify_setpoint(humidity / 100.0)
self.set_dehumidify_setpoint(humidity)
else:
self._thermostat.set_humidify_setpoint(humidity / 100.0)
self.set_humidify_setpoint(humidity)
self._signal_thermostat_update()
@property
@ -453,7 +456,22 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
def set_humidify_setpoint(self, humidity):
"""Set the humidify setpoint."""
self._thermostat.set_humidify_setpoint(humidity / 100.0)
target_humidity = find_humidity_setpoint(humidity / 100.0)
if self._thermostat.get_humidify_setpoint() == target_humidity:
# Trying to set the humidify setpoint to the
# same value will cause the api to timeout
return
self._thermostat.set_humidify_setpoint(target_humidity)
self._signal_thermostat_update()
def set_dehumidify_setpoint(self, humidity):
"""Set the dehumidify setpoint."""
target_humidity = find_humidity_setpoint(humidity / 100.0)
if self._thermostat.get_dehumidify_setpoint() == target_humidity:
# Trying to set the dehumidify setpoint to the
# same value will cause the api to timeout
return
self._thermostat.set_dehumidify_setpoint(target_humidity)
self._signal_thermostat_update()
def _signal_thermostat_update(self):

View File

@ -1,7 +1,7 @@
{
"domain": "nexia",
"name": "Nexia/American Standard/Trane",
"requirements": ["nexia==0.9.11"],
"requirements": ["nexia==0.9.12"],
"codeowners": ["@bdraco"],
"documentation": "https://www.home-assistant.io/integrations/nexia",
"config_flow": true,

View File

@ -85,7 +85,7 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
):
await self.async_set_unique_id(controller.mac)
self._abort_if_unique_id_configured(
updates={CONF_IP_ADDRESS: ip_address}
updates={CONF_IP_ADDRESS: ip_address}, reload_on_update=False
)
# A new rain machine: We will change out the unique id

View File

@ -12,6 +12,7 @@ from homeassistant.const import (
CONF_FORCE_UPDATE,
CONF_NAME,
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
import homeassistant.helpers.event as evt
@ -81,6 +82,7 @@ class RflinkBinarySensor(RflinkDevice, BinarySensorEntity):
if self._state and self._off_delay is not None:
@callback
def off_delay_listener(now):
"""Switch device off after a delay."""
self._delay_listener = None

View File

@ -2,7 +2,7 @@
"domain": "ring",
"name": "Ring",
"documentation": "https://www.home-assistant.io/integrations/ring",
"requirements": ["ring_doorbell==0.7.1"],
"requirements": ["ring_doorbell==0.7.2"],
"dependencies": ["ffmpeg"],
"codeowners": ["@balloob"],
"config_flow": true,

View File

@ -69,7 +69,6 @@ async def async_setup_climate_entities(
) -> None:
"""Set up online climate devices."""
_LOGGER.info("Setup online climate device %s", wrapper.name)
device_block: Block | None = None
sensor_block: Block | None = None
@ -82,6 +81,7 @@ async def async_setup_climate_entities(
sensor_block = block
if sensor_block and device_block:
_LOGGER.debug("Setup online climate device %s", wrapper.name)
async_add_entities([BlockSleepingClimate(wrapper, sensor_block, device_block)])
@ -92,7 +92,6 @@ async def async_restore_climate_entities(
wrapper: BlockDeviceWrapper,
) -> None:
"""Restore sleeping climate devices."""
_LOGGER.info("Setup sleeping climate device %s", wrapper.name)
ent_reg = await entity_registry.async_get_registry(hass)
entries = entity_registry.async_entries_for_config_entry(
@ -104,6 +103,7 @@ async def async_restore_climate_entities(
if entry.domain != CLIMATE_DOMAIN:
continue
_LOGGER.debug("Setup sleeping climate device %s", wrapper.name)
_LOGGER.debug("Found entry %s [%s]", entry.original_name, entry.domain)
async_add_entities([BlockSleepingClimate(wrapper, None, None, entry)])

View File

@ -12,6 +12,7 @@ from simplipy.errors import (
EndpointUnavailableError,
InvalidCredentialsError,
SimplipyError,
WebsocketError,
)
from simplipy.system import SystemNotification
from simplipy.system.v3 import (
@ -472,6 +473,7 @@ class SimpliSafe:
self._api = api
self._hass = hass
self._system_notifications: dict[int, set[SystemNotification]] = {}
self._websocket_reconnect_task: asyncio.Task | None = None
self.entry = entry
self.initial_event_to_use: dict[int, dict[str, Any]] = {}
self.systems: dict[int, SystemType] = {}
@ -516,11 +518,44 @@ class SimpliSafe:
self._system_notifications[system.system_id] = latest_notifications
async def _async_websocket_on_connect(self) -> None:
"""Define a callback for connecting to the websocket."""
async def _async_start_websocket_loop(self) -> None:
"""Start a websocket reconnection loop."""
if TYPE_CHECKING:
assert self._api.websocket
await self._api.websocket.async_listen()
should_reconnect = True
try:
await self._api.websocket.async_connect()
await self._api.websocket.async_listen()
except asyncio.CancelledError:
LOGGER.debug("Request to cancel websocket loop received")
raise
except WebsocketError as err:
LOGGER.error("Failed to connect to websocket: %s", err)
except Exception as err: # pylint: disable=broad-except
LOGGER.error("Unknown exception while connecting to websocket: %s", err)
if should_reconnect:
LOGGER.info("Disconnected from websocket; reconnecting")
await self._async_cancel_websocket_loop()
self._websocket_reconnect_task = self._hass.async_create_task(
self._async_start_websocket_loop()
)
async def _async_cancel_websocket_loop(self) -> None:
"""Stop any existing websocket reconnection loop."""
if self._websocket_reconnect_task:
self._websocket_reconnect_task.cancel()
try:
await self._websocket_reconnect_task
except asyncio.CancelledError:
LOGGER.debug("Websocket reconnection task successfully canceled")
self._websocket_reconnect_task = None
if TYPE_CHECKING:
assert self._api.websocket
await self._api.websocket.async_disconnect()
@callback
def _async_websocket_on_event(self, event: WebsocketEvent) -> None:
@ -560,17 +595,17 @@ class SimpliSafe:
assert self._api.refresh_token
assert self._api.websocket
self._api.websocket.add_connect_callback(self._async_websocket_on_connect)
self._api.websocket.add_event_callback(self._async_websocket_on_event)
asyncio.create_task(self._api.websocket.async_connect())
self._websocket_reconnect_task = asyncio.create_task(
self._async_start_websocket_loop()
)
async def async_websocket_disconnect_listener(_: Event) -> None:
"""Define an event handler to disconnect from the websocket."""
if TYPE_CHECKING:
assert self._api.websocket
if self._api.websocket.connected:
await self._api.websocket.async_disconnect()
await self._async_cancel_websocket_loop()
self.entry.async_on_unload(
self._hass.bus.async_listen_once(
@ -612,18 +647,18 @@ class SimpliSafe:
data={**self.entry.data, CONF_TOKEN: token},
)
@callback
def async_handle_refresh_token(token: str) -> None:
async def async_handle_refresh_token(token: str) -> None:
"""Handle a new refresh token."""
async_save_refresh_token(token)
if TYPE_CHECKING:
assert self._api.websocket
if self._api.websocket.connected:
# If a websocket connection is open, reconnect it to use the
# new access token:
asyncio.create_task(self._api.websocket.async_reconnect())
# Open a new websocket connection with the fresh token:
await self._async_cancel_websocket_loop()
self._websocket_reconnect_task = self._hass.async_create_task(
self._async_start_websocket_loop()
)
self.entry.async_on_unload(
self._api.add_refresh_token_callback(async_handle_refresh_token)

View File

@ -3,7 +3,7 @@
"name": "SimpliSafe",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
"requirements": ["simplisafe-python==2021.12.1"],
"requirements": ["simplisafe-python==2021.12.2"],
"codeowners": ["@bachya"],
"iot_class": "cloud_polling",
"dhcp": [

View File

@ -2,7 +2,7 @@
"domain": "ssdp",
"name": "Simple Service Discovery Protocol (SSDP)",
"documentation": "https://www.home-assistant.io/integrations/ssdp",
"requirements": ["async-upnp-client==0.22.12"],
"requirements": ["async-upnp-client==0.23.1"],
"dependencies": ["network"],
"after_dependencies": ["zeroconf"],
"codeowners": [],

View File

@ -3,7 +3,7 @@
"name": "Tailscale",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tailscale",
"requirements": ["tailscale==0.1.5"],
"requirements": ["tailscale==0.1.6"],
"codeowners": ["@frenck"],
"quality_scale": "platinum",
"iot_class": "cloud_polling"

View File

@ -3,7 +3,7 @@
"name": "UPnP/IGD",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/upnp",
"requirements": ["async-upnp-client==0.22.12"],
"requirements": ["async-upnp-client==0.23.1"],
"dependencies": ["network", "ssdp"],
"codeowners": ["@StevenLooman","@ehendrix23"],
"ssdp": [

View File

@ -61,6 +61,11 @@ class VelbusClimate(VelbusEntity, ClimateEntity):
None,
)
@property
def current_temperature(self) -> int | None:
"""Return the current temperature."""
return self._channel.get_state()
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperatures."""
if (temp := kwargs.get(ATTR_TEMPERATURE)) is None:

View File

@ -49,7 +49,7 @@ class VelbusLight(VelbusEntity, LightEntity):
"""Representation of a Velbus light."""
_channel: VelbusDimmer
_attr_supported_feature = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION
_attr_supported_features = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION
@property
def is_on(self) -> bool:
@ -96,7 +96,7 @@ class VelbusButtonLight(VelbusEntity, LightEntity):
_channel: VelbusButton
_attr_entity_registry_enabled_default = False
_attr_supported_feature = SUPPORT_FLASH
_attr_supported_features = SUPPORT_FLASH
def __init__(self, channel: VelbusChannel) -> None:
"""Initialize the button light (led)."""

View File

@ -0,0 +1,26 @@
{
"config": {
"flow_title": "{name} ({host})",
"step": {
"user": {
"title": "{name}",
"description": "Set up ViCare integration. To generate API key go to https://developer.viessmann.com",
"data": {
"name": "[%key:common::config_flow::data::name%]",
"scan_interval": "Scan Interval (seconds)",
"username": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]",
"client_id": "[%key:common::config_flow::data::api_key%]",
"heating_type": "Heating type"
}
}
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
}
}

View File

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"single_instance_allowed": "Already configured. Only a single configuration possible.",
"unknown": "Unexpected error"
},
"error": {
"invalid_auth": "Invalid authentication"
},
"flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
"name": "Name",
"scan_interval": "Scan Interval (seconds)",
"client_id": "API Key",
"heating_type": "Heating type",
"password": "Password",
"username": "Email"
},
"title": "{name}",
"description": "Set up ViCare integration. To generate API key go to https://developer.viessmann.com"
}
}
}
}

View File

@ -3,7 +3,7 @@
"name": "Belkin WeMo",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/wemo",
"requirements": ["pywemo==0.6.7"],
"requirements": ["pywemo==0.7.0"],
"ssdp": [
{
"manufacturer": "Belkin International Inc."

View File

@ -2,7 +2,7 @@
"domain": "yeelight",
"name": "Yeelight",
"documentation": "https://www.home-assistant.io/integrations/yeelight",
"requirements": ["yeelight==0.7.8", "async-upnp-client==0.22.12"],
"requirements": ["yeelight==0.7.8", "async-upnp-client==0.23.1"],
"codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"],
"config_flow": true,
"dependencies": ["network"],

View File

@ -490,7 +490,7 @@ async def async_setup_entry(hass, config_entry): # noqa: C901
await platform.async_add_entities([entity])
if entity.unique_id:
hass.async_add_job(_add_node_to_component())
hass.create_task(_add_node_to_component())
return
@callback

View File

@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum
MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 12
PATCH_VERSION: Final = "3"
PATCH_VERSION: Final = "4"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)

View File

@ -4,7 +4,7 @@ aiodiscover==1.4.5
aiohttp==3.8.1
aiohttp_cors==0.7.0
astral==2.2
async-upnp-client==0.22.12
async-upnp-client==0.23.1
async_timeout==4.0.0
atomicwrites==1.4.0
attrs==21.2.0
@ -16,7 +16,7 @@ ciso8601==2.2.0
cryptography==35.0.0
emoji==1.5.0
hass-nabucasa==0.50.0
home-assistant-frontend==20211215.0
home-assistant-frontend==20211220.0
httpx==0.21.0
ifaddr==0.1.7
jinja2==3.0.3
@ -30,7 +30,7 @@ pyyaml==6.0
requests==2.26.0
scapy==2.4.5
sqlalchemy==1.4.27
voluptuous-serialize==2.4.0
voluptuous-serialize==2.5.0
voluptuous==0.12.2
yarl==1.6.3
zeroconf==0.37.0

View File

@ -21,5 +21,5 @@ python-slugify==4.0.1
pyyaml==6.0
requests==2.26.0
voluptuous==0.12.2
voluptuous-serialize==2.4.0
voluptuous-serialize==2.5.0
yarl==1.6.3

View File

@ -186,7 +186,7 @@ aiohomekit==0.6.4
aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==3.0.6
aiohue==3.0.7
# homeassistant.components.imap
aioimaplib==0.9.0
@ -336,7 +336,7 @@ asterisk_mbox==0.5.0
# homeassistant.components.ssdp
# homeassistant.components.upnp
# homeassistant.components.yeelight
async-upnp-client==0.22.12
async-upnp-client==0.23.1
# homeassistant.components.supla
asyncpysupla==0.0.5
@ -387,7 +387,7 @@ beautifulsoup4==4.10.0
bellows==0.29.0
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.8.5
bimmer_connected==0.8.7
# homeassistant.components.bizkaibus
bizkaibus==0.1.1
@ -440,7 +440,7 @@ brother==1.1.0
brottsplatskartan==0.0.1
# homeassistant.components.brunt
brunt==1.0.2
brunt==1.1.0
# homeassistant.components.bsblan
bsblan==0.4.0
@ -658,7 +658,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1
# homeassistant.components.flux_led
flux_led==0.26.15
flux_led==0.27.8
# homeassistant.components.homekit
fnvhash==0.1.0
@ -819,7 +819,7 @@ hole==0.7.0
holidays==0.11.3.1
# homeassistant.components.frontend
home-assistant-frontend==20211215.0
home-assistant-frontend==20211220.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@ -1065,7 +1065,7 @@ nettigo-air-monitor==1.2.1
neurio==0.3.1
# homeassistant.components.nexia
nexia==0.9.11
nexia==0.9.12
# homeassistant.components.nextcloud
nextcloudmonitor==1.1.0
@ -1396,7 +1396,7 @@ pycfdns==1.2.2
pychannels==1.0.0
# homeassistant.components.cast
pychromecast==10.2.1
pychromecast==10.2.2
# homeassistant.components.pocketcasts
pycketcasts==1.0.0
@ -1435,7 +1435,7 @@ pydeconz==85
pydelijn==0.6.1
# homeassistant.components.dexcom
pydexcom==0.2.1
pydexcom==0.2.2
# homeassistant.components.zwave
pydispatcher==2.0.5
@ -1661,7 +1661,7 @@ pymyq==3.1.4
pymysensors==0.22.1
# homeassistant.components.netgear
pynetgear==0.7.0
pynetgear==0.8.0
# homeassistant.components.netio
pynetio==0.1.9.1
@ -2007,7 +2007,7 @@ pyvolumio==0.1.3
pywebpush==1.9.2
# homeassistant.components.wemo
pywemo==0.6.7
pywemo==0.7.0
# homeassistant.components.wilight
pywilight==0.0.70
@ -2058,7 +2058,7 @@ rfk101py==0.0.1
rflink==0.0.58
# homeassistant.components.ring
ring_doorbell==0.7.1
ring_doorbell==0.7.2
# homeassistant.components.fleetgo
ritassist==0.9.2
@ -2146,7 +2146,7 @@ simplehound==0.3
simplepush==1.1.4
# homeassistant.components.simplisafe
simplisafe-python==2021.12.1
simplisafe-python==2021.12.2
# homeassistant.components.sisyphus
sisyphus-control==3.0
@ -2272,7 +2272,7 @@ systembridge==2.2.3
tahoma-api==0.0.16
# homeassistant.components.tailscale
tailscale==0.1.5
tailscale==0.1.6
# homeassistant.components.tank_utility
tank_utility==1.4.0
@ -2448,7 +2448,7 @@ xbox-webapi==2.0.11
xboxapi==2.0.1
# homeassistant.components.knx
xknx==0.18.13
xknx==0.18.14
# homeassistant.components.bluesound
# homeassistant.components.fritz

View File

@ -131,7 +131,7 @@ aiohomekit==0.6.4
aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==3.0.6
aiohue==3.0.7
# homeassistant.components.apache_kafka
aiokafka==0.6.0
@ -236,7 +236,7 @@ arcam-fmj==0.12.0
# homeassistant.components.ssdp
# homeassistant.components.upnp
# homeassistant.components.yeelight
async-upnp-client==0.22.12
async-upnp-client==0.23.1
# homeassistant.components.aurora
auroranoaa==0.0.2
@ -257,7 +257,7 @@ base36==0.1.1
bellows==0.29.0
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.8.5
bimmer_connected==0.8.7
# homeassistant.components.blebox
blebox_uniapi==1.3.3
@ -281,7 +281,7 @@ broadlink==0.18.0
brother==1.1.0
# homeassistant.components.brunt
brunt==1.0.2
brunt==1.1.0
# homeassistant.components.bsblan
bsblan==0.4.0
@ -399,7 +399,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1
# homeassistant.components.flux_led
flux_led==0.26.15
flux_led==0.27.8
# homeassistant.components.homekit
fnvhash==0.1.0
@ -515,7 +515,7 @@ hole==0.7.0
holidays==0.11.3.1
# homeassistant.components.frontend
home-assistant-frontend==20211215.0
home-assistant-frontend==20211220.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@ -654,7 +654,7 @@ netmap==0.7.0.2
nettigo-air-monitor==1.2.1
# homeassistant.components.nexia
nexia==0.9.11
nexia==0.9.12
# homeassistant.components.nfandroidtv
notifications-android-tv==0.1.3
@ -850,7 +850,7 @@ pybotvac==0.0.22
pycfdns==1.2.2
# homeassistant.components.cast
pychromecast==10.2.1
pychromecast==10.2.2
# homeassistant.components.climacell
pyclimacell==0.18.2
@ -868,7 +868,7 @@ pydaikin==2.6.0
pydeconz==85
# homeassistant.components.dexcom
pydexcom==0.2.1
pydexcom==0.2.2
# homeassistant.components.zwave
pydispatcher==2.0.5
@ -1019,7 +1019,7 @@ pymyq==3.1.4
pymysensors==0.22.1
# homeassistant.components.netgear
pynetgear==0.7.0
pynetgear==0.8.0
# homeassistant.components.nuki
pynuki==1.4.1
@ -1206,7 +1206,7 @@ pyvolumio==0.1.3
pywebpush==1.9.2
# homeassistant.components.wemo
pywemo==0.6.7
pywemo==0.7.0
# homeassistant.components.wilight
pywilight==0.0.70
@ -1230,7 +1230,7 @@ restrictedpython==5.2
rflink==0.0.58
# homeassistant.components.ring
ring_doorbell==0.7.1
ring_doorbell==0.7.2
# homeassistant.components.roku
rokuecp==0.8.4
@ -1273,7 +1273,7 @@ sharkiqpy==0.1.8
simplehound==0.3
# homeassistant.components.simplisafe
simplisafe-python==2021.12.1
simplisafe-python==2021.12.2
# homeassistant.components.slack
slackclient==2.5.0
@ -1352,7 +1352,7 @@ surepy==0.7.2
systembridge==2.2.3
# homeassistant.components.tailscale
tailscale==0.1.5
tailscale==0.1.6
# homeassistant.components.tellduslive
tellduslive==0.10.11
@ -1450,7 +1450,7 @@ wolf_smartset==0.1.11
xbox-webapi==2.0.11
# homeassistant.components.knx
xknx==0.18.13
xknx==0.18.14
# homeassistant.components.bluesound
# homeassistant.components.fritz

View File

@ -53,7 +53,7 @@ REQUIRES = [
"pyyaml==6.0",
"requests==2.26.0",
"voluptuous==0.12.2",
"voluptuous-serialize==2.4.0",
"voluptuous-serialize==2.5.0",
"yarl==1.6.3",
]

View File

@ -683,10 +683,12 @@ async def test_entity_cast_status(hass: HomeAssistant):
| SUPPORT_PLAY_MEDIA
| SUPPORT_STOP
| SUPPORT_TURN_OFF
| SUPPORT_TURN_ON
| SUPPORT_VOLUME_MUTE
| SUPPORT_VOLUME_SET,
SUPPORT_PLAY_MEDIA
| SUPPORT_TURN_OFF
| SUPPORT_TURN_ON
| SUPPORT_VOLUME_MUTE
| SUPPORT_VOLUME_SET,
),
@ -791,7 +793,7 @@ async def test_entity_play_media(hass: HomeAssistant, quick_play_mock):
chromecast.media_controller.play_media.assert_not_called()
quick_play_mock.assert_called_once_with(
chromecast,
"homeassistant_media",
"default_media_receiver",
{
"media_id": "best.mp3",
"media_type": "audio",
@ -907,7 +909,7 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistant, quick_play_mock):
# Play_media
await common.async_play_media(hass, "audio", "/best.mp3", entity_id)
quick_play_mock.assert_called_once_with(
chromecast, "homeassistant_media", {"media_id": ANY, "media_type": "audio"}
chromecast, "default_media_receiver", {"media_id": ANY, "media_type": "audio"}
)
assert quick_play_mock.call_args[0][2]["media_id"].startswith(
"http://example.com:8123/best.mp3?authSig="
@ -1311,7 +1313,7 @@ async def test_group_media_control(hass, mz_mock, quick_play_mock):
assert not chromecast.media_controller.play_media.called
quick_play_mock.assert_called_once_with(
chromecast,
"homeassistant_media",
"default_media_receiver",
{"media_id": "best.mp3", "media_type": "music"},
)

View File

@ -381,7 +381,7 @@ async def test_event_subscribe_rejected(
Device state will instead be obtained via polling in async_update.
"""
dmr_device_mock.async_subscribe_services.side_effect = UpnpResponseError(501)
dmr_device_mock.async_subscribe_services.side_effect = UpnpResponseError(status=501)
mock_entity_id = await setup_mock_component(hass, config_entry_mock)
mock_state = hass.states.get(mock_entity_id)

View File

@ -1,7 +1,7 @@
"""Test the Fronius update coordinators."""
from unittest.mock import patch
from pyfronius import FroniusError
from pyfronius import BadStatusError, FroniusError
from homeassistant.components.fronius.coordinator import (
FroniusInverterUpdateCoordinator,
@ -18,27 +18,32 @@ async def test_adaptive_update_interval(hass, aioclient_mock):
with patch("pyfronius.Fronius.current_inverter_data") as mock_inverter_data:
mock_responses(aioclient_mock)
await setup_fronius_integration(hass)
assert mock_inverter_data.call_count == 1
mock_inverter_data.assert_called_once()
mock_inverter_data.reset_mock()
async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval
)
await hass.async_block_till_done()
assert mock_inverter_data.call_count == 2
mock_inverter_data.assert_called_once()
mock_inverter_data.reset_mock()
mock_inverter_data.side_effect = FroniusError
# first 3 requests at default interval - 4th has different interval
for _ in range(4):
mock_inverter_data.side_effect = FroniusError()
# first 3 bad requests at default interval - 4th has different interval
for _ in range(3):
async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval
)
await hass.async_block_till_done()
assert mock_inverter_data.call_count == 5
assert mock_inverter_data.call_count == 3
mock_inverter_data.reset_mock()
async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.error_interval
)
await hass.async_block_till_done()
assert mock_inverter_data.call_count == 6
assert mock_inverter_data.call_count == 1
mock_inverter_data.reset_mock()
mock_inverter_data.side_effect = None
# next successful request resets to default interval
@ -46,10 +51,23 @@ async def test_adaptive_update_interval(hass, aioclient_mock):
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.error_interval
)
await hass.async_block_till_done()
assert mock_inverter_data.call_count == 7
mock_inverter_data.assert_called_once()
mock_inverter_data.reset_mock()
async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval
)
await hass.async_block_till_done()
assert mock_inverter_data.call_count == 8
mock_inverter_data.assert_called_once()
mock_inverter_data.reset_mock()
# BadStatusError on inverter endpoints have special handling
mock_inverter_data.side_effect = BadStatusError("mock_endpoint", 8)
# first 3 requests at default interval - 4th has different interval
for _ in range(3):
async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval
)
await hass.async_block_till_done()
# BadStatusError does 3 silent retries for inverter endpoint * 3 request intervals = 9
assert mock_inverter_data.call_count == 9

View File

@ -1,6 +1,8 @@
"""Test honeywell setup process."""
from unittest.mock import patch
from unittest.mock import create_autospec, patch
import somecomfort
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
@ -29,3 +31,20 @@ async def test_setup_multiple_thermostats(
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
assert hass.states.async_entity_ids_count() == 2
@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0)
async def test_setup_multiple_thermostats_with_same_deviceid(
hass: HomeAssistant, caplog, config_entry: MockConfigEntry, device, client
) -> None:
"""Test Honeywell TCC API returning duplicate device IDs."""
mock_location2 = create_autospec(somecomfort.Location, instance=True)
mock_location2.locationid.return_value = "location2"
mock_location2.devices_by_id = {device.deviceid: device}
client.locations_by_id["location2"] = mock_location2
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
assert hass.states.async_entity_ids_count() == 1
assert "Platform honeywell does not generate unique IDs" not in caplog.text

View File

@ -28,7 +28,15 @@ from tests.common import MockConfigEntry
def _gateway_descriptor(ip: str, port: int) -> GatewayDescriptor:
"""Get mock gw descriptor."""
return GatewayDescriptor("Test", ip, port, "eth0", "127.0.0.1", True)
return GatewayDescriptor(
"Test",
ip,
port,
"eth0",
"127.0.0.1",
supports_routing=True,
supports_tunnelling=True,
)
async def test_user_single_instance(hass):

View File

@ -57,7 +57,7 @@ def pywemo_device_fixture(pywemo_registry, pywemo_model):
device.port = MOCK_PORT
device.name = MOCK_NAME
device.serialnumber = MOCK_SERIAL_NUMBER
device.model_name = pywemo_model
device.model_name = pywemo_model.replace("LongPress", "")
device.get_state.return_value = 0 # Default to Off
device.supports_long_press.return_value = cls.supports_long_press()

View File

@ -3,7 +3,6 @@ import pytest
from pywemo.subscribe import EVENT_TYPE_LONG_PRESS
from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.wemo.const import DOMAIN, WEMO_SUBSCRIPTION_EVENT
from homeassistant.const import (
CONF_DEVICE_ID,
@ -11,6 +10,7 @@ from homeassistant.const import (
CONF_ENTITY_ID,
CONF_PLATFORM,
CONF_TYPE,
Platform,
)
from homeassistant.setup import async_setup_component
@ -26,8 +26,8 @@ DATA_MESSAGE = {"message": "service-called"}
@pytest.fixture
def pywemo_model():
"""Pywemo Dimmer models use the light platform (WemoDimmer class)."""
return "Dimmer"
"""Pywemo LightSwitch models use the switch platform."""
return "LightSwitchLongPress"
async def setup_automation(hass, device_id, trigger_type):
@ -67,14 +67,14 @@ async def test_get_triggers(hass, wemo_entity):
},
{
CONF_DEVICE_ID: wemo_entity.device_id,
CONF_DOMAIN: LIGHT_DOMAIN,
CONF_DOMAIN: Platform.SWITCH,
CONF_ENTITY_ID: wemo_entity.entity_id,
CONF_PLATFORM: "device",
CONF_TYPE: "turned_off",
},
{
CONF_DEVICE_ID: wemo_entity.device_id,
CONF_DOMAIN: LIGHT_DOMAIN,
CONF_DOMAIN: Platform.SWITCH,
CONF_ENTITY_ID: wemo_entity.entity_id,
CONF_PLATFORM: "device",
CONF_TYPE: "turned_on",

View File

@ -26,8 +26,8 @@ asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(True))
@pytest.fixture
def pywemo_model():
"""Pywemo Dimmer models use the light platform (WemoDimmer class)."""
return "Dimmer"
"""Pywemo LightSwitch models use the switch platform."""
return "LightSwitchLongPress"
async def test_async_register_device_longpress_fails(hass, pywemo_device):