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

View File

@ -2,7 +2,7 @@
"domain": "bmw_connected_drive", "domain": "bmw_connected_drive",
"name": "BMW Connected Drive", "name": "BMW Connected Drive",
"documentation": "https://www.home-assistant.io/integrations/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"], "codeowners": ["@gerard33", "@rikroe"],
"config_flow": true, "config_flow": true,
"iot_class": "cloud_polling" "iot_class": "cloud_polling"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
"name": "DLNA Digital Media Renderer", "name": "DLNA Digital Media Renderer",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "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"], "dependencies": ["ssdp"],
"ssdp": [ "ssdp": [
{ {

View File

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

View File

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

View File

@ -5,7 +5,7 @@ from abc import ABC, abstractmethod
from datetime import timedelta from datetime import timedelta
from typing import TYPE_CHECKING, Any, Dict, TypeVar 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.components.sensor import SensorEntityDescription
from homeassistant.core import callback from homeassistant.core import callback
@ -43,6 +43,8 @@ class FroniusCoordinatorBase(
error_interval: timedelta error_interval: timedelta
valid_descriptions: list[SensorEntityDescription] valid_descriptions: list[SensorEntityDescription]
MAX_FAILED_UPDATES = 3
def __init__(self, *args: Any, solar_net: FroniusSolarNet, **kwargs: Any) -> None: def __init__(self, *args: Any, solar_net: FroniusSolarNet, **kwargs: Any) -> None:
"""Set up the FroniusCoordinatorBase class.""" """Set up the FroniusCoordinatorBase class."""
self._failed_update_count = 0 self._failed_update_count = 0
@ -62,7 +64,7 @@ class FroniusCoordinatorBase(
data = await self._update_method() data = await self._update_method()
except FroniusError as err: except FroniusError as err:
self._failed_update_count += 1 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 self.update_interval = self.error_interval
raise UpdateFailed(err) from err raise UpdateFailed(err) from err
@ -116,6 +118,8 @@ class FroniusInverterUpdateCoordinator(FroniusCoordinatorBase):
error_interval = timedelta(minutes=10) error_interval = timedelta(minutes=10)
valid_descriptions = INVERTER_ENTITY_DESCRIPTIONS valid_descriptions = INVERTER_ENTITY_DESCRIPTIONS
SILENT_RETRIES = 3
def __init__( def __init__(
self, *args: Any, inverter_info: FroniusDeviceInfo, **kwargs: Any self, *args: Any, inverter_info: FroniusDeviceInfo, **kwargs: Any
) -> None: ) -> None:
@ -125,9 +129,19 @@ class FroniusInverterUpdateCoordinator(FroniusCoordinatorBase):
async def _update_method(self) -> dict[SolarNetId, Any]: async def _update_method(self) -> dict[SolarNetId, Any]:
"""Return data per solar net id from pyfronius.""" """Return data per solar net id from pyfronius."""
# 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( data = await self.solar_net.fronius.current_inverter_data(
self.inverter_info.solar_net_id 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 # wrap a single devices data in a dict with solar_net_id key for
# FroniusCoordinatorBase _async_update_data and add_entities_for_seen_keys # FroniusCoordinatorBase _async_update_data and add_entities_for_seen_keys
return {self.inverter_info.solar_net_id: data} return {self.inverter_info.solar_net_id: data}

View File

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

View File

@ -30,14 +30,13 @@ async def async_setup_entry(hass, config):
loc_id = config.data.get(CONF_LOC_ID) loc_id = config.data.get(CONF_LOC_ID)
dev_id = config.data.get(CONF_DEV_ID) dev_id = config.data.get(CONF_DEV_ID)
devices = [] devices = {}
for location in client.locations_by_id.values(): for location in client.locations_by_id.values():
if not loc_id or location.locationid == loc_id:
for device in location.devices_by_id.values(): for device in location.devices_by_id.values():
if (not loc_id or location.locationid == loc_id) and ( if not dev_id or device.deviceid == dev_id:
not dev_id or device.deviceid == dev_id devices[device.deviceid] = device
):
devices.append(device)
if len(devices) == 0: if len(devices) == 0:
_LOGGER.debug("No devices found") _LOGGER.debug("No devices found")
@ -107,23 +106,30 @@ class HoneywellData:
if self._client is None: if self._client is None:
return False return False
devices = [ refreshed_devices = [
device device
for location in self._client.locations_by_id.values() for location in self._client.locations_by_id.values()
for device in location.devices_by_id.values() for device in location.devices_by_id.values()
] ]
if len(devices) == 0: if len(refreshed_devices) == 0:
_LOGGER.error("Failed to find any devices") _LOGGER.error("Failed to find any devices after retry")
return False 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) await self._hass.config_entries.async_reload(self._config.entry_id)
return True return True
async def _refresh_devices(self): async def _refresh_devices(self):
"""Refresh each enabled device.""" """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 self._hass.async_add_executor_job(device.refresh)
await asyncio.sleep(UPDATE_LOOP_SLEEP_TIME) await asyncio.sleep(UPDATE_LOOP_SLEEP_TIME)
@ -143,11 +149,16 @@ class HoneywellData:
) as exp: ) as exp:
retries -= 1 retries -= 1
if retries == 0: if retries == 0:
_LOGGER.error(
"Ran out of retry attempts (3 attempts allocated). Error: %s",
exp,
)
raise exp raise exp
result = await self._retry() result = await self._retry()
if not result: if not result:
_LOGGER.error("Retry result was empty. Error: %s", exp)
raise 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( async_add_entities(
[ [
HoneywellUSThermostat(data, device, cool_away_temp, heat_away_temp) 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", "name": "Philips Hue",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hue", "documentation": "https://www.home-assistant.io/integrations/hue",
"requirements": ["aiohue==3.0.6"], "requirements": ["aiohue==3.0.7"],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "Royal Philips Electronics", "manufacturer": "Royal Philips Electronics",

View File

@ -47,20 +47,9 @@ class HueBaseEntity(Entity):
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.device.id)}, identifiers={(DOMAIN, self.device.id)},
) )
# some (3th party) Hue lights report their connection status incorrectly # used for availability workaround
# causing the zigbee availability to report as disconnected while in fact self._ignore_availability = None
# it can be controlled. Although this is in fact something the device manufacturer self._last_state = None
# 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
)
@property @property
def name(self) -> str: def name(self) -> str:
@ -82,6 +71,7 @@ class HueBaseEntity(Entity):
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Call when entity is added.""" """Call when entity is added."""
self._check_availability_workaround()
# Add value_changed callbacks. # Add value_changed callbacks.
self.async_on_remove( self.async_on_remove(
self.controller.subscribe( self.controller.subscribe(
@ -140,5 +130,50 @@ class HueBaseEntity(Entity):
ent_reg.async_remove(self.entity_id) ent_reg.async_remove(self.entity_id)
else: else:
self.logger.debug("Received status update for %s", self.entity_id) self.logger.debug("Received status update for %s", self.entity_id)
self._check_availability_workaround()
self.on_update() self.on_update()
self.async_write_ha_state() 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, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/knx", "documentation": "https://www.home-assistant.io/integrations/knx",
"requirements": [ "requirements": [
"xknx==0.18.13" "xknx==0.18.14"
], ],
"codeowners": [ "codeowners": [
"@Julius2342", "@Julius2342",

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from http import HTTPStatus
import logging import logging
from aiohttp.client_exceptions import ClientResponseError from aiohttp.client_exceptions import ClientResponseError
@ -30,7 +31,11 @@ from homeassistant.helpers.update_coordinator import (
UpdateFailed, UpdateFailed,
) )
from .api import ConfigEntryLyricClient, LyricLocalOAuth2Implementation from .api import (
ConfigEntryLyricClient,
LyricLocalOAuth2Implementation,
OAuth2SessionLyric,
)
from .config_flow import OAuth2FlowHandler from .config_flow import OAuth2FlowHandler
from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN 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) 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 = ConfigEntryLyricClient(session, oauth_session)
client_id = hass.data[DOMAIN][CONF_CLIENT_ID] client_id = hass.data[DOMAIN][CONF_CLIENT_ID]
lyric = Lyric(client, 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.""" """Fetch data from Lyric."""
try:
if not force_refresh_token:
await oauth_session.async_ensure_token_valid() 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: try:
async with async_timeout.timeout(60): async with async_timeout.timeout(60):
await lyric.get_locations() await lyric.get_locations()
return lyric return lyric
except LyricAuthenticationException as exception: 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 raise ConfigEntryAuthFailed from exception
except (LyricException, ClientResponseError) as exception: except (LyricException, ClientResponseError) as exception:
raise UpdateFailed(exception) from 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 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): class ConfigEntryLyricClient(LyricClient):
"""Provide Honeywell Lyric authentication tied to an OAuth2 based config entry.""" """Provide Honeywell Lyric authentication tied to an OAuth2 based config entry."""

View File

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

View File

@ -11,6 +11,7 @@ from nexia.const import (
SYSTEM_STATUS_IDLE, SYSTEM_STATUS_IDLE,
UNIT_FAHRENHEIT, UNIT_FAHRENHEIT,
) )
from nexia.util import find_humidity_setpoint
import voluptuous as vol import voluptuous as vol
from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate import ClimateEntity
@ -58,6 +59,8 @@ from .coordinator import NexiaDataUpdateCoordinator
from .entity import NexiaThermostatZoneEntity from .entity import NexiaThermostatZoneEntity
from .util import percent_conv 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_AIRCLEANER_MODE = "set_aircleaner_mode"
SERVICE_SET_HUMIDIFY_SETPOINT = "set_humidify_setpoint" SERVICE_SET_HUMIDIFY_SETPOINT = "set_humidify_setpoint"
SERVICE_SET_HVAC_RUN_MODE = "set_hvac_run_mode" SERVICE_SET_HVAC_RUN_MODE = "set_hvac_run_mode"
@ -231,9 +234,9 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
def set_humidity(self, humidity): def set_humidity(self, humidity):
"""Dehumidify target.""" """Dehumidify target."""
if self._thermostat.has_dehumidify_support(): if self._thermostat.has_dehumidify_support():
self._thermostat.set_dehumidify_setpoint(humidity / 100.0) self.set_dehumidify_setpoint(humidity)
else: else:
self._thermostat.set_humidify_setpoint(humidity / 100.0) self.set_humidify_setpoint(humidity)
self._signal_thermostat_update() self._signal_thermostat_update()
@property @property
@ -453,7 +456,22 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
def set_humidify_setpoint(self, humidity): def set_humidify_setpoint(self, humidity):
"""Set the humidify setpoint.""" """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() self._signal_thermostat_update()
def _signal_thermostat_update(self): def _signal_thermostat_update(self):

View File

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

View File

@ -85,7 +85,7 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
): ):
await self.async_set_unique_id(controller.mac) await self.async_set_unique_id(controller.mac)
self._abort_if_unique_id_configured( 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 # 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_FORCE_UPDATE,
CONF_NAME, CONF_NAME,
) )
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.helpers.event as evt import homeassistant.helpers.event as evt
@ -81,6 +82,7 @@ class RflinkBinarySensor(RflinkDevice, BinarySensorEntity):
if self._state and self._off_delay is not None: if self._state and self._off_delay is not None:
@callback
def off_delay_listener(now): def off_delay_listener(now):
"""Switch device off after a delay.""" """Switch device off after a delay."""
self._delay_listener = None self._delay_listener = None

View File

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

View File

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

View File

@ -12,6 +12,7 @@ from simplipy.errors import (
EndpointUnavailableError, EndpointUnavailableError,
InvalidCredentialsError, InvalidCredentialsError,
SimplipyError, SimplipyError,
WebsocketError,
) )
from simplipy.system import SystemNotification from simplipy.system import SystemNotification
from simplipy.system.v3 import ( from simplipy.system.v3 import (
@ -472,6 +473,7 @@ class SimpliSafe:
self._api = api self._api = api
self._hass = hass self._hass = hass
self._system_notifications: dict[int, set[SystemNotification]] = {} self._system_notifications: dict[int, set[SystemNotification]] = {}
self._websocket_reconnect_task: asyncio.Task | None = None
self.entry = entry self.entry = entry
self.initial_event_to_use: dict[int, dict[str, Any]] = {} self.initial_event_to_use: dict[int, dict[str, Any]] = {}
self.systems: dict[int, SystemType] = {} self.systems: dict[int, SystemType] = {}
@ -516,11 +518,44 @@ class SimpliSafe:
self._system_notifications[system.system_id] = latest_notifications self._system_notifications[system.system_id] = latest_notifications
async def _async_websocket_on_connect(self) -> None: async def _async_start_websocket_loop(self) -> None:
"""Define a callback for connecting to the websocket.""" """Start a websocket reconnection loop."""
if TYPE_CHECKING: if TYPE_CHECKING:
assert self._api.websocket assert self._api.websocket
should_reconnect = True
try:
await self._api.websocket.async_connect()
await self._api.websocket.async_listen() 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 @callback
def _async_websocket_on_event(self, event: WebsocketEvent) -> None: def _async_websocket_on_event(self, event: WebsocketEvent) -> None:
@ -560,17 +595,17 @@ class SimpliSafe:
assert self._api.refresh_token assert self._api.refresh_token
assert self._api.websocket 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) 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: async def async_websocket_disconnect_listener(_: Event) -> None:
"""Define an event handler to disconnect from the websocket.""" """Define an event handler to disconnect from the websocket."""
if TYPE_CHECKING: if TYPE_CHECKING:
assert self._api.websocket assert self._api.websocket
if self._api.websocket.connected: await self._async_cancel_websocket_loop()
await self._api.websocket.async_disconnect()
self.entry.async_on_unload( self.entry.async_on_unload(
self._hass.bus.async_listen_once( self._hass.bus.async_listen_once(
@ -612,18 +647,18 @@ class SimpliSafe:
data={**self.entry.data, CONF_TOKEN: token}, data={**self.entry.data, CONF_TOKEN: token},
) )
@callback async def async_handle_refresh_token(token: str) -> None:
def async_handle_refresh_token(token: str) -> None:
"""Handle a new refresh token.""" """Handle a new refresh token."""
async_save_refresh_token(token) async_save_refresh_token(token)
if TYPE_CHECKING: if TYPE_CHECKING:
assert self._api.websocket assert self._api.websocket
if self._api.websocket.connected: # Open a new websocket connection with the fresh token:
# If a websocket connection is open, reconnect it to use the await self._async_cancel_websocket_loop()
# new access token: self._websocket_reconnect_task = self._hass.async_create_task(
asyncio.create_task(self._api.websocket.async_reconnect()) self._async_start_websocket_loop()
)
self.entry.async_on_unload( self.entry.async_on_unload(
self._api.add_refresh_token_callback(async_handle_refresh_token) self._api.add_refresh_token_callback(async_handle_refresh_token)

View File

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

View File

@ -2,7 +2,7 @@
"domain": "ssdp", "domain": "ssdp",
"name": "Simple Service Discovery Protocol (SSDP)", "name": "Simple Service Discovery Protocol (SSDP)",
"documentation": "https://www.home-assistant.io/integrations/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"], "dependencies": ["network"],
"after_dependencies": ["zeroconf"], "after_dependencies": ["zeroconf"],
"codeowners": [], "codeowners": [],

View File

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

View File

@ -3,7 +3,7 @@
"name": "UPnP/IGD", "name": "UPnP/IGD",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/upnp", "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"], "dependencies": ["network", "ssdp"],
"codeowners": ["@StevenLooman","@ehendrix23"], "codeowners": ["@StevenLooman","@ehendrix23"],
"ssdp": [ "ssdp": [

View File

@ -61,6 +61,11 @@ class VelbusClimate(VelbusEntity, ClimateEntity):
None, 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: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperatures.""" """Set new target temperatures."""
if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: if (temp := kwargs.get(ATTR_TEMPERATURE)) is None:

View File

@ -49,7 +49,7 @@ class VelbusLight(VelbusEntity, LightEntity):
"""Representation of a Velbus light.""" """Representation of a Velbus light."""
_channel: VelbusDimmer _channel: VelbusDimmer
_attr_supported_feature = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION _attr_supported_features = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
@ -96,7 +96,7 @@ class VelbusButtonLight(VelbusEntity, LightEntity):
_channel: VelbusButton _channel: VelbusButton
_attr_entity_registry_enabled_default = False _attr_entity_registry_enabled_default = False
_attr_supported_feature = SUPPORT_FLASH _attr_supported_features = SUPPORT_FLASH
def __init__(self, channel: VelbusChannel) -> None: def __init__(self, channel: VelbusChannel) -> None:
"""Initialize the button light (led).""" """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", "name": "Belkin WeMo",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/wemo", "documentation": "https://www.home-assistant.io/integrations/wemo",
"requirements": ["pywemo==0.6.7"], "requirements": ["pywemo==0.7.0"],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "Belkin International Inc." "manufacturer": "Belkin International Inc."

View File

@ -2,7 +2,7 @@
"domain": "yeelight", "domain": "yeelight",
"name": "Yeelight", "name": "Yeelight",
"documentation": "https://www.home-assistant.io/integrations/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"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"],
"config_flow": true, "config_flow": true,
"dependencies": ["network"], "dependencies": ["network"],

View File

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

View File

@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum
MAJOR_VERSION: Final = 2021 MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 12 MINOR_VERSION: Final = 12
PATCH_VERSION: Final = "3" PATCH_VERSION: Final = "4"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) 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==3.8.1
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
astral==2.2 astral==2.2
async-upnp-client==0.22.12 async-upnp-client==0.23.1
async_timeout==4.0.0 async_timeout==4.0.0
atomicwrites==1.4.0 atomicwrites==1.4.0
attrs==21.2.0 attrs==21.2.0
@ -16,7 +16,7 @@ ciso8601==2.2.0
cryptography==35.0.0 cryptography==35.0.0
emoji==1.5.0 emoji==1.5.0
hass-nabucasa==0.50.0 hass-nabucasa==0.50.0
home-assistant-frontend==20211215.0 home-assistant-frontend==20211220.0
httpx==0.21.0 httpx==0.21.0
ifaddr==0.1.7 ifaddr==0.1.7
jinja2==3.0.3 jinja2==3.0.3
@ -30,7 +30,7 @@ pyyaml==6.0
requests==2.26.0 requests==2.26.0
scapy==2.4.5 scapy==2.4.5
sqlalchemy==1.4.27 sqlalchemy==1.4.27
voluptuous-serialize==2.4.0 voluptuous-serialize==2.5.0
voluptuous==0.12.2 voluptuous==0.12.2
yarl==1.6.3 yarl==1.6.3
zeroconf==0.37.0 zeroconf==0.37.0

View File

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

View File

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

View File

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

View File

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

View File

@ -683,10 +683,12 @@ async def test_entity_cast_status(hass: HomeAssistant):
| SUPPORT_PLAY_MEDIA | SUPPORT_PLAY_MEDIA
| SUPPORT_STOP | SUPPORT_STOP
| SUPPORT_TURN_OFF | SUPPORT_TURN_OFF
| SUPPORT_TURN_ON
| SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_MUTE
| SUPPORT_VOLUME_SET, | SUPPORT_VOLUME_SET,
SUPPORT_PLAY_MEDIA SUPPORT_PLAY_MEDIA
| SUPPORT_TURN_OFF | SUPPORT_TURN_OFF
| SUPPORT_TURN_ON
| SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_MUTE
| SUPPORT_VOLUME_SET, | 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() chromecast.media_controller.play_media.assert_not_called()
quick_play_mock.assert_called_once_with( quick_play_mock.assert_called_once_with(
chromecast, chromecast,
"homeassistant_media", "default_media_receiver",
{ {
"media_id": "best.mp3", "media_id": "best.mp3",
"media_type": "audio", "media_type": "audio",
@ -907,7 +909,7 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistant, quick_play_mock):
# Play_media # Play_media
await common.async_play_media(hass, "audio", "/best.mp3", entity_id) await common.async_play_media(hass, "audio", "/best.mp3", entity_id)
quick_play_mock.assert_called_once_with( 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( assert quick_play_mock.call_args[0][2]["media_id"].startswith(
"http://example.com:8123/best.mp3?authSig=" "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 assert not chromecast.media_controller.play_media.called
quick_play_mock.assert_called_once_with( quick_play_mock.assert_called_once_with(
chromecast, chromecast,
"homeassistant_media", "default_media_receiver",
{"media_id": "best.mp3", "media_type": "music"}, {"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. 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_entity_id = await setup_mock_component(hass, config_entry_mock)
mock_state = hass.states.get(mock_entity_id) mock_state = hass.states.get(mock_entity_id)

View File

@ -1,7 +1,7 @@
"""Test the Fronius update coordinators.""" """Test the Fronius update coordinators."""
from unittest.mock import patch from unittest.mock import patch
from pyfronius import FroniusError from pyfronius import BadStatusError, FroniusError
from homeassistant.components.fronius.coordinator import ( from homeassistant.components.fronius.coordinator import (
FroniusInverterUpdateCoordinator, 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: with patch("pyfronius.Fronius.current_inverter_data") as mock_inverter_data:
mock_responses(aioclient_mock) mock_responses(aioclient_mock)
await setup_fronius_integration(hass) 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( async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval
) )
await hass.async_block_till_done() 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 mock_inverter_data.side_effect = FroniusError()
# first 3 requests at default interval - 4th has different interval # first 3 bad requests at default interval - 4th has different interval
for _ in range(4): for _ in range(3):
async_fire_time_changed( async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval
) )
await hass.async_block_till_done() 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( async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.error_interval hass, dt.utcnow() + FroniusInverterUpdateCoordinator.error_interval
) )
await hass.async_block_till_done() 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 mock_inverter_data.side_effect = None
# next successful request resets to default interval # 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 hass, dt.utcnow() + FroniusInverterUpdateCoordinator.error_interval
) )
await hass.async_block_till_done() 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( async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval
) )
await hass.async_block_till_done() 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.""" """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.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -29,3 +31,20 @@ async def test_setup_multiple_thermostats(
await hass.async_block_till_done() await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
assert hass.states.async_entity_ids_count() == 2 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: def _gateway_descriptor(ip: str, port: int) -> GatewayDescriptor:
"""Get mock gw descriptor.""" """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): 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.port = MOCK_PORT
device.name = MOCK_NAME device.name = MOCK_NAME
device.serialnumber = MOCK_SERIAL_NUMBER 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.get_state.return_value = 0 # Default to Off
device.supports_long_press.return_value = cls.supports_long_press() 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 pywemo.subscribe import EVENT_TYPE_LONG_PRESS
from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN 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.components.wemo.const import DOMAIN, WEMO_SUBSCRIPTION_EVENT
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE_ID, CONF_DEVICE_ID,
@ -11,6 +10,7 @@ from homeassistant.const import (
CONF_ENTITY_ID, CONF_ENTITY_ID,
CONF_PLATFORM, CONF_PLATFORM,
CONF_TYPE, CONF_TYPE,
Platform,
) )
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -26,8 +26,8 @@ DATA_MESSAGE = {"message": "service-called"}
@pytest.fixture @pytest.fixture
def pywemo_model(): def pywemo_model():
"""Pywemo Dimmer models use the light platform (WemoDimmer class).""" """Pywemo LightSwitch models use the switch platform."""
return "Dimmer" return "LightSwitchLongPress"
async def setup_automation(hass, device_id, trigger_type): 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_DEVICE_ID: wemo_entity.device_id,
CONF_DOMAIN: LIGHT_DOMAIN, CONF_DOMAIN: Platform.SWITCH,
CONF_ENTITY_ID: wemo_entity.entity_id, CONF_ENTITY_ID: wemo_entity.entity_id,
CONF_PLATFORM: "device", CONF_PLATFORM: "device",
CONF_TYPE: "turned_off", CONF_TYPE: "turned_off",
}, },
{ {
CONF_DEVICE_ID: wemo_entity.device_id, CONF_DEVICE_ID: wemo_entity.device_id,
CONF_DOMAIN: LIGHT_DOMAIN, CONF_DOMAIN: Platform.SWITCH,
CONF_ENTITY_ID: wemo_entity.entity_id, CONF_ENTITY_ID: wemo_entity.entity_id,
CONF_PLATFORM: "device", CONF_PLATFORM: "device",
CONF_TYPE: "turned_on", CONF_TYPE: "turned_on",

View File

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