mirror of
https://github.com/home-assistant/core.git
synced 2026-05-04 03:51:12 +02:00
Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 20a1bc710e | |||
| 5e0ea9fd24 | |||
| 1f0c13f259 | |||
| a1fc223914 | |||
| 836d8a6fca | |||
| 520c3411dd | |||
| d90c107b1b | |||
| 5a2bc8e493 | |||
| b8f8b30b9b | |||
| e80f4e03a4 | |||
| 7ec369d8ef | |||
| dcc08a0aac | |||
| 4802e4e33f | |||
| eb4b041d45 | |||
| bfd8579566 | |||
| beb5a992e6 | |||
| 7c925778eb | |||
| 7db161868e | |||
| 5f2a2280c5 | |||
| b327628b6e | |||
| 311ebd4a96 | |||
| dc4659b167 | |||
| c1d0fe9eae | |||
| 3fde6bfd73 | |||
| 4efa3b634e | |||
| cd65aaee60 | |||
| 2395c753fe | |||
| bc2949ef31 | |||
| f665c4e588 | |||
| 57b7b28d60 | |||
| 9a0f42f9a7 | |||
| 895dcaf690 | |||
| 4e96ff78b5 | |||
| a87ed13a04 | |||
| 735deff45e | |||
| 22867acaf8 | |||
| c507c72350 | |||
| 7fc36c4fe0 | |||
| 5196a770cc | |||
| 54d7380f4d | |||
| c445e93d45 | |||
| 614529d7c3 | |||
| 9361c9ef60 | |||
| 19a0644b50 | |||
| 82173f477c | |||
| b4af32624d | |||
| 78f40bd4bf | |||
| d92ad76ed9 | |||
| e44d50e1b1 | |||
| 95c0eeecfb | |||
| ec263840ba | |||
| 8047134c88 | |||
| cb89688873 | |||
| c73319e162 | |||
| 499cc2e51d | |||
| 5a03fffc20 | |||
| 6d8d472f0f | |||
| ac2897fc67 | |||
| e7e20533bd | |||
| 2772bae2e1 | |||
| 86622794e0 | |||
| 686f6768fc | |||
| f271fea07c | |||
| 77b1df5902 | |||
| 1faa111222 | |||
| b513301363 | |||
| 32bdcdd663 | |||
| 40f76d4ed9 | |||
| 34568aad89 | |||
| ffe84e8ece | |||
| 8cbd89282b | |||
| 1467668c94 | |||
| bbef38964d | |||
| 03b88af032 | |||
| 0626bc8b4f | |||
| 37ecbc53a7 | |||
| 52c96654a4 | |||
| 791c2f4b8a | |||
| ed041d5b7c | |||
| 1833ab96dc | |||
| ff2e2656b3 | |||
| 599c20c76e |
@@ -131,7 +131,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2021.11.4
|
||||
uses: home-assistant/builder@2021.12.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@@ -170,6 +170,17 @@ jobs:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2.4.0
|
||||
|
||||
- name: Set build additional args
|
||||
run: |
|
||||
# Create general tags
|
||||
if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then
|
||||
echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV
|
||||
elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then
|
||||
echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV
|
||||
else
|
||||
echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1.10.0
|
||||
with:
|
||||
@@ -184,7 +195,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2021.11.4
|
||||
uses: home-assistant/builder@2021.12.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Brunt Blind Engine",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/brunt",
|
||||
"requirements": ["brunt==1.0.0"],
|
||||
"requirements": ["brunt==1.1.0"],
|
||||
"codeowners": ["@eavanvalkenburg"],
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Google Cast",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/cast",
|
||||
"requirements": ["pychromecast==10.1.1"],
|
||||
"requirements": ["pychromecast==10.2.2"],
|
||||
"after_dependencies": [
|
||||
"cloud",
|
||||
"http",
|
||||
|
||||
@@ -47,7 +47,6 @@ from homeassistant.components.plex.const import PLEX_URI_SCHEME
|
||||
from homeassistant.components.plex.services import lookup_plex_media
|
||||
from homeassistant.const import (
|
||||
CAST_APP_ID_HOMEASSISTANT_LOVELACE,
|
||||
CAST_APP_ID_HOMEASSISTANT_MEDIA,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
STATE_IDLE,
|
||||
STATE_OFF,
|
||||
@@ -230,7 +229,6 @@ class CastDevice(MediaPlayerEntity):
|
||||
self._cast_info.cast_info,
|
||||
ChromeCastZeroconf.get_zeroconf(),
|
||||
)
|
||||
chromecast.media_controller.app_id = CAST_APP_ID_HOMEASSISTANT_MEDIA
|
||||
self._chromecast = chromecast
|
||||
|
||||
if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data:
|
||||
@@ -397,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."""
|
||||
@@ -527,9 +528,8 @@ class CastDevice(MediaPlayerEntity):
|
||||
self._chromecast.register_handler(controller)
|
||||
controller.play_media(media)
|
||||
else:
|
||||
self._chromecast.media_controller.play_media(
|
||||
media_id, media_type, **kwargs.get(ATTR_MEDIA_EXTRA, {})
|
||||
)
|
||||
app_data = {"media_id": media_id, "media_type": media_type, **extra}
|
||||
quick_play(self._chromecast, "default_media_receiver", app_data)
|
||||
|
||||
def _media_status(self):
|
||||
"""
|
||||
@@ -677,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
|
||||
|
||||
@@ -820,7 +820,6 @@ class DynamicCastGroup:
|
||||
self._cast_info.cast_info,
|
||||
ChromeCastZeroconf.get_zeroconf(),
|
||||
)
|
||||
chromecast.media_controller.app_id = CAST_APP_ID_HOMEASSISTANT_MEDIA
|
||||
self._chromecast = chromecast
|
||||
|
||||
if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data:
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "ebusd",
|
||||
"name": "ebusd",
|
||||
"documentation": "https://www.home-assistant.io/integrations/ebusd",
|
||||
"requirements": ["ebusdpy==0.0.16"],
|
||||
"requirements": ["ebusdpy==0.0.17"],
|
||||
"codeowners": [],
|
||||
"iot_class": "local_polling"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "environment_canada",
|
||||
"name": "Environment Canada",
|
||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||
"requirements": ["env_canada==0.5.18"],
|
||||
"requirements": ["env_canada==0.5.20"],
|
||||
"codeowners": ["@gwww", "@michaeldavie"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
|
||||
@@ -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."
|
||||
|
||||
|
||||
@@ -94,6 +94,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
firmware_date=None,
|
||||
model_info=None,
|
||||
model_description=None,
|
||||
remote_access_enabled=None,
|
||||
remote_access_host=None,
|
||||
remote_access_port=None,
|
||||
)
|
||||
return await self._async_handle_discovery()
|
||||
|
||||
@@ -261,6 +264,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
firmware_date=None,
|
||||
model_info=None,
|
||||
model_description=bulb.model_data.description,
|
||||
remote_access_enabled=None,
|
||||
remote_access_host=None,
|
||||
remote_access_port=None,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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.7"],
|
||||
"requirements": ["flux_led==0.27.8"],
|
||||
"quality_scale": "platinum",
|
||||
"codeowners": ["@icemanch"],
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": [
|
||||
"home-assistant-frontend==20211212.0"
|
||||
"home-assistant-frontend==20211220.0"
|
||||
],
|
||||
"dependencies": [
|
||||
"api",
|
||||
|
||||
@@ -120,10 +120,11 @@ class Light(HomeAccessory):
|
||||
if self._event_timer:
|
||||
self._event_timer()
|
||||
self._event_timer = async_call_later(
|
||||
self.hass, CHANGE_COALESCE_TIME_WINDOW, self._send_events
|
||||
self.hass, CHANGE_COALESCE_TIME_WINDOW, self._async_send_events
|
||||
)
|
||||
|
||||
def _send_events(self, *_):
|
||||
@callback
|
||||
def _async_send_events(self, *_):
|
||||
"""Process all changes at once."""
|
||||
_LOGGER.debug("Coalesced _set_chars: %s", self._pending_events)
|
||||
char_values = self._pending_events
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Philips Hue",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/hue",
|
||||
"requirements": ["aiohue==3.0.3"],
|
||||
"requirements": ["aiohue==3.0.7"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Royal Philips Electronics",
|
||||
|
||||
@@ -146,8 +146,10 @@ async def hue_activate_scene_v2(
|
||||
continue
|
||||
# found match!
|
||||
if transition:
|
||||
transition = transition * 100 # in steps of 100ms
|
||||
await api.scenes.recall(scene.id, dynamic=dynamic, duration=transition)
|
||||
transition = transition * 1000 # transition is in ms
|
||||
await bridge.async_request_call(
|
||||
api.scenes.recall, scene.id, dynamic=dynamic, duration=transition
|
||||
)
|
||||
return True
|
||||
LOGGER.debug(
|
||||
"Unable to find scene %s for group %s on bridge %s",
|
||||
|
||||
@@ -7,6 +7,7 @@ from aiohue.v2.models.button import ButtonEvent
|
||||
from aiohue.v2.models.resource import ResourceTypes
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
|
||||
from homeassistant.components.homeassistant.triggers import event as event_trigger
|
||||
from homeassistant.const import (
|
||||
@@ -35,7 +36,7 @@ if TYPE_CHECKING:
|
||||
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): str,
|
||||
vol.Required(CONF_SUBTYPE): int,
|
||||
vol.Required(CONF_SUBTYPE): vol.Union(int, str),
|
||||
vol.Optional(CONF_UNIQUE_ID): str,
|
||||
}
|
||||
)
|
||||
@@ -54,6 +55,33 @@ DEVICE_SPECIFIC_EVENT_TYPES = {
|
||||
}
|
||||
|
||||
|
||||
def check_invalid_device_trigger(
|
||||
bridge: HueBridge,
|
||||
config: ConfigType,
|
||||
device_entry: DeviceEntry,
|
||||
automation_info: AutomationTriggerInfo | None = None,
|
||||
):
|
||||
"""Check automation config for deprecated format."""
|
||||
# NOTE: Remove this check after 2022.6
|
||||
if isinstance(config["subtype"], int):
|
||||
return
|
||||
# found deprecated V1 style trigger, notify the user that it should be adjusted
|
||||
msg = (
|
||||
f"Incompatible device trigger detected for "
|
||||
f"[{device_entry.name}](/config/devices/device/{device_entry.id}) "
|
||||
"Please manually fix the outdated automation(s) once to fix this issue."
|
||||
)
|
||||
if automation_info:
|
||||
automation_id = automation_info["variables"]["this"]["attributes"]["id"] # type: ignore
|
||||
msg += f"\n\n[Check it out](/config/automation/edit/{automation_id})."
|
||||
persistent_notification.async_create(
|
||||
bridge.hass,
|
||||
msg,
|
||||
title="Outdated device trigger found",
|
||||
notification_id=f"hue_trigger_{device_entry.id}",
|
||||
)
|
||||
|
||||
|
||||
async def async_validate_trigger_config(
|
||||
bridge: "HueBridge",
|
||||
device_entry: DeviceEntry,
|
||||
@@ -61,6 +89,7 @@ async def async_validate_trigger_config(
|
||||
):
|
||||
"""Validate config."""
|
||||
config = TRIGGER_SCHEMA(config)
|
||||
check_invalid_device_trigger(bridge, config, device_entry)
|
||||
return config
|
||||
|
||||
|
||||
@@ -84,6 +113,7 @@ async def async_attach_trigger(
|
||||
},
|
||||
}
|
||||
)
|
||||
check_invalid_device_trigger(bridge, config, device_entry, automation_info)
|
||||
return await event_trigger.async_attach_trigger(
|
||||
hass, event_config, action, automation_info, platform_type="device"
|
||||
)
|
||||
|
||||
@@ -47,6 +47,9 @@ class HueBaseEntity(Entity):
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.device.id)},
|
||||
)
|
||||
# used for availability workaround
|
||||
self._ignore_availability = None
|
||||
self._last_state = None
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -68,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(
|
||||
@@ -98,13 +102,12 @@ class HueBaseEntity(Entity):
|
||||
def available(self) -> bool:
|
||||
"""Return entity availability."""
|
||||
if self.device is None:
|
||||
# devices without a device attached should be always available
|
||||
# entities without a device attached should be always available
|
||||
return True
|
||||
if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY:
|
||||
# the zigbee connectivity sensor itself should be always available
|
||||
return True
|
||||
if self.device.product_data.manufacturer_name != "Signify Netherlands B.V.":
|
||||
# availability status for non-philips brand lights is unreliable
|
||||
if self._ignore_availability:
|
||||
return True
|
||||
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
|
||||
# all device-attached entities get availability from the zigbee connectivity
|
||||
@@ -127,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
|
||||
|
||||
@@ -6,16 +6,19 @@ from typing import Any
|
||||
from aiohue.v2 import HueBridgeV2
|
||||
from aiohue.v2.controllers.events import EventType
|
||||
from aiohue.v2.controllers.groups import GroupedLight, Room, Zone
|
||||
from aiohue.v2.models.feature import AlertEffectType
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_FLASH,
|
||||
ATTR_TRANSITION,
|
||||
ATTR_XY_COLOR,
|
||||
COLOR_MODE_BRIGHTNESS,
|
||||
COLOR_MODE_COLOR_TEMP,
|
||||
COLOR_MODE_ONOFF,
|
||||
COLOR_MODE_XY,
|
||||
SUPPORT_FLASH,
|
||||
SUPPORT_TRANSITION,
|
||||
LightEntity,
|
||||
)
|
||||
@@ -32,6 +35,7 @@ ALLOWED_ERRORS = [
|
||||
'device (groupedLight) is "soft off", command (on) may not have effect',
|
||||
"device (light) has communication issues, command (on) may not have effect",
|
||||
'device (light) is "soft off", command (on) may not have effect',
|
||||
"attribute (supportedAlertActions) cannot be written",
|
||||
]
|
||||
|
||||
|
||||
@@ -88,6 +92,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
||||
self.group = group
|
||||
self.controller = controller
|
||||
self.api: HueBridgeV2 = bridge.api
|
||||
self._attr_supported_features |= SUPPORT_FLASH
|
||||
self._attr_supported_features |= SUPPORT_TRANSITION
|
||||
|
||||
# Entities for Hue groups are disabled by default
|
||||
@@ -146,6 +151,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
||||
xy_color = kwargs.get(ATTR_XY_COLOR)
|
||||
color_temp = kwargs.get(ATTR_COLOR_TEMP)
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||
flash = kwargs.get(ATTR_FLASH)
|
||||
if brightness is not None:
|
||||
# Hue uses a range of [0, 100] to control brightness.
|
||||
brightness = float((brightness / 255) * 100)
|
||||
@@ -160,6 +166,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
||||
and xy_color is None
|
||||
and color_temp is None
|
||||
and transition is None
|
||||
and flash is None
|
||||
):
|
||||
await self.bridge.async_request_call(
|
||||
self.controller.set_state,
|
||||
@@ -180,17 +187,37 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
||||
color_xy=xy_color if light.supports_color else None,
|
||||
color_temp=color_temp if light.supports_color_temperature else None,
|
||||
transition_time=transition,
|
||||
alert=AlertEffectType.BREATHE if flash is not None else None,
|
||||
allowed_errors=ALLOWED_ERRORS,
|
||||
)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the light off."""
|
||||
await self.bridge.async_request_call(
|
||||
self.controller.set_state,
|
||||
id=self.resource.id,
|
||||
on=False,
|
||||
allowed_errors=ALLOWED_ERRORS,
|
||||
)
|
||||
transition = kwargs.get(ATTR_TRANSITION)
|
||||
if transition is not None:
|
||||
# hue transition duration is in milliseconds
|
||||
transition = int(transition * 1000)
|
||||
|
||||
# NOTE: a grouped_light can only handle turn on/off
|
||||
# To set other features, you'll have to control the attached lights
|
||||
if transition is None:
|
||||
await self.bridge.async_request_call(
|
||||
self.controller.set_state,
|
||||
id=self.resource.id,
|
||||
on=False,
|
||||
allowed_errors=ALLOWED_ERRORS,
|
||||
)
|
||||
return
|
||||
|
||||
# redirect all other feature commands to underlying lights
|
||||
for light in self.controller.get_lights(self.resource.id):
|
||||
await self.bridge.async_request_call(
|
||||
self.api.lights.set_state,
|
||||
light.id,
|
||||
on=False,
|
||||
transition_time=transition,
|
||||
allowed_errors=ALLOWED_ERRORS,
|
||||
)
|
||||
|
||||
@callback
|
||||
def on_update(self) -> None:
|
||||
|
||||
@@ -6,17 +6,20 @@ from typing import Any
|
||||
from aiohue import HueBridgeV2
|
||||
from aiohue.v2.controllers.events import EventType
|
||||
from aiohue.v2.controllers.lights import LightsController
|
||||
from aiohue.v2.models.feature import AlertEffectType
|
||||
from aiohue.v2.models.light import Light
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_FLASH,
|
||||
ATTR_TRANSITION,
|
||||
ATTR_XY_COLOR,
|
||||
COLOR_MODE_BRIGHTNESS,
|
||||
COLOR_MODE_COLOR_TEMP,
|
||||
COLOR_MODE_ONOFF,
|
||||
COLOR_MODE_XY,
|
||||
SUPPORT_FLASH,
|
||||
SUPPORT_TRANSITION,
|
||||
LightEntity,
|
||||
)
|
||||
@@ -31,6 +34,7 @@ from .entity import HueBaseEntity
|
||||
ALLOWED_ERRORS = [
|
||||
"device (light) has communication issues, command (on) may not have effect",
|
||||
'device (light) is "soft off", command (on) may not have effect',
|
||||
"attribute (supportedAlertActions) cannot be written",
|
||||
]
|
||||
|
||||
|
||||
@@ -68,6 +72,7 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
) -> None:
|
||||
"""Initialize the light."""
|
||||
super().__init__(bridge, controller, resource)
|
||||
self._attr_supported_features |= SUPPORT_FLASH
|
||||
self.resource = resource
|
||||
self.controller = controller
|
||||
self._supported_color_modes = set()
|
||||
@@ -154,6 +159,7 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
xy_color = kwargs.get(ATTR_XY_COLOR)
|
||||
color_temp = kwargs.get(ATTR_COLOR_TEMP)
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||
flash = kwargs.get(ATTR_FLASH)
|
||||
if brightness is not None:
|
||||
# Hue uses a range of [0, 100] to control brightness.
|
||||
brightness = float((brightness / 255) * 100)
|
||||
@@ -169,12 +175,14 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
color_xy=xy_color,
|
||||
color_temp=color_temp,
|
||||
transition_time=transition,
|
||||
alert=AlertEffectType.BREATHE if flash is not None else None,
|
||||
allowed_errors=ALLOWED_ERRORS,
|
||||
)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the light off."""
|
||||
transition = kwargs.get(ATTR_TRANSITION)
|
||||
flash = kwargs.get(ATTR_FLASH)
|
||||
if transition is not None:
|
||||
# hue transition duration is in milliseconds
|
||||
transition = int(transition * 1000)
|
||||
@@ -183,5 +191,6 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
id=self.resource.id,
|
||||
on=False,
|
||||
transition_time=transition,
|
||||
alert=AlertEffectType.BREATHE if flash is not None else None,
|
||||
allowed_errors=ALLOWED_ERRORS,
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ from homeassistant.const import (
|
||||
CONF_PORT,
|
||||
CONF_TYPE,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
@@ -44,6 +45,7 @@ from .const import (
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS,
|
||||
CONF_KNX_ROUTING,
|
||||
CONF_KNX_TUNNELING,
|
||||
DATA_HASS_CONFIG,
|
||||
DATA_KNX_CONFIG,
|
||||
DOMAIN,
|
||||
KNX_ADDRESS,
|
||||
@@ -195,6 +197,7 @@ SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA = vol.Any(
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Start the KNX integration."""
|
||||
hass.data[DATA_HASS_CONFIG] = config
|
||||
conf: ConfigType | None = config.get(DOMAIN)
|
||||
|
||||
if conf is None:
|
||||
@@ -251,15 +254,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
|
||||
hass.config_entries.async_setup_platforms(
|
||||
entry, [platform for platform in SUPPORTED_PLATFORMS if platform in config]
|
||||
entry,
|
||||
[
|
||||
platform
|
||||
for platform in SUPPORTED_PLATFORMS
|
||||
if platform in config and platform is not Platform.NOTIFY
|
||||
],
|
||||
)
|
||||
|
||||
# set up notify platform, no entry support for notify component yet,
|
||||
# have to use discovery to load platform.
|
||||
if NotifySchema.PLATFORM in conf:
|
||||
# set up notify platform, no entry support for notify component yet
|
||||
if NotifySchema.PLATFORM in config:
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(
|
||||
hass, "notify", DOMAIN, conf[NotifySchema.PLATFORM], config
|
||||
hass, "notify", DOMAIN, {}, hass.data[DATA_HASS_CONFIG]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -312,6 +319,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
platform
|
||||
for platform in SUPPORTED_PLATFORMS
|
||||
if platform in hass.data[DATA_KNX_CONFIG]
|
||||
and platform is not Platform.NOTIFY
|
||||
],
|
||||
)
|
||||
if unload_ok:
|
||||
@@ -383,6 +391,7 @@ class KNXModule:
|
||||
if _conn_type == CONF_KNX_ROUTING:
|
||||
return ConnectionConfig(
|
||||
connection_type=ConnectionType.ROUTING,
|
||||
local_ip=self.config.get(ConnectionSchema.CONF_KNX_LOCAL_IP),
|
||||
auto_reconnect=True,
|
||||
)
|
||||
if _conn_type == CONF_KNX_TUNNELING:
|
||||
|
||||
@@ -137,9 +137,11 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
vol.Required(
|
||||
ConnectionSchema.CONF_KNX_ROUTE_BACK, default=False
|
||||
): vol.Coerce(bool),
|
||||
vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str,
|
||||
}
|
||||
|
||||
if self.show_advanced_options:
|
||||
fields[vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP)] = str
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="manual_tunnel", data_schema=vol.Schema(fields), errors=errors
|
||||
)
|
||||
@@ -195,6 +197,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS: user_input[
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS
|
||||
],
|
||||
ConnectionSchema.CONF_KNX_LOCAL_IP: user_input.get(
|
||||
ConnectionSchema.CONF_KNX_LOCAL_IP
|
||||
),
|
||||
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
|
||||
},
|
||||
)
|
||||
@@ -211,6 +216,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
): cv.port,
|
||||
}
|
||||
|
||||
if self.show_advanced_options:
|
||||
fields[vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP)] = str
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="routing", data_schema=vol.Schema(fields), errors=errors
|
||||
)
|
||||
@@ -306,7 +314,6 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
||||
vol.Required(
|
||||
CONF_PORT, default=self.current_config.get(CONF_PORT, 3671)
|
||||
): cv.port,
|
||||
vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str,
|
||||
vol.Required(
|
||||
ConnectionSchema.CONF_KNX_ROUTE_BACK,
|
||||
default=self.current_config.get(
|
||||
@@ -381,6 +388,14 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
||||
}
|
||||
|
||||
if self.show_advanced_options:
|
||||
data_schema[
|
||||
vol.Optional(
|
||||
ConnectionSchema.CONF_KNX_LOCAL_IP,
|
||||
default=self.current_config.get(
|
||||
ConnectionSchema.CONF_KNX_LOCAL_IP,
|
||||
),
|
||||
)
|
||||
] = str
|
||||
data_schema[
|
||||
vol.Required(
|
||||
ConnectionSchema.CONF_KNX_STATE_UPDATER,
|
||||
|
||||
@@ -42,7 +42,10 @@ CONF_STATE_ADDRESS: Final = "state_address"
|
||||
CONF_SYNC_STATE: Final = "sync_state"
|
||||
CONF_KNX_INITIAL_CONNECTION_TYPES: Final = [CONF_KNX_TUNNELING, CONF_KNX_ROUTING]
|
||||
|
||||
# yaml config merged with config entry data
|
||||
DATA_KNX_CONFIG: Final = "knx_config"
|
||||
# original hass yaml config
|
||||
DATA_HASS_CONFIG: Final = "knx_hass_config"
|
||||
|
||||
ATTR_COUNTER: Final = "counter"
|
||||
ATTR_SOURCE: Final = "source"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -11,7 +11,8 @@ from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import DOMAIN, KNX_ADDRESS
|
||||
from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS
|
||||
from .schema import NotifySchema
|
||||
|
||||
|
||||
async def async_get_service(
|
||||
@@ -20,24 +21,28 @@ async def async_get_service(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> KNXNotificationService | None:
|
||||
"""Get the KNX notification service."""
|
||||
if not discovery_info:
|
||||
if discovery_info is None:
|
||||
return None
|
||||
|
||||
platform_config: dict = discovery_info
|
||||
xknx: XKNX = hass.data[DOMAIN].xknx
|
||||
if platform_config := hass.data[DATA_KNX_CONFIG].get(NotifySchema.PLATFORM):
|
||||
xknx: XKNX = hass.data[DOMAIN].xknx
|
||||
|
||||
notification_devices = []
|
||||
for device_config in platform_config:
|
||||
notification_devices.append(
|
||||
XknxNotification(
|
||||
xknx,
|
||||
name=device_config[CONF_NAME],
|
||||
group_address=device_config[KNX_ADDRESS],
|
||||
notification_devices = []
|
||||
for device_config in platform_config:
|
||||
notification_devices.append(
|
||||
XknxNotification(
|
||||
xknx,
|
||||
name=device_config[CONF_NAME],
|
||||
group_address=device_config[KNX_ADDRESS],
|
||||
)
|
||||
)
|
||||
return (
|
||||
KNXNotificationService(notification_devices)
|
||||
if notification_devices
|
||||
else None
|
||||
)
|
||||
return (
|
||||
KNXNotificationService(notification_devices) if notification_devices else None
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class KNXNotificationService(BaseNotificationService):
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
"data": {
|
||||
"individual_address": "Individual address for the routing connection",
|
||||
"multicast_group": "The multicast group used for routing",
|
||||
"multicast_port": "The multicast port used for routing"
|
||||
"multicast_port": "The multicast port used for routing",
|
||||
"local_ip": "Local IP (leave empty if unsure)"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -48,6 +49,7 @@
|
||||
"individual_address": "Default individual address",
|
||||
"multicast_group": "Multicast group used for routing and discovery",
|
||||
"multicast_port": "Multicast port used for routing and discovery",
|
||||
"local_ip": "Local IP (leave empty if unsure)",
|
||||
"state_updater": "Globally enable reading states from the KNX Bus",
|
||||
"rate_limit": "Maximum outgoing telegrams per second"
|
||||
}
|
||||
@@ -56,8 +58,7 @@
|
||||
"data": {
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"route_back": "Route Back / NAT Mode",
|
||||
"local_ip": "Local IP (leave empty if unsure)"
|
||||
"route_back": "Route Back / NAT Mode"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"data": {
|
||||
"individual_address": "Individual address for the routing connection",
|
||||
"multicast_group": "The multicast group used for routing",
|
||||
"multicast_port": "The multicast port used for routing"
|
||||
"multicast_port": "The multicast port used for routing",
|
||||
"local_ip": "Local IP (leave empty if unsure)"
|
||||
},
|
||||
"description": "Please configure the routing options."
|
||||
},
|
||||
@@ -48,6 +49,7 @@
|
||||
"individual_address": "Default individual address",
|
||||
"multicast_group": "Multicast group used for routing and discovery",
|
||||
"multicast_port": "Multicast port used for routing and discovery",
|
||||
"local_ip": "Local IP (leave empty if unsure)",
|
||||
"rate_limit": "Maximum outgoing telegrams per second",
|
||||
"state_updater": "Globally enable reading states from the KNX Bus"
|
||||
}
|
||||
@@ -55,7 +57,6 @@
|
||||
"tunnel": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"local_ip": "Local IP (leave empty if unsure)",
|
||||
"port": "Port",
|
||||
"route_back": "Route Back / NAT Mode"
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@ def log_entry(hass, name, message, domain=None, entity_id=None, context=None):
|
||||
hass.add_job(async_log_entry, hass, name, message, domain, entity_id, context)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_log_entry(hass, name, message, domain=None, entity_id=None, context=None):
|
||||
"""Add an entry to the logbook."""
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"disabled": "Library has incompatible requirements.",
|
||||
"domain": "lupusec",
|
||||
"name": "Lupus Electronics LUPUSEC",
|
||||
"documentation": "https://www.home-assistant.io/integrations/lupusec",
|
||||
"requirements": ["lupupy==0.0.21"],
|
||||
"requirements": ["lupupy==0.0.24"],
|
||||
"codeowners": ["@majuss"],
|
||||
"iot_class": "local_polling"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "MELCloud",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/melcloud",
|
||||
"requirements": ["pymelcloud==2.5.5"],
|
||||
"requirements": ["pymelcloud==2.5.6"],
|
||||
"codeowners": ["@vilppuvuorinen"],
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["ffmpeg", "http", "media_source"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/nest",
|
||||
"requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.6"],
|
||||
"requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.9"],
|
||||
"codeowners": ["@allenporter"],
|
||||
"quality_scale": "platinum",
|
||||
"dhcp": [
|
||||
|
||||
@@ -63,6 +63,9 @@ async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
|
||||
|
||||
async def get_media_source_devices(hass: HomeAssistant) -> Mapping[str, Device]:
|
||||
"""Return a mapping of device id to eligible Nest event media devices."""
|
||||
if DATA_SUBSCRIBER not in hass.data[DOMAIN]:
|
||||
# Integration unloaded, or is legacy nest integration
|
||||
return {}
|
||||
subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER]
|
||||
device_manager = await subscriber.async_get_device_manager()
|
||||
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -214,7 +214,7 @@ class NextBusDepartureSensor(SensorEntity):
|
||||
|
||||
# Generate list of upcoming times
|
||||
self._attributes["upcoming"] = ", ".join(
|
||||
sorted(p["minutes"] for p in predictions)
|
||||
sorted((p["minutes"] for p in predictions), key=int)
|
||||
)
|
||||
|
||||
latest_prediction = maybe_first(predictions)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""The 1-Wire component."""
|
||||
import logging
|
||||
|
||||
from pyownet import protocol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
@@ -18,7 +20,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
onewirehub = OneWireHub(hass)
|
||||
try:
|
||||
await onewirehub.initialize(entry)
|
||||
except CannotConnect as exc:
|
||||
except (
|
||||
CannotConnect, # Failed to connect to the server
|
||||
protocol.OwnetError, # Connected to server, but failed to list the devices
|
||||
) as exc:
|
||||
raise ConfigEntryNotReady() from exc
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = onewirehub
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -68,13 +68,12 @@ from .utils import (
|
||||
BLOCK_PLATFORMS: Final = [
|
||||
"binary_sensor",
|
||||
"button",
|
||||
"climate",
|
||||
"cover",
|
||||
"light",
|
||||
"sensor",
|
||||
"switch",
|
||||
]
|
||||
BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "sensor"]
|
||||
BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "climate", "sensor"]
|
||||
RPC_PLATFORMS: Final = ["binary_sensor", "button", "light", "sensor", "switch"]
|
||||
_LOGGER: Final = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any, Final, cast
|
||||
|
||||
from aioshelly.block_device import Block
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.components.climate import ClimateEntity
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity
|
||||
from homeassistant.components.climate.const import (
|
||||
CURRENT_HVAC_HEAT,
|
||||
CURRENT_HVAC_IDLE,
|
||||
@@ -20,11 +21,12 @@ from homeassistant.components.climate.const import (
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
)
|
||||
from homeassistant.components.shelly import BlockDeviceWrapper
|
||||
from homeassistant.components.shelly.entity import ShellyBlockEntity
|
||||
from homeassistant.components.shelly.utils import get_device_entry_gen
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.helpers import device_registry, entity, entity_registry
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
@@ -49,10 +51,29 @@ async def async_setup_entry(
|
||||
if get_device_entry_gen(config_entry) == 2:
|
||||
return
|
||||
|
||||
wrapper: BlockDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
|
||||
config_entry.entry_id
|
||||
][BLOCK]
|
||||
|
||||
if wrapper.device.initialized:
|
||||
await async_setup_climate_entities(async_add_entities, wrapper)
|
||||
else:
|
||||
await async_restore_climate_entities(
|
||||
hass, config_entry, async_add_entities, wrapper
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_climate_entities(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
wrapper: BlockDeviceWrapper,
|
||||
) -> None:
|
||||
"""Set up online climate devices."""
|
||||
|
||||
device_block: Block | None = None
|
||||
sensor_block: Block | None = None
|
||||
|
||||
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK]
|
||||
assert wrapper.device.blocks
|
||||
|
||||
for block in wrapper.device.blocks:
|
||||
if block.type == "device":
|
||||
device_block = block
|
||||
@@ -60,10 +81,38 @@ async def async_setup_entry(
|
||||
sensor_block = block
|
||||
|
||||
if sensor_block and device_block:
|
||||
async_add_entities([ShellyClimate(wrapper, sensor_block, device_block)])
|
||||
_LOGGER.debug("Setup online climate device %s", wrapper.name)
|
||||
async_add_entities([BlockSleepingClimate(wrapper, sensor_block, device_block)])
|
||||
|
||||
|
||||
class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
|
||||
async def async_restore_climate_entities(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
wrapper: BlockDeviceWrapper,
|
||||
) -> None:
|
||||
"""Restore sleeping climate devices."""
|
||||
|
||||
ent_reg = await entity_registry.async_get_registry(hass)
|
||||
entries = entity_registry.async_entries_for_config_entry(
|
||||
ent_reg, config_entry.entry_id
|
||||
)
|
||||
|
||||
for entry in entries:
|
||||
|
||||
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)])
|
||||
|
||||
|
||||
class BlockSleepingClimate(
|
||||
RestoreEntity,
|
||||
ClimateEntity,
|
||||
entity.Entity,
|
||||
):
|
||||
"""Representation of a Shelly climate device."""
|
||||
|
||||
_attr_hvac_modes = [HVAC_MODE_OFF, HVAC_MODE_HEAT]
|
||||
@@ -74,45 +123,77 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
|
||||
_attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS["step"]
|
||||
_attr_temperature_unit = TEMP_CELSIUS
|
||||
|
||||
# pylint: disable=super-init-not-called
|
||||
def __init__(
|
||||
self, wrapper: BlockDeviceWrapper, sensor_block: Block, device_block: Block
|
||||
self,
|
||||
wrapper: BlockDeviceWrapper,
|
||||
sensor_block: Block | None,
|
||||
device_block: Block | None,
|
||||
entry: entity_registry.RegistryEntry | None = None,
|
||||
) -> None:
|
||||
"""Initialize climate."""
|
||||
super().__init__(wrapper, sensor_block)
|
||||
|
||||
self.device_block = device_block
|
||||
|
||||
assert self.block.channel
|
||||
|
||||
self.wrapper = wrapper
|
||||
self.block: Block | None = sensor_block
|
||||
self.control_result: dict[str, Any] | None = None
|
||||
self.device_block: Block | None = device_block
|
||||
self.last_state: State | None = None
|
||||
self.last_state_attributes: MappingProxyType[str, Any]
|
||||
self._preset_modes: list[str] = []
|
||||
|
||||
self._attr_name = self.wrapper.name
|
||||
self._attr_unique_id = self.wrapper.mac
|
||||
self._attr_preset_modes: list[str] = [
|
||||
PRESET_NONE,
|
||||
*wrapper.device.settings["thermostats"][int(self.block.channel)][
|
||||
"schedule_profile_names"
|
||||
],
|
||||
]
|
||||
if self.block is not None and self.device_block is not None:
|
||||
self._unique_id = f"{self.wrapper.mac}-{self.block.description}"
|
||||
assert self.block.channel
|
||||
self._preset_modes = [
|
||||
PRESET_NONE,
|
||||
*wrapper.device.settings["thermostats"][int(self.block.channel)][
|
||||
"schedule_profile_names"
|
||||
],
|
||||
]
|
||||
elif entry is not None:
|
||||
self._unique_id = entry.unique_id
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Set unique id of entity."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Name of entity."""
|
||||
return self.wrapper.name
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""If device should be polled."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Set target temperature."""
|
||||
return cast(float, self.block.targetTemp)
|
||||
if self.block is not None:
|
||||
return cast(float, self.block.targetTemp)
|
||||
return self.last_state_attributes.get("temperature")
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return current temperature."""
|
||||
return cast(float, self.block.temp)
|
||||
if self.block is not None:
|
||||
return cast(float, self.block.temp)
|
||||
return self.last_state_attributes.get("current_temperature")
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Device availability."""
|
||||
return not cast(bool, self.device_block.valveError)
|
||||
if self.device_block is not None:
|
||||
return not cast(bool, self.device_block.valveError)
|
||||
return self.wrapper.last_update_success
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> str:
|
||||
"""HVAC current mode."""
|
||||
if self.device_block is None:
|
||||
return self.last_state.state if self.last_state else HVAC_MODE_OFF
|
||||
if self.device_block.mode is None or self._check_is_off():
|
||||
return HVAC_MODE_OFF
|
||||
|
||||
@@ -121,20 +202,45 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Preset current mode."""
|
||||
if self.device_block is None:
|
||||
return self.last_state_attributes.get("preset_mode")
|
||||
if self.device_block.mode is None:
|
||||
return None
|
||||
return self._attr_preset_modes[cast(int, self.device_block.mode)]
|
||||
return PRESET_NONE
|
||||
return self._preset_modes[cast(int, self.device_block.mode)]
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> str | None:
|
||||
"""HVAC current action."""
|
||||
if self.device_block.status is None or self._check_is_off():
|
||||
if (
|
||||
self.device_block is None
|
||||
or self.device_block.status is None
|
||||
or self._check_is_off()
|
||||
):
|
||||
return CURRENT_HVAC_OFF
|
||||
|
||||
return (
|
||||
CURRENT_HVAC_IDLE if self.device_block.status == "0" else CURRENT_HVAC_HEAT
|
||||
)
|
||||
|
||||
@property
|
||||
def preset_modes(self) -> list[str]:
|
||||
"""Preset available modes."""
|
||||
return self._preset_modes
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Device info."""
|
||||
return {
|
||||
"connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)}
|
||||
}
|
||||
|
||||
@property
|
||||
def channel(self) -> str | None:
|
||||
"""Device channel."""
|
||||
if self.block is not None:
|
||||
return self.block.channel
|
||||
return self.last_state_attributes.get("channel")
|
||||
|
||||
def _check_is_off(self) -> bool:
|
||||
"""Return if valve is off or on."""
|
||||
return bool(
|
||||
@@ -148,7 +254,7 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
|
||||
try:
|
||||
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
|
||||
return await self.wrapper.device.http_request(
|
||||
"get", f"thermostat/{self.block.channel}", kwargs
|
||||
"get", f"thermostat/{self.channel}", kwargs
|
||||
)
|
||||
except (asyncio.TimeoutError, OSError) as err:
|
||||
_LOGGER.error(
|
||||
@@ -186,3 +292,41 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity):
|
||||
await self.set_state_full_path(
|
||||
schedule=1, schedule_profile=f"{preset_index}"
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
_LOGGER.info("Restoring entity %s", self.name)
|
||||
|
||||
last_state = await self.async_get_last_state()
|
||||
|
||||
if last_state is not None:
|
||||
self.last_state = last_state
|
||||
self.last_state_attributes = self.last_state.attributes
|
||||
self._preset_modes = cast(
|
||||
list, self.last_state.attributes.get("preset_modes")
|
||||
)
|
||||
|
||||
self.async_on_remove(self.wrapper.async_add_listener(self._update_callback))
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update entity with latest info."""
|
||||
await self.wrapper.async_request_refresh()
|
||||
|
||||
@callback
|
||||
def _update_callback(self) -> None:
|
||||
"""Handle device update."""
|
||||
if not self.wrapper.device.initialized:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
assert self.wrapper.device.blocks
|
||||
|
||||
for block in self.wrapper.device.blocks:
|
||||
if block.type == "device":
|
||||
self.device_block = block
|
||||
if hasattr(block, "targetTemp"):
|
||||
self.block = block
|
||||
|
||||
_LOGGER.debug("Entity %s attached to block", self.name)
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
@@ -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,10 +647,24 @@ class SimpliSafe:
|
||||
data={**self.entry.data, CONF_TOKEN: token},
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
# 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_save_refresh_token)
|
||||
self._api.add_refresh_token_callback(async_handle_refresh_token)
|
||||
)
|
||||
|
||||
# Save the refresh token we got on entry setup:
|
||||
async_save_refresh_token(self._api.refresh_token)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/smappee",
|
||||
"dependencies": ["http"],
|
||||
"requirements": [
|
||||
"pysmappee==0.2.27"
|
||||
"pysmappee==0.2.29"
|
||||
],
|
||||
"codeowners": [
|
||||
"@bsmappee"
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Tailscale",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/tailscale",
|
||||
"requirements": ["tailscale==0.1.4"],
|
||||
"requirements": ["tailscale==0.1.6"],
|
||||
"codeowners": ["@frenck"],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "cloud_polling"
|
||||
|
||||
@@ -641,9 +641,13 @@ class LightTemplate(TemplateEntity, LightEntity):
|
||||
@callback
|
||||
def _update_color(self, render):
|
||||
"""Update the hs_color from the template."""
|
||||
if render is None:
|
||||
self._color = None
|
||||
return
|
||||
|
||||
h_str = s_str = None
|
||||
if isinstance(render, str):
|
||||
if render in (None, "None", ""):
|
||||
if render in ("None", ""):
|
||||
self._color = None
|
||||
return
|
||||
h_str, s_str = map(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "tibber",
|
||||
"name": "Tibber",
|
||||
"documentation": "https://www.home-assistant.io/integrations/tibber",
|
||||
"requirements": ["pyTibber==0.21.0"],
|
||||
"requirements": ["pyTibber==0.21.1"],
|
||||
"codeowners": ["@danielhiversen"],
|
||||
"quality_scale": "silver",
|
||||
"config_flow": true,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "totalconnect",
|
||||
"name": "Total Connect",
|
||||
"documentation": "https://www.home-assistant.io/integrations/totalconnect",
|
||||
"requirements": ["total_connect_client==2021.11.4"],
|
||||
"requirements": ["total_connect_client==2021.12"],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@austinmroczek"],
|
||||
"config_flow": true,
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -22,6 +22,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import ValloxDataUpdateCoordinator
|
||||
from .const import (
|
||||
@@ -107,7 +108,7 @@ class ValloxFilterRemainingSensor(ValloxSensor):
|
||||
days_remaining_delta = timedelta(days=days_remaining)
|
||||
now = datetime.utcnow().replace(hour=13, minute=0, second=0, microsecond=0)
|
||||
|
||||
return now + days_remaining_delta
|
||||
return (now + days_remaining_delta).astimezone(dt_util.UTC)
|
||||
|
||||
|
||||
class ValloxCellStateSensor(ValloxSensor):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)."""
|
||||
|
||||
@@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Xiaomi Miio",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
|
||||
"requirements": ["construct==2.10.56", "micloud==0.4", "python-miio==0.5.9.1"],
|
||||
"requirements": ["construct==2.10.56", "micloud==0.4", "python-miio==0.5.9.2"],
|
||||
"codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"],
|
||||
"zeroconf": ["_miio._udp.local."],
|
||||
"iot_class": "local_polling"
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -206,11 +206,11 @@ class Thermostat(ZhaEntity, ClimateEntity):
|
||||
|
||||
unoccupied_cooling_setpoint = self._thrm.unoccupied_cooling_setpoint
|
||||
if unoccupied_cooling_setpoint is not None:
|
||||
data[ATTR_UNOCCP_HEAT_SETPT] = unoccupied_cooling_setpoint
|
||||
data[ATTR_UNOCCP_COOL_SETPT] = unoccupied_cooling_setpoint
|
||||
|
||||
unoccupied_heating_setpoint = self._thrm.unoccupied_heating_setpoint
|
||||
if unoccupied_heating_setpoint is not None:
|
||||
data[ATTR_UNOCCP_COOL_SETPT] = unoccupied_heating_setpoint
|
||||
data[ATTR_UNOCCP_HEAT_SETPT] = unoccupied_heating_setpoint
|
||||
return data
|
||||
|
||||
@property
|
||||
|
||||
@@ -234,6 +234,7 @@ class GroupProbe:
|
||||
unsub()
|
||||
self._unsubs.remove(unsub)
|
||||
|
||||
@callback
|
||||
def _reprobe_group(self, group_id: int) -> None:
|
||||
"""Reprobe a group for entities after its members change."""
|
||||
zha_gateway = self._hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum
|
||||
|
||||
MAJOR_VERSION: Final = 2021
|
||||
MINOR_VERSION: Final = 12
|
||||
PATCH_VERSION: Final = "1"
|
||||
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)
|
||||
|
||||
@@ -1052,6 +1052,7 @@ class Script:
|
||||
if self._change_listener_job:
|
||||
self._hass.async_run_hass_job(self._change_listener_job)
|
||||
|
||||
@callback
|
||||
def _chain_change_listener(self, sub_script: Script) -> None:
|
||||
if sub_script.is_running:
|
||||
self.last_action = sub_script.last_action
|
||||
|
||||
@@ -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==20211212.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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+26
-23
@@ -186,7 +186,7 @@ aiohomekit==0.6.4
|
||||
aiohttp_cors==0.7.0
|
||||
|
||||
# homeassistant.components.hue
|
||||
aiohue==3.0.3
|
||||
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.0
|
||||
brunt==1.1.0
|
||||
|
||||
# homeassistant.components.bsblan
|
||||
bsblan==0.4.0
|
||||
@@ -573,7 +573,7 @@ dweepy==0.3.0
|
||||
dynalite_devices==0.1.46
|
||||
|
||||
# homeassistant.components.ebusd
|
||||
ebusdpy==0.0.16
|
||||
ebusdpy==0.0.17
|
||||
|
||||
# homeassistant.components.ecoal_boiler
|
||||
ecoaliface==0.4.0
|
||||
@@ -600,7 +600,7 @@ enocean==0.50
|
||||
enturclient==0.2.2
|
||||
|
||||
# homeassistant.components.environment_canada
|
||||
env_canada==0.5.18
|
||||
env_canada==0.5.20
|
||||
|
||||
# homeassistant.components.envirophat
|
||||
# envirophat==0.0.6
|
||||
@@ -658,7 +658,7 @@ fjaraskupan==1.0.2
|
||||
flipr-api==1.4.1
|
||||
|
||||
# homeassistant.components.flux_led
|
||||
flux_led==0.26.7
|
||||
flux_led==0.27.8
|
||||
|
||||
# homeassistant.components.homekit
|
||||
fnvhash==0.1.0
|
||||
@@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0
|
||||
google-cloud-texttospeech==0.4.0
|
||||
|
||||
# homeassistant.components.nest
|
||||
google-nest-sdm==0.4.6
|
||||
google-nest-sdm==0.4.9
|
||||
|
||||
# homeassistant.components.google_travel_time
|
||||
googlemaps==2.5.1
|
||||
@@ -819,7 +819,7 @@ hole==0.7.0
|
||||
holidays==0.11.3.1
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20211212.0
|
||||
home-assistant-frontend==20211220.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@@ -968,6 +968,9 @@ london-tube-status==0.2
|
||||
# homeassistant.components.luftdaten
|
||||
luftdaten==0.7.1
|
||||
|
||||
# homeassistant.components.lupusec
|
||||
lupupy==0.0.24
|
||||
|
||||
# homeassistant.components.lw12wifi
|
||||
lw12==0.9.2
|
||||
|
||||
@@ -1062,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
|
||||
@@ -1324,7 +1327,7 @@ pyRFXtrx==0.27.0
|
||||
# pySwitchmate==0.4.6
|
||||
|
||||
# homeassistant.components.tibber
|
||||
pyTibber==0.21.0
|
||||
pyTibber==0.21.1
|
||||
|
||||
# homeassistant.components.dlink
|
||||
pyW215==0.7.0
|
||||
@@ -1393,7 +1396,7 @@ pycfdns==1.2.2
|
||||
pychannels==1.0.0
|
||||
|
||||
# homeassistant.components.cast
|
||||
pychromecast==10.1.1
|
||||
pychromecast==10.2.2
|
||||
|
||||
# homeassistant.components.pocketcasts
|
||||
pycketcasts==1.0.0
|
||||
@@ -1432,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
|
||||
@@ -1628,7 +1631,7 @@ pymazda==0.2.2
|
||||
pymediaroom==0.6.4.1
|
||||
|
||||
# homeassistant.components.melcloud
|
||||
pymelcloud==2.5.5
|
||||
pymelcloud==2.5.6
|
||||
|
||||
# homeassistant.components.meteoclimatic
|
||||
pymeteoclimatic==0.0.6
|
||||
@@ -1658,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
|
||||
@@ -1802,7 +1805,7 @@ pyskyqhub==0.1.3
|
||||
pysma==0.6.9
|
||||
|
||||
# homeassistant.components.smappee
|
||||
pysmappee==0.2.27
|
||||
pysmappee==0.2.29
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartapp==0.3.3
|
||||
@@ -1901,7 +1904,7 @@ python-kasa==0.4.0
|
||||
# python-lirc==1.2.3
|
||||
|
||||
# homeassistant.components.xiaomi_miio
|
||||
python-miio==0.5.9.1
|
||||
python-miio==0.5.9.2
|
||||
|
||||
# homeassistant.components.mpd
|
||||
python-mpd2==3.0.4
|
||||
@@ -2004,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
|
||||
@@ -2055,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
|
||||
@@ -2143,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
|
||||
@@ -2269,7 +2272,7 @@ systembridge==2.2.3
|
||||
tahoma-api==0.0.16
|
||||
|
||||
# homeassistant.components.tailscale
|
||||
tailscale==0.1.4
|
||||
tailscale==0.1.6
|
||||
|
||||
# homeassistant.components.tank_utility
|
||||
tank_utility==1.4.0
|
||||
@@ -2326,7 +2329,7 @@ tololib==0.1.0b3
|
||||
toonapi==0.2.1
|
||||
|
||||
# homeassistant.components.totalconnect
|
||||
total_connect_client==2021.11.4
|
||||
total_connect_client==2021.12
|
||||
|
||||
# homeassistant.components.tplink_lte
|
||||
tp-connected==0.0.4
|
||||
@@ -2445,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
|
||||
|
||||
+22
-22
@@ -131,7 +131,7 @@ aiohomekit==0.6.4
|
||||
aiohttp_cors==0.7.0
|
||||
|
||||
# homeassistant.components.hue
|
||||
aiohue==3.0.3
|
||||
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.0
|
||||
brunt==1.1.0
|
||||
|
||||
# homeassistant.components.bsblan
|
||||
bsblan==0.4.0
|
||||
@@ -375,7 +375,7 @@ emulated_roku==0.2.1
|
||||
enocean==0.50
|
||||
|
||||
# homeassistant.components.environment_canada
|
||||
env_canada==0.5.18
|
||||
env_canada==0.5.20
|
||||
|
||||
# homeassistant.components.enphase_envoy
|
||||
envoy_reader==0.20.1
|
||||
@@ -399,7 +399,7 @@ fjaraskupan==1.0.2
|
||||
flipr-api==1.4.1
|
||||
|
||||
# homeassistant.components.flux_led
|
||||
flux_led==0.26.7
|
||||
flux_led==0.27.8
|
||||
|
||||
# homeassistant.components.homekit
|
||||
fnvhash==0.1.0
|
||||
@@ -461,7 +461,7 @@ google-api-python-client==1.6.4
|
||||
google-cloud-pubsub==2.1.0
|
||||
|
||||
# homeassistant.components.nest
|
||||
google-nest-sdm==0.4.6
|
||||
google-nest-sdm==0.4.9
|
||||
|
||||
# homeassistant.components.google_travel_time
|
||||
googlemaps==2.5.1
|
||||
@@ -515,7 +515,7 @@ hole==0.7.0
|
||||
holidays==0.11.3.1
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20211212.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
|
||||
@@ -808,7 +808,7 @@ pyMetno==0.9.0
|
||||
pyRFXtrx==0.27.0
|
||||
|
||||
# homeassistant.components.tibber
|
||||
pyTibber==0.21.0
|
||||
pyTibber==0.21.1
|
||||
|
||||
# homeassistant.components.nextbus
|
||||
py_nextbusnext==0.1.5
|
||||
@@ -850,7 +850,7 @@ pybotvac==0.0.22
|
||||
pycfdns==1.2.2
|
||||
|
||||
# homeassistant.components.cast
|
||||
pychromecast==10.1.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
|
||||
@@ -995,7 +995,7 @@ pymata-express==1.19
|
||||
pymazda==0.2.2
|
||||
|
||||
# homeassistant.components.melcloud
|
||||
pymelcloud==2.5.5
|
||||
pymelcloud==2.5.6
|
||||
|
||||
# homeassistant.components.meteoclimatic
|
||||
pymeteoclimatic==0.0.6
|
||||
@@ -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
|
||||
@@ -1109,7 +1109,7 @@ pysignalclirestapi==0.3.4
|
||||
pysma==0.6.9
|
||||
|
||||
# homeassistant.components.smappee
|
||||
pysmappee==0.2.27
|
||||
pysmappee==0.2.29
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartapp==0.3.3
|
||||
@@ -1145,7 +1145,7 @@ python-juicenet==1.0.2
|
||||
python-kasa==0.4.0
|
||||
|
||||
# homeassistant.components.xiaomi_miio
|
||||
python-miio==0.5.9.1
|
||||
python-miio==0.5.9.2
|
||||
|
||||
# homeassistant.components.nest
|
||||
python-nest==4.1.0
|
||||
@@ -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.4
|
||||
tailscale==0.1.6
|
||||
|
||||
# homeassistant.components.tellduslive
|
||||
tellduslive==0.10.11
|
||||
@@ -1370,7 +1370,7 @@ tololib==0.1.0b3
|
||||
toonapi==0.2.1
|
||||
|
||||
# homeassistant.components.totalconnect
|
||||
total_connect_client==2021.11.4
|
||||
total_connect_client==2021.12
|
||||
|
||||
# homeassistant.components.transmission
|
||||
transmissionrpc==0.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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
@@ -754,7 +756,7 @@ async def test_supported_features(
|
||||
assert state.attributes.get("supported_features") == supported_features
|
||||
|
||||
|
||||
async def test_entity_play_media(hass: HomeAssistant):
|
||||
async def test_entity_play_media(hass: HomeAssistant, quick_play_mock):
|
||||
"""Test playing media."""
|
||||
entity_id = "media_player.speaker"
|
||||
reg = er.async_get(hass)
|
||||
@@ -776,8 +778,28 @@ async def test_entity_play_media(hass: HomeAssistant):
|
||||
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
|
||||
|
||||
# Play_media
|
||||
await common.async_play_media(hass, "audio", "best.mp3", entity_id)
|
||||
chromecast.media_controller.play_media.assert_called_once_with("best.mp3", "audio")
|
||||
await hass.services.async_call(
|
||||
media_player.DOMAIN,
|
||||
media_player.SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
media_player.ATTR_MEDIA_CONTENT_TYPE: "audio",
|
||||
media_player.ATTR_MEDIA_CONTENT_ID: "best.mp3",
|
||||
media_player.ATTR_MEDIA_EXTRA: {"metadata": {"metadatatype": 3}},
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
chromecast.media_controller.play_media.assert_not_called()
|
||||
quick_play_mock.assert_called_once_with(
|
||||
chromecast,
|
||||
"default_media_receiver",
|
||||
{
|
||||
"media_id": "best.mp3",
|
||||
"media_type": "audio",
|
||||
"metadata": {"metadatatype": 3},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def test_entity_play_media_cast(hass: HomeAssistant, quick_play_mock):
|
||||
@@ -865,7 +887,7 @@ async def test_entity_play_media_cast_invalid(hass, caplog, quick_play_mock):
|
||||
assert "App unknown not supported" in caplog.text
|
||||
|
||||
|
||||
async def test_entity_play_media_sign_URL(hass: HomeAssistant):
|
||||
async def test_entity_play_media_sign_URL(hass: HomeAssistant, quick_play_mock):
|
||||
"""Test playing media."""
|
||||
entity_id = "media_player.speaker"
|
||||
|
||||
@@ -886,8 +908,10 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistant):
|
||||
|
||||
# Play_media
|
||||
await common.async_play_media(hass, "audio", "/best.mp3", entity_id)
|
||||
chromecast.media_controller.play_media.assert_called_once_with(ANY, "audio")
|
||||
assert chromecast.media_controller.play_media.call_args[0][0].startswith(
|
||||
quick_play_mock.assert_called_once_with(
|
||||
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="
|
||||
)
|
||||
|
||||
@@ -1231,7 +1255,7 @@ async def test_group_media_states(hass, mz_mock):
|
||||
assert state.state == "playing"
|
||||
|
||||
|
||||
async def test_group_media_control(hass, mz_mock):
|
||||
async def test_group_media_control(hass, mz_mock, quick_play_mock):
|
||||
"""Test media controls are handled by group if entity has no state."""
|
||||
entity_id = "media_player.speaker"
|
||||
reg = er.async_get(hass)
|
||||
@@ -1286,7 +1310,12 @@ async def test_group_media_control(hass, mz_mock):
|
||||
# Verify play_media is not forwarded
|
||||
await common.async_play_media(hass, "music", "best.mp3", entity_id)
|
||||
assert not grp_media.play_media.called
|
||||
assert chromecast.media_controller.play_media.called
|
||||
assert not chromecast.media_controller.play_media.called
|
||||
quick_play_mock.assert_called_once_with(
|
||||
chromecast,
|
||||
"default_media_receiver",
|
||||
{"media_id": "best.mp3", "media_type": "music"},
|
||||
)
|
||||
|
||||
|
||||
async def test_failed_cast_on_idle(hass, caplog):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -121,6 +121,17 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat
|
||||
assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is True
|
||||
assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 6000
|
||||
|
||||
# test again with sending flash/alert
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{"entity_id": test_light_id, "flash": "long"},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(mock_bridge_v2.mock_requests) == 3
|
||||
assert mock_bridge_v2.mock_requests[2]["json"]["on"]["on"] is True
|
||||
assert mock_bridge_v2.mock_requests[2]["json"]["alert"]["action"] == "breathe"
|
||||
|
||||
|
||||
async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_data):
|
||||
"""Test calling the turn off service on a light."""
|
||||
@@ -295,7 +306,12 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data):
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{"entity_id": test_light_id, "brightness_pct": 100, "xy_color": (0.123, 0.123)},
|
||||
{
|
||||
"entity_id": test_light_id,
|
||||
"brightness_pct": 100,
|
||||
"xy_color": (0.123, 0.123),
|
||||
"transition": 6,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
@@ -308,6 +324,9 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data):
|
||||
)
|
||||
assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["x"] == 0.123
|
||||
assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["y"] == 0.123
|
||||
assert (
|
||||
mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 6000
|
||||
)
|
||||
|
||||
# Now generate update events by emitting the json we've sent as incoming events
|
||||
for index in range(0, 3):
|
||||
@@ -346,3 +365,24 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data):
|
||||
test_light = hass.states.get(test_light_id)
|
||||
assert test_light is not None
|
||||
assert test_light.state == "off"
|
||||
|
||||
# Test calling the turn off service on a grouped light with transition
|
||||
mock_bridge_v2.mock_requests.clear()
|
||||
test_light_id = "light.test_zone"
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_off",
|
||||
{
|
||||
"entity_id": test_light_id,
|
||||
"transition": 6,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# PUT request should have been sent to ALL group lights with correct params
|
||||
assert len(mock_bridge_v2.mock_requests) == 3
|
||||
for index in range(0, 3):
|
||||
assert mock_bridge_v2.mock_requests[index]["json"]["on"]["on"] is False
|
||||
assert (
|
||||
mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 6000
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
@@ -83,6 +91,60 @@ async def test_routing_setup(hass: HomeAssistant) -> None:
|
||||
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
|
||||
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
|
||||
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
|
||||
ConnectionSchema.CONF_KNX_LOCAL_IP: None,
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110",
|
||||
}
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_routing_setup_advanced(hass: HomeAssistant) -> None:
|
||||
"""Test routing setup with advanced options."""
|
||||
with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways:
|
||||
gateways.return_value = []
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": config_entries.SOURCE_USER,
|
||||
"show_advanced_options": True,
|
||||
},
|
||||
)
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert not result["errors"]
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result2["type"] == RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == "routing"
|
||||
assert not result2["errors"]
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.knx.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
|
||||
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110",
|
||||
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result3["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||
assert result3["title"] == CONF_KNX_ROUTING.capitalize()
|
||||
assert result3["data"] == {
|
||||
**DEFAULT_ENTRY_DATA,
|
||||
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
|
||||
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
|
||||
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
|
||||
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110",
|
||||
}
|
||||
|
||||
@@ -144,7 +206,11 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None:
|
||||
with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways:
|
||||
gateways.return_value = [gateway]
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": config_entries.SOURCE_USER,
|
||||
"show_advanced_options": True,
|
||||
},
|
||||
)
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert not result["errors"]
|
||||
@@ -563,7 +629,6 @@ async def test_tunneling_options_flow(
|
||||
CONF_HOST: "192.168.1.1",
|
||||
CONF_PORT: 3675,
|
||||
ConnectionSchema.CONF_KNX_ROUTE_BACK: True,
|
||||
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -581,7 +646,6 @@ async def test_tunneling_options_flow(
|
||||
CONF_HOST: "192.168.1.1",
|
||||
CONF_PORT: 3675,
|
||||
ConnectionSchema.CONF_KNX_ROUTE_BACK: True,
|
||||
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
|
||||
}
|
||||
|
||||
|
||||
@@ -611,6 +675,7 @@ async def test_advanced_options(
|
||||
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
|
||||
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
|
||||
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
|
||||
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -626,4 +691,5 @@ async def test_advanced_options(
|
||||
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
|
||||
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
|
||||
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
|
||||
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Test KNX init."""
|
||||
import pytest
|
||||
from xknx import XKNX
|
||||
from xknx.io import ConnectionConfig, ConnectionType
|
||||
|
||||
from homeassistant.components.knx.const import (
|
||||
CONF_KNX_AUTOMATIC,
|
||||
CONF_KNX_CONNECTION_TYPE,
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS,
|
||||
CONF_KNX_ROUTING,
|
||||
CONF_KNX_TUNNELING,
|
||||
DOMAIN as KNX_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.knx.schema import ConnectionSchema
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .conftest import KNXTestKit
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config_entry_data,connection_config",
|
||||
[
|
||||
(
|
||||
{
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS,
|
||||
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
|
||||
},
|
||||
ConnectionConfig(),
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
|
||||
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.1",
|
||||
},
|
||||
ConnectionConfig(
|
||||
connection_type=ConnectionType.ROUTING, local_ip="192.168.1.1"
|
||||
),
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING,
|
||||
CONF_HOST: "192.168.0.2",
|
||||
CONF_PORT: 3675,
|
||||
ConnectionSchema.CONF_KNX_ROUTE_BACK: False,
|
||||
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
|
||||
},
|
||||
ConnectionConfig(
|
||||
connection_type=ConnectionType.TUNNELING,
|
||||
route_back=False,
|
||||
gateway_ip="192.168.0.2",
|
||||
gateway_port=3675,
|
||||
local_ip="192.168.1.112",
|
||||
auto_reconnect=True,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_init_connection_handling(
|
||||
hass: HomeAssistant, knx: KNXTestKit, config_entry_data, connection_config
|
||||
):
|
||||
"""Test correctly generating connection config."""
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
title="KNX",
|
||||
domain=KNX_DOMAIN,
|
||||
data=config_entry_data,
|
||||
)
|
||||
knx.mock_config_entry = config_entry
|
||||
await knx.setup_integration({})
|
||||
|
||||
assert hass.data.get(KNX_DOMAIN) is not None
|
||||
|
||||
assert (
|
||||
hass.data[KNX_DOMAIN].connection_config().__dict__ == connection_config.__dict__
|
||||
)
|
||||
@@ -39,13 +39,12 @@ async def async_setup_devices(hass, device_type, traits={}):
|
||||
return await async_setup_sdm_platform(hass, PLATFORM, devices=devices)
|
||||
|
||||
|
||||
def create_device_traits(event_trait):
|
||||
def create_device_traits(event_traits=[]):
|
||||
"""Create fake traits for a device."""
|
||||
return {
|
||||
result = {
|
||||
"sdm.devices.traits.Info": {
|
||||
"customName": "Front",
|
||||
},
|
||||
event_trait: {},
|
||||
"sdm.devices.traits.CameraLiveStream": {
|
||||
"maxVideoResolution": {
|
||||
"width": 640,
|
||||
@@ -55,6 +54,8 @@ def create_device_traits(event_trait):
|
||||
"audioCodecs": ["AAC"],
|
||||
},
|
||||
}
|
||||
result.update({t: {} for t in event_traits})
|
||||
return result
|
||||
|
||||
|
||||
def create_event(event_type, device_id=DEVICE_ID, timestamp=None):
|
||||
@@ -91,7 +92,7 @@ async def test_doorbell_chime_event(hass):
|
||||
subscriber = await async_setup_devices(
|
||||
hass,
|
||||
"sdm.devices.types.DOORBELL",
|
||||
create_device_traits("sdm.devices.traits.DoorbellChime"),
|
||||
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
|
||||
)
|
||||
|
||||
registry = er.async_get(hass)
|
||||
@@ -129,7 +130,7 @@ async def test_camera_motion_event(hass):
|
||||
subscriber = await async_setup_devices(
|
||||
hass,
|
||||
"sdm.devices.types.CAMERA",
|
||||
create_device_traits("sdm.devices.traits.CameraMotion"),
|
||||
create_device_traits(["sdm.devices.traits.CameraMotion"]),
|
||||
)
|
||||
registry = er.async_get(hass)
|
||||
entry = registry.async_get("camera.front")
|
||||
@@ -157,7 +158,7 @@ async def test_camera_sound_event(hass):
|
||||
subscriber = await async_setup_devices(
|
||||
hass,
|
||||
"sdm.devices.types.CAMERA",
|
||||
create_device_traits("sdm.devices.traits.CameraSound"),
|
||||
create_device_traits(["sdm.devices.traits.CameraSound"]),
|
||||
)
|
||||
registry = er.async_get(hass)
|
||||
entry = registry.async_get("camera.front")
|
||||
@@ -185,7 +186,7 @@ async def test_camera_person_event(hass):
|
||||
subscriber = await async_setup_devices(
|
||||
hass,
|
||||
"sdm.devices.types.DOORBELL",
|
||||
create_device_traits("sdm.devices.traits.CameraEventImage"),
|
||||
create_device_traits(["sdm.devices.traits.CameraPerson"]),
|
||||
)
|
||||
registry = er.async_get(hass)
|
||||
entry = registry.async_get("camera.front")
|
||||
@@ -213,7 +214,9 @@ async def test_camera_multiple_event(hass):
|
||||
subscriber = await async_setup_devices(
|
||||
hass,
|
||||
"sdm.devices.types.DOORBELL",
|
||||
create_device_traits("sdm.devices.traits.CameraEventImage"),
|
||||
create_device_traits(
|
||||
["sdm.devices.traits.CameraMotion", "sdm.devices.traits.CameraPerson"]
|
||||
),
|
||||
)
|
||||
registry = er.async_get(hass)
|
||||
entry = registry.async_get("camera.front")
|
||||
@@ -256,7 +259,7 @@ async def test_unknown_event(hass):
|
||||
subscriber = await async_setup_devices(
|
||||
hass,
|
||||
"sdm.devices.types.DOORBELL",
|
||||
create_device_traits("sdm.devices.traits.DoorbellChime"),
|
||||
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
|
||||
)
|
||||
await subscriber.async_receive_event(create_event("some-event-id"))
|
||||
await hass.async_block_till_done()
|
||||
@@ -270,7 +273,7 @@ async def test_unknown_device_id(hass):
|
||||
subscriber = await async_setup_devices(
|
||||
hass,
|
||||
"sdm.devices.types.DOORBELL",
|
||||
create_device_traits("sdm.devices.traits.DoorbellChime"),
|
||||
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
|
||||
)
|
||||
await subscriber.async_receive_event(
|
||||
create_event("sdm.devices.events.DoorbellChime.Chime", "invalid-device-id")
|
||||
@@ -286,7 +289,7 @@ async def test_event_message_without_device_event(hass):
|
||||
subscriber = await async_setup_devices(
|
||||
hass,
|
||||
"sdm.devices.types.DOORBELL",
|
||||
create_device_traits("sdm.devices.traits.DoorbellChime"),
|
||||
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
|
||||
)
|
||||
timestamp = utcnow()
|
||||
event = EventMessage(
|
||||
@@ -308,14 +311,12 @@ async def test_doorbell_event_thread(hass):
|
||||
subscriber = await async_setup_devices(
|
||||
hass,
|
||||
"sdm.devices.types.DOORBELL",
|
||||
traits={
|
||||
"sdm.devices.traits.Info": {
|
||||
"customName": "Front",
|
||||
},
|
||||
"sdm.devices.traits.CameraLiveStream": {},
|
||||
"sdm.devices.traits.CameraClipPreview": {},
|
||||
"sdm.devices.traits.CameraPerson": {},
|
||||
},
|
||||
create_device_traits(
|
||||
[
|
||||
"sdm.devices.traits.CameraClipPreview",
|
||||
"sdm.devices.traits.CameraPerson",
|
||||
]
|
||||
),
|
||||
)
|
||||
registry = er.async_get(hass)
|
||||
entry = registry.async_get("camera.front")
|
||||
@@ -351,7 +352,7 @@ async def test_doorbell_event_thread(hass):
|
||||
)
|
||||
await subscriber.async_receive_event(EventMessage(message_data_1, auth=None))
|
||||
|
||||
# Publish message #1 that sends a no-op update to end the event thread
|
||||
# Publish message #2 that sends a no-op update to end the event thread
|
||||
timestamp2 = timestamp1 + datetime.timedelta(seconds=1)
|
||||
message_data_2 = event_message_data.copy()
|
||||
message_data_2.update(
|
||||
@@ -371,3 +372,77 @@ async def test_doorbell_event_thread(hass):
|
||||
"timestamp": timestamp1.replace(microsecond=0),
|
||||
"nest_event_id": EVENT_SESSION_ID,
|
||||
}
|
||||
|
||||
|
||||
async def test_doorbell_event_session_update(hass):
|
||||
"""Test a pubsub message with updates to an existing session."""
|
||||
events = async_capture_events(hass, NEST_EVENT)
|
||||
subscriber = await async_setup_devices(
|
||||
hass,
|
||||
"sdm.devices.types.DOORBELL",
|
||||
create_device_traits(
|
||||
[
|
||||
"sdm.devices.traits.CameraClipPreview",
|
||||
"sdm.devices.traits.CameraPerson",
|
||||
"sdm.devices.traits.CameraMotion",
|
||||
]
|
||||
),
|
||||
)
|
||||
registry = er.async_get(hass)
|
||||
entry = registry.async_get("camera.front")
|
||||
assert entry is not None
|
||||
|
||||
# Message #1 has a motion event
|
||||
timestamp1 = utcnow()
|
||||
await subscriber.async_receive_event(
|
||||
create_events(
|
||||
{
|
||||
"sdm.devices.events.CameraMotion.Motion": {
|
||||
"eventSessionId": EVENT_SESSION_ID,
|
||||
"eventId": "n:1",
|
||||
},
|
||||
"sdm.devices.events.CameraClipPreview.ClipPreview": {
|
||||
"eventSessionId": EVENT_SESSION_ID,
|
||||
"previewUrl": "image-url-1",
|
||||
},
|
||||
},
|
||||
timestamp=timestamp1,
|
||||
)
|
||||
)
|
||||
|
||||
# Message #2 has an extra person event
|
||||
timestamp2 = utcnow()
|
||||
await subscriber.async_receive_event(
|
||||
create_events(
|
||||
{
|
||||
"sdm.devices.events.CameraMotion.Motion": {
|
||||
"eventSessionId": EVENT_SESSION_ID,
|
||||
"eventId": "n:1",
|
||||
},
|
||||
"sdm.devices.events.CameraPerson.Person": {
|
||||
"eventSessionId": EVENT_SESSION_ID,
|
||||
"eventId": "n:2",
|
||||
},
|
||||
"sdm.devices.events.CameraClipPreview.ClipPreview": {
|
||||
"eventSessionId": EVENT_SESSION_ID,
|
||||
"previewUrl": "image-url-1",
|
||||
},
|
||||
},
|
||||
timestamp=timestamp2,
|
||||
)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(events) == 2
|
||||
assert events[0].data == {
|
||||
"device_id": entry.device_id,
|
||||
"type": "camera_motion",
|
||||
"timestamp": timestamp1.replace(microsecond=0),
|
||||
"nest_event_id": EVENT_SESSION_ID,
|
||||
}
|
||||
assert events[1].data == {
|
||||
"device_id": entry.device_id,
|
||||
"type": "camera_person",
|
||||
"timestamp": timestamp2.replace(microsecond=0),
|
||||
"nest_event_id": EVENT_SESSION_ID,
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.components import media_source
|
||||
from homeassistant.components.media_player.errors import BrowseError
|
||||
from homeassistant.components.media_source import const
|
||||
from homeassistant.components.media_source.error import Unresolvable
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.template import DATE_STR_FORMAT
|
||||
import homeassistant.util.dt as dt_util
|
||||
@@ -164,6 +165,37 @@ async def test_supported_device(hass, auth):
|
||||
assert len(browse.children) == 0
|
||||
|
||||
|
||||
async def test_integration_unloaded(hass, auth):
|
||||
"""Test the media player loads, but has no devices, when config unloaded."""
|
||||
await async_setup_devices(
|
||||
hass,
|
||||
auth,
|
||||
CAMERA_DEVICE_TYPE,
|
||||
CAMERA_TRAITS,
|
||||
)
|
||||
|
||||
browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}")
|
||||
assert browse.domain == DOMAIN
|
||||
assert browse.identifier == ""
|
||||
assert browse.title == "Nest"
|
||||
assert len(browse.children) == 1
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
entry = entries[0]
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
assert entry.state == ConfigEntryState.NOT_LOADED
|
||||
|
||||
# No devices returned
|
||||
browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}")
|
||||
assert browse.domain == DOMAIN
|
||||
assert browse.identifier == ""
|
||||
assert browse.title == "Nest"
|
||||
assert len(browse.children) == 0
|
||||
|
||||
|
||||
async def test_camera_event(hass, auth, hass_client):
|
||||
"""Test a media source and image created for an event."""
|
||||
event_timestamp = dt_util.now()
|
||||
|
||||
@@ -40,6 +40,7 @@ BASIC_RESULTS = {
|
||||
{"minutes": "1", "epochTime": "1553807371000"},
|
||||
{"minutes": "2", "epochTime": "1553807372000"},
|
||||
{"minutes": "3", "epochTime": "1553807373000"},
|
||||
{"minutes": "10", "epochTime": "1553807380000"},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -128,7 +129,7 @@ async def test_verify_valid_state(
|
||||
assert state.attributes["route"] == VALID_ROUTE_TITLE
|
||||
assert state.attributes["stop"] == VALID_STOP_TITLE
|
||||
assert state.attributes["direction"] == "Outbound"
|
||||
assert state.attributes["upcoming"] == "1, 2, 3"
|
||||
assert state.attributes["upcoming"] == "1, 2, 3, 10"
|
||||
|
||||
|
||||
async def test_message_dict(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Tests for 1-Wire config flow."""
|
||||
import logging
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from pyownet import protocol
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.onewire.const import DOMAIN
|
||||
@@ -19,6 +21,20 @@ async def test_owserver_connect_failure(hass: HomeAssistant, config_entry: Confi
|
||||
assert not hass.data.get(DOMAIN)
|
||||
|
||||
|
||||
async def test_owserver_listing_failure(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock
|
||||
):
|
||||
"""Test listing failure raises ConfigEntryNotReady."""
|
||||
owproxy.return_value.dir.side_effect = protocol.OwnetError()
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
assert not hass.data.get(DOMAIN)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("owproxy")
|
||||
async def test_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
"""Test being able to unload an entry."""
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user