Compare commits

...

82 Commits

Author SHA1 Message Date
Paulus Schoutsen 20a1bc710e Merge pull request #62366 from home-assistant/rc 2021-12-20 19:58:40 -08:00
Marcel van der Veldt 5e0ea9fd24 Change Hue availability blacklist logic a bit (#62446) 2021-12-20 16:09:32 -08:00
Marcel van der Veldt 1f0c13f259 bump aiohue to 3.0.7 (#62444) 2021-12-20 16:09:31 -08:00
rikroe a1fc223914 Bump bimmer_connected to 0.8.7 (#62435)
Co-authored-by: rikroe <rikroe@users.noreply.github.com>
2021-12-20 16:09:30 -08:00
Erik Montnemery 836d8a6fca Make it possible to turn on audio only google cast devices (#62420) 2021-12-20 11:18:28 -08:00
Franck Nijhof 520c3411dd Invalidate CI cache when bumping dependencies (#62394)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2021-12-20 10:36:39 -08:00
Franck Nijhof d90c107b1b Invalidate CI cache when bumping dependencies, part 2 (#62412) 2021-12-20 10:34:27 -08:00
Matthias Alphart 5a2bc8e493 Update xknx to 0.18.14 (#62411)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2021-12-20 10:26:27 -08:00
Erik Montnemery b8f8b30b9b Bump pychromecast to 10.2.2 (#62390) 2021-12-20 10:26:27 -08:00
Bram Kragten e80f4e03a4 Update frontend to 20211220.0 (#62389) 2021-12-20 10:26:26 -08:00
Eduard van Valkenburg 7ec369d8ef Bump brunt to 1.1.0 (#62386) 2021-12-20 10:25:50 -08:00
Erik Montnemery dcc08a0aac Don't use the homeassistant media app when casting media (#62385) 2021-12-20 10:24:40 -08:00
Paulus Schoutsen 4802e4e33f Bumped version to 2021.12.4 2021-12-19 22:32:50 -08:00
Paulus Schoutsen eb4b041d45 Bump voluptuous_serialize to 2.5.0 (#62363) 2021-12-19 22:32:37 -08:00
Eric Severance bfd8579566 Bump pywemo==0.7.0 (#62360) 2021-12-19 22:32:36 -08:00
Aaron Bach beb5a992e6 Ensure existing SimpliSafe websocket tasks are cancelled appropriately (#62347) 2021-12-19 22:32:36 -08:00
Maikel Punie 7c925778eb Fix velbus climate current temp (#62329) 2021-12-19 22:32:35 -08:00
Thijs Walcarius 7db161868e Fix missing brightness for Velbus entities (#62314)
* Fix #62169: missing brightness for Velbus-entities

* Use default implementation of supported_features

Co-authored-by: Thijs Walcarius <thijs.walcarius@ugent.be>
2021-12-19 22:32:34 -08:00
Paulus Schoutsen 5f2a2280c5 Bump ring to 0.7.2 (#62299) 2021-12-19 22:32:33 -08:00
Michael Chisholm b327628b6e Update async-upnp-client library to 0.23.1 (#62298) 2021-12-19 22:32:20 -08:00
Diego Elio Pettenò 311ebd4a96 Bump async-upnp-client to 0.23.0 (#62223) 2021-12-19 22:31:18 -08:00
J. Nick Koston dc4659b167 Bump flux_led to 0.27.8 to fix discovery of older devices (#62292) 2021-12-19 22:29:51 -08:00
J. Nick Koston c1d0fe9eae Fix Non-thread-safe operation in zwave node_added (#62287) 2021-12-19 22:29:50 -08:00
J. Nick Koston 3fde6bfd73 Fix Non-thread-safe operation in rflink binary_sensor (#62286) 2021-12-19 22:29:49 -08:00
Martin Hjelmare 4efa3b634e Fix fitbit no SSL URL handling (#62270) 2021-12-19 22:29:47 -08:00
Franck Nijhof cd65aaee60 Upgrade tailscale to 0.1.6 (#62267) 2021-12-19 22:29:47 -08:00
Simone Chemelli 2395c753fe Fix logging for Shelly climate platform (#62264)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2021-12-19 22:29:46 -08:00
starkillerOG bc2949ef31 bump pynetgear to 0.8.0 (#62261) 2021-12-19 22:29:45 -08:00
Aaron Bach f665c4e588 Fix bug in which SimpliSafe websocket won't reconnect on error (#62241) 2021-12-19 22:29:45 -08:00
Aaron Bach 57b7b28d60 Fix spurious RainMachine config entry reload (#62215) 2021-12-19 22:29:44 -08:00
Gage Benne 9a0f42f9a7 Bump pydexcom to 0.2.2 (#62207) 2021-12-19 22:29:43 -08:00
Aidan Timson 895dcaf690 Force Lyric token refresh on first authentication failure (#62100) 2021-12-19 22:29:43 -08:00
J. Nick Koston 4e96ff78b5 Avoid setting nexia humidity to the same value since it causes the api to fail (#61843) 2021-12-19 22:29:42 -08:00
Matthias Alphart a87ed13a04 Silently retry Fronius inverter endpoint 2 times (#61826) 2021-12-19 22:29:42 -08:00
jkuettner 735deff45e Fix "vevent" KeyError in caldav component (#61718) 2021-12-19 22:29:41 -08:00
Hans Oischinger 22867acaf8 Add vicare strings (#61593)
* Add vicare strings

* Remove duplicates

* Remove duplicates from english translation

* Add missing strings
2021-12-19 22:29:40 -08:00
RDFurman c507c72350 Honeywell unique id fix (#59393)
* Move error logging and remove reload

* Change device assignment and improve logging

* Use dictionary for devices

* Check if new device exists in API response

* Add test and make loop better

* Make test assert on error in log
2021-12-19 22:29:40 -08:00
Franck Nijhof 7fc36c4fe0 Merge pull request #62182 from home-assistant/rc 2021-12-17 13:43:37 +01:00
Franck Nijhof 5196a770cc Bumped version to 2021.12.3 2021-12-17 11:43:38 +01:00
Erik Montnemery 54d7380f4d Fix threading error in zha (#62170) 2021-12-17 11:43:04 +01:00
Erik Montnemery c445e93d45 Fix threading error in scripts with repeat or choose actions (#62168) 2021-12-17 11:43:00 +01:00
Marcel van der Veldt 614529d7c3 Add guard in call to activate_scene in Hue (#62177) 2021-12-17 11:41:04 +01:00
Allen Porter 9361c9ef60 Bump google-nest-sdm to 0.4.9 (#62160) 2021-12-17 11:41:00 +01:00
J. Nick Koston 19a0644b50 Fix Non-thread-safe operation in logbook (#62148) 2021-12-17 11:40:57 +01:00
J. Nick Koston 82173f477c Fix Non-thread-safe operation in homekit light events (#62147) 2021-12-17 11:40:54 +01:00
Simone Chemelli b4af32624d Improve availability for Shelly Valve (#62129) 2021-12-17 11:40:50 +01:00
Maximilian 78f40bd4bf Add missing timezone information (#62106) 2021-12-17 11:40:47 +01:00
Erik Montnemery d92ad76ed9 Fix none-check in template light (#62089) 2021-12-17 11:40:44 +01:00
J. Nick Koston e44d50e1b1 Bump flux_led to 0.26.15 (#62017) 2021-12-17 11:40:40 +01:00
Eduard van Valkenburg 95c0eeecfb Brunt dependency bump to 1.0.2 (#62014) 2021-12-17 11:40:37 +01:00
Marcel van der Veldt ec263840ba Bump aiohue to 3.0.6 (#61974) 2021-12-17 11:40:34 +01:00
Marvin Wichmann 8047134c88 Fix notify platform setup for KNX (#61842)
* Fix notify platform setup for KNX

* Apply review suggestions

* Store hass config in DATA_HASS_CONFIG

* Readd guard clause
2021-12-17 11:40:31 +01:00
epenet cb89688873 Fix OwnetError preventing onewire initialisation (#61696)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2021-12-17 11:40:27 +01:00
Simone Chemelli c73319e162 Add restore logic to Shelly climate platform (#61632)
* Add restore logic to Shelly climate platform

* Handle missing channel on restore
2021-12-17 11:39:47 +01:00
Ian 499cc2e51d Nextbus upcoming sort as integer (#61416) 2021-12-17 11:33:21 +01:00
sindudas 5a03fffc20 Update ebusdpy version (#59899) 2021-12-17 11:33:17 +01:00
Franck Nijhof 6d8d472f0f Merge pull request #61902 from home-assistant/rc 2021-12-15 17:02:35 +01:00
Franck Nijhof ac2897fc67 Bumped version to 2021.12.2 2021-12-15 16:04:48 +01:00
Bram Kragten e7e20533bd Update frontend to 20211215.0 (#61877) 2021-12-15 16:03:37 +01:00
Marcel van der Veldt 2772bae2e1 Bump aiohue to 3.0.5 (#61875) 2021-12-15 16:03:34 +01:00
Allen Porter 86622794e0 Bump google-nest-sdm to 0.4.8 (#61851) 2021-12-15 16:03:30 +01:00
Michael Davie 686f6768fc Fix broken Environment Canada (#61848) 2021-12-15 16:03:27 +01:00
Marvin Wichmann f271fea07c Allow setting local_ip for knx routing connections (#61836) 2021-12-15 16:03:24 +01:00
Aaron Bach 77b1df5902 Ensure SimpliSafe websocket reconnects upon new token (#61835) 2021-12-15 16:03:20 +01:00
Teemu R 1faa111222 Bump python-miio to 0.5.9.2 (#61831) 2021-12-15 16:03:17 +01:00
Daniel Hjelseth Høyer b513301363 Tibber, update library, fixes #61525 (#61813) 2021-12-15 16:03:14 +01:00
Erik Montnemery 32bdcdd663 Bump pychromecast to 10.2.1 (#61811) 2021-12-15 16:03:11 +01:00
Erik Montnemery 40f76d4ed9 Don't override pychromecast MediaController's APP ID (#61796) 2021-12-15 16:03:07 +01:00
MattWestb 34568aad89 Fix ZHA unoccupied setpoints. (#61791)
ATTR_UNOCCP_HEAT_SETPT and ATTR_UNOCCP_COOL_SETPT is mixed up. 
Fixing so heating is heating and cooling is colling.
2021-12-15 16:03:04 +01:00
Eduard van Valkenburg ffe84e8ece Bump brunt package to 1.0.1 (#61784) 2021-12-15 16:03:01 +01:00
Franck Nijhof 8cbd89282b Upgrade tailscale to 0.1.5 (#61744) 2021-12-15 16:02:58 +01:00
Marcel van der Veldt 1467668c94 Blacklist availability check for a light at startup in Hue integration (#61737) 2021-12-15 16:02:55 +01:00
Marcel van der Veldt bbef38964d Fix Flash effect for Hue lights (#61733) 2021-12-15 16:02:52 +01:00
Marcel van der Veldt 03b88af032 Fix turn_off with transition for grouped Hue lights (#61728)
* fix turn_off with transition for grouped hue lights

* add test
2021-12-15 16:02:49 +01:00
Marcel van der Veldt 0626bc8b4f Add check for incompatible device trigger in Hue integration (#61726) 2021-12-15 16:02:46 +01:00
Vilppu Vuorinen 37ecbc53a7 Update pymelcloud to 2.5.6 (#61717) 2021-12-15 16:02:43 +01:00
Paulus Schoutsen 52c96654a4 Bump aiohue to 3.0.4 (#61709) 2021-12-15 16:02:39 +01:00
Joakim Sørensen 791c2f4b8a Add additional-tag to machine builds (#61693) 2021-12-15 16:02:36 +01:00
Austin Mroczek ed041d5b7c Bump total_connect_client to 2021.12 (#61634) 2021-12-15 16:02:33 +01:00
Allen Porter 1833ab96dc Suppress errors for legacy nest api when using media source (#61629) 2021-12-15 16:02:29 +01:00
majuss ff2e2656b3 Upgrade lupupy to 0.0.24 (#61598) 2021-12-15 16:02:26 +01:00
bsmappee 599c20c76e Bump pysmappee to 0.2.29 (#61160) 2021-12-15 16:02:19 +01:00
92 changed files with 1181 additions and 283 deletions
+13 -2
View File
@@ -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 \
+18 -8
View File
@@ -155,10 +155,15 @@ jobs:
key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
steps.generate-python-key.outputs.key }}
restore-keys: |
${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-
# Temporary disabling the restore of environments when bumping
# a dependency. It seems that we are experiencing issues with
# restoring environments in GitHub Actions, although unclear why.
# First attempt: https://github.com/home-assistant/core/pull/62383
#
# restore-keys: |
# ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-
# ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-
# ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -517,10 +522,15 @@ jobs:
key: >-
${{ runner.os }}-${{ matrix.python-version }}-${{
steps.generate-python-key.outputs.key }}
restore-keys: |
${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}-
${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-
${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-
# Temporary disabling the restore of environments when bumping
# a dependency. It seems that we are experiencing issues with
# restoring environments in GitHub Actions, although unclear why.
# First attempt: https://github.com/home-assistant/core/pull/62383
#
# restore-keys: |
# ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}-
# ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-
# ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-
- name: Create full Python ${{ matrix.python-version }} virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -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"
+1 -1
View File
@@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try:
async with async_timeout.timeout(10):
things = await bapi.async_get_things(force=True)
return {thing.SERIAL: thing for thing in things}
return {thing.serial: thing for thing in things}
except ServerDisconnectedError as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
except ClientResponseError as err:
+9 -12
View File
@@ -100,7 +100,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
self._remove_update_listener = None
self._attr_name = self._thing.NAME
self._attr_name = self._thing.name
self._attr_device_class = DEVICE_CLASS_SHADE
self._attr_supported_features = COVER_FEATURES
self._attr_attribution = ATTRIBUTION
@@ -109,8 +109,8 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
name=self._attr_name,
via_device=(DOMAIN, self._entry_id),
manufacturer="Brunt",
sw_version=self._thing.FW_VERSION,
model=self._thing.MODEL,
sw_version=self._thing.fw_version,
model=self._thing.model,
)
async def async_added_to_hass(self) -> None:
@@ -127,8 +127,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
None is unknown, 0 is closed, 100 is fully open.
"""
pos = self.coordinator.data[self.unique_id].currentPosition
return int(pos) if pos is not None else None
return self.coordinator.data[self.unique_id].current_position
@property
def request_cover_position(self) -> int | None:
@@ -139,8 +138,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
to Brunt, at times there is a diff of 1 to current
None is unknown, 0 is closed, 100 is fully open.
"""
pos = self.coordinator.data[self.unique_id].requestPosition
return int(pos) if pos is not None else None
return self.coordinator.data[self.unique_id].request_position
@property
def move_state(self) -> int | None:
@@ -149,8 +147,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
None is unknown, 0 when stopped, 1 when opening, 2 when closing
"""
mov = self.coordinator.data[self.unique_id].moveState
return int(mov) if mov is not None else None
return self.coordinator.data[self.unique_id].move_state
@property
def is_opening(self) -> bool:
@@ -190,11 +187,11 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
"""Set the cover to the new position and wait for the update to be reflected."""
try:
await self._bapi.async_change_request_position(
position, thingUri=self._thing.thingUri
position, thing_uri=self._thing.thing_uri
)
except ClientResponseError as exc:
raise HomeAssistantError(
f"Unable to reposition {self._thing.NAME}"
f"Unable to reposition {self._thing.name}"
) from exc
self.coordinator.update_interval = FAST_INTERVAL
await self.coordinator.async_request_refresh()
@@ -204,7 +201,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity):
"""Update the update interval after each refresh."""
if (
self.request_cover_position
== self._bapi.last_requested_positions[self._thing.thingUri]
== self._bapi.last_requested_positions[self._thing.thing_uri]
and self.move_state == 0
):
self.coordinator.update_interval = REGULAR_INTERVAL
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Brunt Blind Engine",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/brunt",
"requirements": ["brunt==1.0.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
+1 -1
View File
@@ -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",
+10 -11
View File
@@ -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": [
{
+1 -1
View File
@@ -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"
+5 -4
View File
@@ -112,10 +112,11 @@ def request_app_setup(
Then come back here and hit the below button.
"""
except NoURLAvailableError:
error_msg = """Could not find a SSL enabled URL for your Home Assistant instance.
Fitbit requires that your Home Assistant instance is accessible via HTTPS.
"""
configurator.notify_errors(_CONFIGURING["fitbit"], error_msg)
_LOGGER.error(
"Could not find an SSL enabled URL for your Home Assistant instance. "
"Fitbit requires that your Home Assistant instance is accessible via HTTPS"
)
return
submit = "I have saved my Client ID and Client Secret into fitbit.conf."
@@ -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
+23 -12
View File
@@ -30,14 +30,13 @@ async def async_setup_entry(hass, config):
loc_id = config.data.get(CONF_LOC_ID)
dev_id = config.data.get(CONF_DEV_ID)
devices = []
devices = {}
for location in client.locations_by_id.values():
for device in location.devices_by_id.values():
if (not loc_id or location.locationid == loc_id) and (
not dev_id or device.deviceid == dev_id
):
devices.append(device)
if not loc_id or location.locationid == loc_id:
for device in location.devices_by_id.values():
if not dev_id or device.deviceid == dev_id:
devices[device.deviceid] = device
if len(devices) == 0:
_LOGGER.debug("No devices found")
@@ -107,23 +106,30 @@ class HoneywellData:
if self._client is None:
return False
devices = [
refreshed_devices = [
device
for location in self._client.locations_by_id.values()
for device in location.devices_by_id.values()
]
if len(devices) == 0:
_LOGGER.error("Failed to find any devices")
if len(refreshed_devices) == 0:
_LOGGER.error("Failed to find any devices after retry")
return False
self.devices = devices
for updated_device in refreshed_devices:
if updated_device.deviceid in self.devices:
self.devices[updated_device.deviceid] = updated_device
else:
_LOGGER.info(
"New device with ID %s detected, reload the honeywell integration if you want to access it in Home Assistant"
)
await self._hass.config_entries.async_reload(self._config.entry_id)
return True
async def _refresh_devices(self):
"""Refresh each enabled device."""
for device in self.devices:
for device in self.devices.values():
await self._hass.async_add_executor_job(device.refresh)
await asyncio.sleep(UPDATE_LOOP_SLEEP_TIME)
@@ -143,11 +149,16 @@ class HoneywellData:
) as exp:
retries -= 1
if retries == 0:
_LOGGER.error(
"Ran out of retry attempts (3 attempts allocated). Error: %s",
exp,
)
raise exp
result = await self._retry()
if not result:
_LOGGER.error("Retry result was empty. Error: %s", exp)
raise exp
_LOGGER.error("SomeComfort update failed, Retrying - Error: %s", exp)
_LOGGER.info("SomeComfort update failed, retrying. Error: %s", exp)
@@ -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()
]
)
+1 -1
View File
@@ -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",
+4 -2
View File
@@ -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"
)
+51 -3
View File
@@ -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
+33 -6
View File
@@ -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:
+9
View File
@@ -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,
)
+14 -5
View File
@@ -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:
+17 -2
View File
@@ -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,
+3
View File
@@ -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"
+1 -1
View File
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/knx",
"requirements": [
"xknx==0.18.13"
"xknx==0.18.14"
],
"codeowners": [
"@Julius2342",
+19 -14
View File
@@ -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):
+4 -3
View File
@@ -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"
}
+23 -4
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
from datetime import timedelta
from http import HTTPStatus
import logging
from aiohttp.client_exceptions import ClientResponseError
@@ -30,7 +31,11 @@ from homeassistant.helpers.update_coordinator import (
UpdateFailed,
)
from .api import ConfigEntryLyricClient, LyricLocalOAuth2Implementation
from .api import (
ConfigEntryLyricClient,
LyricLocalOAuth2Implementation,
OAuth2SessionLyric,
)
from .config_flow import OAuth2FlowHandler
from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN
@@ -84,21 +89,35 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
session = aiohttp_client.async_get_clientsession(hass)
oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
oauth_session = OAuth2SessionLyric(hass, entry, implementation)
client = ConfigEntryLyricClient(session, oauth_session)
client_id = hass.data[DOMAIN][CONF_CLIENT_ID]
lyric = Lyric(client, client_id)
async def async_update_data() -> Lyric:
async def async_update_data(force_refresh_token: bool = False) -> Lyric:
"""Fetch data from Lyric."""
await oauth_session.async_ensure_token_valid()
try:
if not force_refresh_token:
await oauth_session.async_ensure_token_valid()
else:
await oauth_session.force_refresh_token()
except ClientResponseError as exception:
if exception.status in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN):
raise ConfigEntryAuthFailed from exception
raise UpdateFailed(exception) from exception
try:
async with async_timeout.timeout(60):
await lyric.get_locations()
return lyric
except LyricAuthenticationException as exception:
# Attempt to refresh the token before failing.
# Honeywell appear to have issues keeping tokens saved.
_LOGGER.debug("Authentication failed. Attempting to refresh token")
if not force_refresh_token:
return await async_update_data(force_refresh_token=True)
raise ConfigEntryAuthFailed from exception
except (LyricException, ClientResponseError) as exception:
raise UpdateFailed(exception) from exception
+12
View File
@@ -8,6 +8,18 @@ from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.aiohttp_client import async_get_clientsession
class OAuth2SessionLyric(config_entry_oauth2_flow.OAuth2Session):
"""OAuth2Session for Lyric."""
async def force_refresh_token(self) -> None:
"""Force a token refresh."""
new_token = await self.implementation.async_refresh_token(self.token)
self.hass.config_entries.async_update_entry(
self.config_entry, data={**self.config_entry.data, "token": new_token}
)
class ConfigEntryLyricClient(LyricClient):
"""Provide Honeywell Lyric authentication tied to an OAuth2 based config entry."""
@@ -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"
}
+1 -1
View File
@@ -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,
+21 -3
View File
@@ -11,6 +11,7 @@ from nexia.const import (
SYSTEM_STATUS_IDLE,
UNIT_FAHRENHEIT,
)
from nexia.util import find_humidity_setpoint
import voluptuous as vol
from homeassistant.components.climate import ClimateEntity
@@ -58,6 +59,8 @@ from .coordinator import NexiaDataUpdateCoordinator
from .entity import NexiaThermostatZoneEntity
from .util import percent_conv
PARALLEL_UPDATES = 1 # keep data in sync with only one connection at a time
SERVICE_SET_AIRCLEANER_MODE = "set_aircleaner_mode"
SERVICE_SET_HUMIDIFY_SETPOINT = "set_humidify_setpoint"
SERVICE_SET_HVAC_RUN_MODE = "set_hvac_run_mode"
@@ -231,9 +234,9 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
def set_humidity(self, humidity):
"""Dehumidify target."""
if self._thermostat.has_dehumidify_support():
self._thermostat.set_dehumidify_setpoint(humidity / 100.0)
self.set_dehumidify_setpoint(humidity)
else:
self._thermostat.set_humidify_setpoint(humidity / 100.0)
self.set_humidify_setpoint(humidity)
self._signal_thermostat_update()
@property
@@ -453,7 +456,22 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
def set_humidify_setpoint(self, humidity):
"""Set the humidify setpoint."""
self._thermostat.set_humidify_setpoint(humidity / 100.0)
target_humidity = find_humidity_setpoint(humidity / 100.0)
if self._thermostat.get_humidify_setpoint() == target_humidity:
# Trying to set the humidify setpoint to the
# same value will cause the api to timeout
return
self._thermostat.set_humidify_setpoint(target_humidity)
self._signal_thermostat_update()
def set_dehumidify_setpoint(self, humidity):
"""Set the dehumidify setpoint."""
target_humidity = find_humidity_setpoint(humidity / 100.0)
if self._thermostat.get_dehumidify_setpoint() == target_humidity:
# Trying to set the dehumidify setpoint to the
# same value will cause the api to timeout
return
self._thermostat.set_dehumidify_setpoint(target_humidity)
self._signal_thermostat_update()
def _signal_thermostat_update(self):
+1 -1
View File
@@ -1,7 +1,7 @@
{
"domain": "nexia",
"name": "Nexia/American Standard/Trane",
"requirements": ["nexia==0.9.11"],
"requirements": ["nexia==0.9.12"],
"codeowners": ["@bdraco"],
"documentation": "https://www.home-assistant.io/integrations/nexia",
"config_flow": true,
+1 -1
View File
@@ -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)
+6 -1
View File
@@ -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
+1 -1
View File
@@ -2,7 +2,7 @@
"domain": "ring",
"name": "Ring",
"documentation": "https://www.home-assistant.io/integrations/ring",
"requirements": ["ring_doorbell==0.7.1"],
"requirements": ["ring_doorbell==0.7.2"],
"dependencies": ["ffmpeg"],
"codeowners": ["@balloob"],
"config_flow": true,
+1 -2
View File
@@ -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__)
+171 -27
View File
@@ -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"
+1 -1
View File
@@ -2,7 +2,7 @@
"domain": "ssdp",
"name": "Simple Service Discovery Protocol (SSDP)",
"documentation": "https://www.home-assistant.io/integrations/ssdp",
"requirements": ["async-upnp-client==0.22.12"],
"requirements": ["async-upnp-client==0.23.1"],
"dependencies": ["network"],
"after_dependencies": ["zeroconf"],
"codeowners": [],
@@ -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"
+5 -1
View File
@@ -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,
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "UPnP/IGD",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/upnp",
"requirements": ["async-upnp-client==0.22.12"],
"requirements": ["async-upnp-client==0.23.1"],
"dependencies": ["network", "ssdp"],
"codeowners": ["@StevenLooman","@ehendrix23"],
"ssdp": [
+2 -1
View File
@@ -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:
+2 -2
View File
@@ -49,7 +49,7 @@ class VelbusLight(VelbusEntity, LightEntity):
"""Representation of a Velbus light."""
_channel: VelbusDimmer
_attr_supported_feature = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION
_attr_supported_features = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION
@property
def is_on(self) -> bool:
@@ -96,7 +96,7 @@ class VelbusButtonLight(VelbusEntity, LightEntity):
_channel: VelbusButton
_attr_entity_registry_enabled_default = False
_attr_supported_feature = SUPPORT_FLASH
_attr_supported_features = SUPPORT_FLASH
def __init__(self, channel: VelbusChannel) -> None:
"""Initialize the button light (led)."""
@@ -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"
}
}
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Belkin WeMo",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/wemo",
"requirements": ["pywemo==0.6.7"],
"requirements": ["pywemo==0.7.0"],
"ssdp": [
{
"manufacturer": "Belkin International Inc."
@@ -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"],
+2 -2
View File
@@ -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]
+1 -1
View File
@@ -490,7 +490,7 @@ async def async_setup_entry(hass, config_entry): # noqa: C901
await platform.async_add_entities([entity])
if entity.unique_id:
hass.async_add_job(_add_node_to_component())
hass.create_task(_add_node_to_component())
return
@callback
+1 -1
View File
@@ -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)
+1
View File
@@ -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
+3 -3
View File
@@ -4,7 +4,7 @@ aiodiscover==1.4.5
aiohttp==3.8.1
aiohttp_cors==0.7.0
astral==2.2
async-upnp-client==0.22.12
async-upnp-client==0.23.1
async_timeout==4.0.0
atomicwrites==1.4.0
attrs==21.2.0
@@ -16,7 +16,7 @@ ciso8601==2.2.0
cryptography==35.0.0
emoji==1.5.0
hass-nabucasa==0.50.0
home-assistant-frontend==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
View File
@@ -21,5 +21,5 @@ python-slugify==4.0.1
pyyaml==6.0
requests==2.26.0
voluptuous==0.12.2
voluptuous-serialize==2.4.0
voluptuous-serialize==2.5.0
yarl==1.6.3
+26 -23
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -53,7 +53,7 @@ REQUIRES = [
"pyyaml==6.0",
"requests==2.26.0",
"voluptuous==0.12.2",
"voluptuous-serialize==2.4.0",
"voluptuous-serialize==2.5.0",
"yarl==1.6.3",
]
+37 -8
View File
@@ -683,10 +683,12 @@ async def test_entity_cast_status(hass: HomeAssistant):
| SUPPORT_PLAY_MEDIA
| SUPPORT_STOP
| SUPPORT_TURN_OFF
| SUPPORT_TURN_ON
| SUPPORT_VOLUME_MUTE
| SUPPORT_VOLUME_SET,
SUPPORT_PLAY_MEDIA
| SUPPORT_TURN_OFF
| SUPPORT_TURN_ON
| SUPPORT_VOLUME_MUTE
| SUPPORT_VOLUME_SET,
),
@@ -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)
+28 -10
View File
@@ -1,7 +1,7 @@
"""Test the Fronius update coordinators."""
from unittest.mock import patch
from pyfronius import FroniusError
from pyfronius import BadStatusError, FroniusError
from homeassistant.components.fronius.coordinator import (
FroniusInverterUpdateCoordinator,
@@ -18,27 +18,32 @@ async def test_adaptive_update_interval(hass, aioclient_mock):
with patch("pyfronius.Fronius.current_inverter_data") as mock_inverter_data:
mock_responses(aioclient_mock)
await setup_fronius_integration(hass)
assert mock_inverter_data.call_count == 1
mock_inverter_data.assert_called_once()
mock_inverter_data.reset_mock()
async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval
)
await hass.async_block_till_done()
assert mock_inverter_data.call_count == 2
mock_inverter_data.assert_called_once()
mock_inverter_data.reset_mock()
mock_inverter_data.side_effect = FroniusError
# first 3 requests at default interval - 4th has different interval
for _ in range(4):
mock_inverter_data.side_effect = FroniusError()
# first 3 bad requests at default interval - 4th has different interval
for _ in range(3):
async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval
)
await hass.async_block_till_done()
assert mock_inverter_data.call_count == 5
assert mock_inverter_data.call_count == 3
mock_inverter_data.reset_mock()
async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.error_interval
)
await hass.async_block_till_done()
assert mock_inverter_data.call_count == 6
assert mock_inverter_data.call_count == 1
mock_inverter_data.reset_mock()
mock_inverter_data.side_effect = None
# next successful request resets to default interval
@@ -46,10 +51,23 @@ async def test_adaptive_update_interval(hass, aioclient_mock):
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.error_interval
)
await hass.async_block_till_done()
assert mock_inverter_data.call_count == 7
mock_inverter_data.assert_called_once()
mock_inverter_data.reset_mock()
async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval
)
await hass.async_block_till_done()
assert mock_inverter_data.call_count == 8
mock_inverter_data.assert_called_once()
mock_inverter_data.reset_mock()
# BadStatusError on inverter endpoints have special handling
mock_inverter_data.side_effect = BadStatusError("mock_endpoint", 8)
# first 3 requests at default interval - 4th has different interval
for _ in range(3):
async_fire_time_changed(
hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval
)
await hass.async_block_till_done()
# BadStatusError does 3 silent retries for inverter endpoint * 3 request intervals = 9
assert mock_inverter_data.call_count == 9
+20 -1
View File
@@ -1,6 +1,8 @@
"""Test honeywell setup process."""
from unittest.mock import patch
from unittest.mock import create_autospec, patch
import somecomfort
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
@@ -29,3 +31,20 @@ async def test_setup_multiple_thermostats(
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
assert hass.states.async_entity_ids_count() == 2
@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0)
async def test_setup_multiple_thermostats_with_same_deviceid(
hass: HomeAssistant, caplog, config_entry: MockConfigEntry, device, client
) -> None:
"""Test Honeywell TCC API returning duplicate device IDs."""
mock_location2 = create_autospec(somecomfort.Location, instance=True)
mock_location2.locationid.return_value = "location2"
mock_location2.devices_by_id = {device.deviceid: device}
client.locations_by_id["location2"] = mock_location2
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
assert hass.states.async_entity_ids_count() == 1
assert "Platform honeywell does not generate unique IDs" not in caplog.text
+41 -1
View File
@@ -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
)
+70 -4
View File
@@ -28,7 +28,15 @@ from tests.common import MockConfigEntry
def _gateway_descriptor(ip: str, port: int) -> GatewayDescriptor:
"""Get mock gw descriptor."""
return GatewayDescriptor("Test", ip, port, "eth0", "127.0.0.1", True)
return GatewayDescriptor(
"Test",
ip,
port,
"eth0",
"127.0.0.1",
supports_routing=True,
supports_tunnelling=True,
)
async def test_user_single_instance(hass):
@@ -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",
}
+78
View File
@@ -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__
)
+95 -20
View File
@@ -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()
+2 -1
View File
@@ -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(
+16
View File
@@ -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."""
+1 -1
View File
@@ -57,7 +57,7 @@ def pywemo_device_fixture(pywemo_registry, pywemo_model):
device.port = MOCK_PORT
device.name = MOCK_NAME
device.serialnumber = MOCK_SERIAL_NUMBER
device.model_name = pywemo_model
device.model_name = pywemo_model.replace("LongPress", "")
device.get_state.return_value = 0 # Default to Off
device.supports_long_press.return_value = cls.supports_long_press()
+5 -5
View File
@@ -3,7 +3,6 @@ import pytest
from pywemo.subscribe import EVENT_TYPE_LONG_PRESS
from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.wemo.const import DOMAIN, WEMO_SUBSCRIPTION_EVENT
from homeassistant.const import (
CONF_DEVICE_ID,
@@ -11,6 +10,7 @@ from homeassistant.const import (
CONF_ENTITY_ID,
CONF_PLATFORM,
CONF_TYPE,
Platform,
)
from homeassistant.setup import async_setup_component
@@ -26,8 +26,8 @@ DATA_MESSAGE = {"message": "service-called"}
@pytest.fixture
def pywemo_model():
"""Pywemo Dimmer models use the light platform (WemoDimmer class)."""
return "Dimmer"
"""Pywemo LightSwitch models use the switch platform."""
return "LightSwitchLongPress"
async def setup_automation(hass, device_id, trigger_type):
@@ -67,14 +67,14 @@ async def test_get_triggers(hass, wemo_entity):
},
{
CONF_DEVICE_ID: wemo_entity.device_id,
CONF_DOMAIN: LIGHT_DOMAIN,
CONF_DOMAIN: Platform.SWITCH,
CONF_ENTITY_ID: wemo_entity.entity_id,
CONF_PLATFORM: "device",
CONF_TYPE: "turned_off",
},
{
CONF_DEVICE_ID: wemo_entity.device_id,
CONF_DOMAIN: LIGHT_DOMAIN,
CONF_DOMAIN: Platform.SWITCH,
CONF_ENTITY_ID: wemo_entity.entity_id,
CONF_PLATFORM: "device",
CONF_TYPE: "turned_on",
+2 -2
View File
@@ -26,8 +26,8 @@ asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(True))
@pytest.fixture
def pywemo_model():
"""Pywemo Dimmer models use the light platform (WemoDimmer class)."""
return "Dimmer"
"""Pywemo LightSwitch models use the switch platform."""
return "LightSwitchLongPress"
async def test_async_register_device_longpress_fails(hass, pywemo_device):