Compare commits

..

103 Commits

Author SHA1 Message Date
Franck Nijhof
267dfac737 2024.7.3 (#122194) 2024-07-19 19:38:08 +02:00
Franck Nijhof
a08ffdc8d3 Bump version to 2024.7.3 2024-07-19 18:50:21 +02:00
Mr. Bubbles
1ef4332af6 Fix KeyError in config flow of Bring integration (#122136) 2024-07-19 18:49:45 +02:00
Marc Mueller
d0d2fd7918 Update yt-dlp to 2024.07.16 (#122124) 2024-07-19 18:49:41 +02:00
Steven B.
c518c4756b Bump tplink dependency python-kasa to 0.7.0.5 (#122119) 2024-07-19 18:49:38 +02:00
Shay Levy
a3a99cc631 Prevent connecting to a Shelly device that is already connected (#122105) 2024-07-19 18:49:35 +02:00
Steven B.
977a55e3b8 Update tplink device config during reauth flow (#122089) 2024-07-19 18:49:31 +02:00
Harry Martland
002db3c3e9 Fix hive not updating when boosting (#122042)
* fixes issue where hive does not update when boosting

* formats files
2024-07-19 18:49:28 +02:00
Robert Svensson
d9e44bab69 Mark UniFi power cycle button as unavailable if PoE is not enabled on port (#122035) 2024-07-19 18:49:25 +02:00
G Johansson
4b93fc61b5 Bump python-holidays to 0.53 (#122021) 2024-07-19 18:49:21 +02:00
J. Nick Koston
214b5efd72 Narrow sqlite database corruption check to ensure disk image is malformed (#121947)
* Narrow sqlite database corruption check to ensure disk image is malformed

The database corruption check would also replace the database when it
locked externally instead of only when its malformed.

This was discovered in https://github.com/home-assistant/core/issues/121909#issuecomment-2227409124
when a user did a manual index creation while HA was online

* tweak

* tweak

* fix

* fix
2024-07-19 18:49:16 +02:00
Maciej Bieniek
9bd822d693 Fix configuration_url for Shelly device using IPv6 (#121939)
Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
2024-07-19 18:48:32 +02:00
Tomasz Gorochowik
bf89eaae25 Fix enigma2 mute (#121928) 2024-07-19 18:42:44 +02:00
Scott K Logan
f9b359ae30 Fix rainforest_raven closing device due to timeout (#121905) 2024-07-19 18:42:41 +02:00
mvn23
24ed003471 Fix opentherm_gw availability (#121892) 2024-07-19 18:42:38 +02:00
J. Nick Koston
a835750252 Log add/remove index complete at the same level as when it starts (#121852) 2024-07-19 18:42:35 +02:00
Avi Miller
ad07bdb62b Bump aiolifx to 1.0.5 (#121824) 2024-07-19 18:42:32 +02:00
Avi Miller
41104324ec Bump aiolifx to 1.0.4 (#121267) 2024-07-19 18:42:28 +02:00
ollo69
e9344ae101 Bump PySwitchbot to 0.48.1 (#121804) 2024-07-19 18:40:25 +02:00
starkillerOG
56a9167ed2 Reolink media second lens (#121800)
DUO lens camera distinguish between lenses for media playback
2024-07-19 18:40:22 +02:00
Jan Bouwhuis
e0b90c4b36 Fix alexa does to check current_position correctly when handling cover range changes (#121798) 2024-07-19 18:40:19 +02:00
Jan-Philipp Benecke
976902f22c Add missing translations to Roborock (#121796) 2024-07-19 18:40:16 +02:00
Steven B
0f69c58ba9 Bump python-kasa to 0.7.0.4 (#121791) 2024-07-19 18:40:13 +02:00
Glenn Waters
1e6c96c6eb Use async_connect in newly bumped 0.5.8 UPB library (#121789) 2024-07-19 18:40:09 +02:00
J. Nick Koston
63b14d14c1 Add some missing tplink ouis (#121785) 2024-07-19 18:40:06 +02:00
Josef Zweck
6aaaba6419 Bump pytedee_async to 0.2.20 (#121783) 2024-07-19 18:40:03 +02:00
Allen Porter
3b8e736fe3 Pin mashumaro version >= 3.13.1 for python 3.12.4 compatibility. (#121782)
Pin mashumaro version for python 3.12.4 compatibility.
2024-07-19 18:40:00 +02:00
tronikos
68841b3d8a Bump opower to 0.5.2 to fix 403 forbidden errors for users with multiple accounts (#121762) 2024-07-19 18:39:56 +02:00
Abílio Costa
3d8afe7cb8 Update Idasen Desk library to 2.6.2 (#121729) 2024-07-19 18:29:45 +02:00
Robert Svensson
8595242142 Fix bad access to UniFi runtime_data when not assigned (#121725)
* Fix bad access to runtime_data when not assigned

* Fix review comment

* Clean up if statements
2024-07-19 18:29:42 +02:00
Joost Lekkerkerker
ebe7bc0686 Bump knocki to 0.3.1 (#121717) 2024-07-19 18:29:39 +02:00
J. Nick Koston
4ab180f016 Fix update happening too early in unifiprotect (#121714) 2024-07-19 18:29:36 +02:00
Mr. Bubbles
372649069e Bump pyloadapi to v1.3.2 (#121709) 2024-07-19 18:29:33 +02:00
Joost Lekkerkerker
98df46f3ea Bump knocki to 0.3.0 (#121704) 2024-07-19 18:29:30 +02:00
Steven B
269fb23527 Fix tplink bug changing color temp on bulbs with light effects (#121696) 2024-07-19 18:29:27 +02:00
Sid
ad5cbf0da6 Allow enigma2 devices to use different source bouquets (#121686) 2024-07-19 18:29:24 +02:00
Lucas Mindêllo de Andrade
10cdf64f90 Bump sunweg 3.0.2 (#121684) 2024-07-19 18:29:21 +02:00
Tomek Porozynski
ec8e639804 Update Supla async_set_cover_position to use "REVEAL_PARTIALLY" (#121663) 2024-07-19 18:29:17 +02:00
Jan Stienstra
37f37f7287 Retain Jellyfin config flow input on connection issue (#121618) 2024-07-19 18:29:13 +02:00
Mr. Bubbles
ef7d68bfd6 Fix reauth error and exception in ista EcoTrend integration (#121482) 2024-07-19 18:29:08 +02:00
Franck Nijhof
058b012e6c 2024.7.2 (#121671) 2024-07-10 13:18:48 +02:00
Franck Nijhof
71370758a8 Bump version to 2024.7.2 2024-07-10 12:06:02 +02:00
Franck Nijhof
38a44676eb Block variable <=3.4.4 custom integration from breaking the recorder (#121670) 2024-07-10 12:01:11 +02:00
Marcel van der Veldt
05ce3d35b3 Matter lock state follow-up (#121669) 2024-07-10 12:01:08 +02:00
Marcel van der Veldt
2151086b0a Fix state for Matter Locks (including optional door sensor) (#121665) 2024-07-10 12:01:05 +02:00
Franck Nijhof
9c83af3789 Block places <=2.7.0 custom integration from breaking the recorder (#121662) 2024-07-10 12:01:01 +02:00
tronikos
ac3eecc879 Handle errors in Fully Kiosk camera (#121659) 2024-07-10 12:00:58 +02:00
Franck Nijhof
ec0910e3da Block icloud3 custom integration from breaking the recorder (#121658) 2024-07-10 12:00:55 +02:00
Maikel Punie
fd0c26cd56 Small fix in velbus cover for the assumed states (#121656) 2024-07-10 12:00:52 +02:00
Paul Bottein
a4c5dee082 Update frontend to 20240710.0 (#121651) 2024-07-10 12:00:48 +02:00
Joakim Plate
37c09dbdb6 Allow ambilight when we have connection (philips_js) (#121620) 2024-07-10 12:00:45 +02:00
Arie Catsman
73d1973625 Bump pyenphase to 1.20.6 (#121583)
bump pyenphase to 1.20.6
2024-07-10 12:00:42 +02:00
Glenn Waters
5a04a886cf Fix upb config flow connect (#121571) 2024-07-10 12:00:39 +02:00
Franck Nijhof
50802f84f0 Update tailscale to 0.6.1 (#121557) 2024-07-10 12:00:36 +02:00
Franck Nijhof
138b68ecc0 Update vehicle to 2.2.2 (#121556) 2024-07-10 12:00:33 +02:00
Christoph
e0b01ee94e Remove homematic state_class from GAS_POWER sensor (#121533) 2024-07-10 12:00:30 +02:00
J. Nick Koston
4f2c3df518 Fix person tracking in unifiprotect (#121528) 2024-07-10 12:00:26 +02:00
Paulus Schoutsen
51a6bb1c22 Include hass device ID in mobile app get_config webhook (#121496) 2024-07-10 12:00:23 +02:00
Ovidiu D. Nițan
6bf9ec69f3 Bump xiaomi-ble to 0.30.2 (#121471) 2024-07-10 12:00:19 +02:00
Joost Lekkerkerker
21309eeb5d Bump xiaomi-ble to 0.30.1 (#120743) 2024-07-10 12:00:14 +02:00
J. Nick Koston
0a1b46c52f Bump yalexs to 6.4.2 (#121467) 2024-07-10 11:52:56 +02:00
Jason R. Coombs
9512f9eec3 Bump jaraco.abode to 5.2.1 (#121446)
Bump dependency on jaraco.abode to 5.2.1.

Closes #121300
2024-07-10 11:52:53 +02:00
jan iversen
ab94422c18 Bump pymodbus to 3.6.9 (#121445)
Bump pymodbus 3.6.9.
2024-07-10 11:52:50 +02:00
Joost Lekkerkerker
ec105e5265 Fix Mealie URL field (#121434) 2024-07-10 11:52:47 +02:00
Joost Lekkerkerker
cadd8521ae Sort mealie mealplans (#121433) 2024-07-10 11:52:43 +02:00
Joost Lekkerkerker
8825c50671 Fix MPD config flow (#121431) 2024-07-10 11:52:40 +02:00
Michael
a72cc3c248 Allow current empty feeds to be configured in Feedreader (#121421) 2024-07-10 11:52:37 +02:00
G Johansson
780f7254c1 Fix feature flag in climate (#121398) 2024-07-10 11:52:34 +02:00
G Johansson
37621e77ae Fix timezone issue in smhi weather (#121389) 2024-07-10 11:52:31 +02:00
G Johansson
8017386c73 Fix unnecessary logging of turn on/off feature flags in Climate (#121387) 2024-07-10 11:52:28 +02:00
G Johansson
a5f4c25a2c Bump psutil to 6.0.0 (#121385) 2024-07-10 11:52:25 +02:00
Brett Adams
1d7bddf449 Fix initial Wall Connector values in Tessie (#121353) 2024-07-10 11:52:21 +02:00
Luke Lashley
711bdaf373 Bump anova-wifi to 0.17.0 (#121337)
* Bump anova-wifi to 0.16.0

* Bump to .17
2024-07-10 11:52:18 +02:00
Jan Temešinko
803d9c5a8e Fix ombi configuration validation (#121314) 2024-07-10 11:52:15 +02:00
Rasmus Lundsgaard
1133c41fa8 Fix empty list in kodi media_player (#121250)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2024-07-10 11:52:12 +02:00
Alan
a06af7ee93 LLM to handle int attributes (#121037) 2024-07-10 11:52:08 +02:00
Robert C. Maehl
c54717707e Direct Users to App-Specific Passwords for iCloud integration to prevent MFA spam (#120945)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2024-07-10 11:52:05 +02:00
J. Nick Koston
440d83d754 Remove legacy foreign key constraint from sqlite states table (#120779) 2024-07-10 11:51:59 +02:00
Franck Nijhof
1cf62916a7 2024.7.1 (#121289) 2024-07-05 17:25:40 +02:00
Bram Kragten
e3958d4adb Update frontend to 20240705.0 (#121295) 2024-07-05 15:04:01 +02:00
Franck Nijhof
dfccd4abf9 Bump version to 2024.7.1 2024-07-05 11:21:36 +02:00
Steven B
994d6f552c Fix tplink light effect behaviour when activating a scene (#121288) 2024-07-05 11:21:07 +02:00
G Johansson
b015611a2a Bump python-holidays to 0.52 (#121283) 2024-07-05 11:21:04 +02:00
Shay Levy
f4e362c5d0 Bump aiowebostv to 0.4.2 (#121258) 2024-07-05 11:21:00 +02:00
Jordi
a542236614 Bump aioaquacell to 0.1.8 (#121253) 2024-07-05 11:20:57 +02:00
Shay Levy
651439ea06 Fix WebOS TV media player status when OFF after IDLE (#121251) 2024-07-05 11:20:54 +02:00
Robert Resch
eda450838e Bump deebot-client to 8.1.1 (#121241) 2024-07-05 11:20:50 +02:00
hahn-th
b906daa493 Revert Homematic IP Cloud unique ID changes (#121231) 2024-07-05 11:20:47 +02:00
Thomas55555
ac668dce7d Fix work area sensor in Husqvarna Automower (#121228) 2024-07-05 11:20:44 +02:00
Luke Lashley
1bb4d62a3e Bump anova-wifi to 0.15.0 (#121222) 2024-07-05 11:20:40 +02:00
Marcel van der Veldt
0b970f9a85 Listen for attribute changes of OnOff cluster in appliances (#121198) 2024-07-05 11:20:37 +02:00
Marcel van der Veldt
d2b695e7b5 Fix Matter light discovery schema for DimmerSwitch (#121185) 2024-07-05 11:20:34 +02:00
Steven B
b2f23c1a5e Bump python-kasa to 0.7.0.3 (#121183) 2024-07-05 11:20:31 +02:00
Gerben Jongerius
f403afb012 Bump youless library version 2.1.2 (#121181) 2024-07-05 11:20:27 +02:00
Maciej Bieniek
ee276aff44 Fix pulse counter frequency sensors for Shelly Plus Uni (#121178)
Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
2024-07-05 11:20:23 +02:00
Maikel Punie
0acd1dc5d1 Bump velbusaio to 2024.7.5 (#121156)
* Bump velbusaio to 2024.7.4

* bump to 2024.7.5 to remove print functions
2024-07-05 11:20:20 +02:00
Martin Weinelt
21815e1621 Fix broken pathlib import in august integration (#121135) 2024-07-05 11:20:17 +02:00
J. Nick Koston
15933bb16f Bump inkbird-ble to 0.5.8 (#121134) 2024-07-05 11:20:13 +02:00
Pavel Skuratovich
930cd0dc50 Starline: Fix "Error updating SLNet token" message in Log (#121122)
Fixes https://github.com/home-assistant/core/issues/116715
2024-07-05 11:20:10 +02:00
Christoph
fc4af48179 Fix HmIP-ESI GAS sensor DeviceClass (#121106)
should be SensorDeviceClass:GAS instead of SensorDeviceClass:VOLUME to be supported in the Energy Dashboard
2024-07-05 11:20:07 +02:00
Marcel van der Veldt
ba1cf84ea5 Fix locking/unlocking transition state in Matter lock platform (#121099) 2024-07-05 11:20:04 +02:00
dougiteixeira
59cf01e252 Add device class translations in Random (#120890) 2024-07-05 11:20:00 +02:00
Allen Porter
46e681f4fc Improve redaction for stream error messages (#120867) 2024-07-05 11:19:56 +02:00
152 changed files with 1793 additions and 656 deletions

View File

@@ -777,6 +777,8 @@ build.json @home-assistant/supervisor
/tests/components/lg_netcast/ @Drafteed @splinter98
/homeassistant/components/lidarr/ @tkdrob
/tests/components/lidarr/ @tkdrob
/homeassistant/components/lifx/ @Djelibeybi
/tests/components/lifx/ @Djelibeybi
/homeassistant/components/light/ @home-assistant/core
/tests/components/light/ @home-assistant/core
/homeassistant/components/linear_garage_door/ @IceBotYT

View File

@@ -9,5 +9,5 @@
},
"iot_class": "cloud_push",
"loggers": ["jaraco.abode", "lomond"],
"requirements": ["jaraco.abode==5.1.2"]
"requirements": ["jaraco.abode==5.2.1"]
}

View File

@@ -1497,7 +1497,7 @@ async def async_api_adjust_range(
if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
range_delta = int(range_delta * 20) if range_delta_default else int(range_delta)
service = SERVICE_SET_COVER_POSITION
if not (current := entity.attributes.get(cover.ATTR_POSITION)):
if not (current := entity.attributes.get(cover.ATTR_CURRENT_POSITION)):
msg = f"Unable to determine {entity.entity_id} current position"
raise AlexaInvalidValueError(msg)
position = response_value = min(100, max(0, range_delta + current))

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/anova",
"iot_class": "cloud_push",
"loggers": ["anova_wifi"],
"requirements": ["anova-wifi==0.14.0"]
"requirements": ["anova-wifi==0.17.0"]
}

View File

@@ -8,5 +8,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"loggers": ["aioaquacell"],
"requirements": ["aioaquacell==0.1.7"]
"requirements": ["aioaquacell==0.1.8"]
}

View File

@@ -2,10 +2,10 @@
from __future__ import annotations
from pathlib import Path
from typing import cast
from aiohttp import ClientResponseError
from path import Path
from yalexs.exceptions import AugustApiAIOHTTPError
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
from yalexs.manager.gateway import Config as YaleXSConfig

View File

@@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==6.4.1", "yalexs-ble==2.4.3"]
"requirements": ["yalexs==6.4.2", "yalexs-ble==2.4.3"]
}

View File

@@ -58,7 +58,7 @@ class BringConfigFlow(ConfigFlow, domain=DOMAIN):
):
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=self.info["name"] or user_input[CONF_EMAIL], data=user_input
title=self.info.get("name") or user_input[CONF_EMAIL], data=user_input
)
return self.async_show_form(

View File

@@ -377,6 +377,14 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
# Return if integration has migrated already
return
supported_features = self.supported_features
if supported_features & (
ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
):
# The entity supports both turn_on and turn_off, the backwards compatibility
# checks are not needed
return
supported_features = self.supported_features
if not supported_features & ClimateEntityFeature.TURN_OFF and (
type(self).async_turn_off is not ClimateEntity.async_turn_off

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.10", "deebot-client==8.1.0"]
"requirements": ["py-sucks==0.9.10", "deebot-client==8.1.1"]
}

View File

@@ -16,6 +16,8 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import CONF_SOURCE_BOUQUET
type Enigma2ConfigEntry = ConfigEntry[OpenWebIfDevice]
PLATFORMS = [Platform.MEDIA_PLAYER]
@@ -35,7 +37,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: Enigma2ConfigEntry) -> b
hass, verify_ssl=entry.data[CONF_VERIFY_SSL], base_url=base_url
)
entry.runtime_data = OpenWebIfDevice(session)
entry.runtime_data = OpenWebIfDevice(
session, source_bouquet=entry.options.get(CONF_SOURCE_BOUQUET)
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["openwebif"],
"requirements": ["openwebifpy==4.2.4"]
"requirements": ["openwebifpy==4.2.5"]
}

View File

@@ -199,7 +199,8 @@ class Enigma2Device(MediaPlayerEntity):
async def async_mute_volume(self, mute: bool) -> None:
"""Mute or unmute."""
await self._device.toggle_mute()
if mute != self._device.status.muted:
await self._device.toggle_mute()
async def async_select_source(self, source: str) -> None:
"""Select input source."""

View File

@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"requirements": ["pyenphase==1.20.3"],
"requirements": ["pyenphase==1.20.6"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."

View File

@@ -107,13 +107,6 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
return self.abort_on_import_error(user_input[CONF_URL], "url_error")
return self.show_user_form(user_input, {"base": "url_error"})
if not feed.entries:
if self.context["source"] == SOURCE_IMPORT:
return self.abort_on_import_error(
user_input[CONF_URL], "no_feed_entries"
)
return self.show_user_form(user_input, {"base": "no_feed_entries"})
feed_title = feed["feed"]["title"]
return self.async_create_entry(
@@ -161,13 +154,6 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="reconfigure_confirm",
errors={"base": "url_error"},
)
if not feed.entries:
return self.show_user_form(
user_input=user_input,
description_placeholders={"name": self._config_entry.title},
step_id="reconfigure_confirm",
errors={"base": "no_feed_entries"},
)
self.hass.config_entries.async_update_entry(self._config_entry, data=user_input)
return self.async_abort(reason="reconfigure_successful")

View File

@@ -18,8 +18,7 @@
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
},
"error": {
"url_error": "The URL could not be opened.",
"no_feed_entries": "The URL seems not to serve any feed entries."
"url_error": "The URL could not be opened."
}
},
"options": {
@@ -38,10 +37,6 @@
"import_yaml_error_url_error": {
"title": "The Feedreader YAML configuration import failed",
"description": "Configuring the Feedreader using YAML is being removed but there was a connection error when trying to import the YAML configuration for `{url}`.\n\nPlease verify that url is reachable and accessable for Home Assistant and restart Home Assistant to try again or remove the Feedreader YAML configuration from your configuration.yaml file and continue to set up the integration manually."
},
"import_yaml_error_no_feed_entries": {
"title": "[%key:component::feedreader::issues::import_yaml_error_url_error::title%]",
"description": "Configuring the Feedreader using YAML is being removed but when trying to import the YAML configuration for `{url}` no feed entries were found.\n\nPlease verify that url serves any feed entries and restart Home Assistant to try again or remove the Feedreader YAML configuration from your configuration.yaml file and continue to set up the integration manually."
}
}
}

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20240703.0"]
"requirements": ["home-assistant-frontend==20240710.0"]
}

View File

@@ -2,9 +2,12 @@
from __future__ import annotations
from fullykiosk import FullyKioskError
from homeassistant.components.camera import Camera, CameraEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
@@ -36,8 +39,12 @@ class FullyCameraEntity(FullyKioskEntity, Camera):
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return bytes of camera image."""
image_bytes: bytes = await self.coordinator.fully.getCamshot()
return image_bytes
try:
image_bytes: bytes = await self.coordinator.fully.getCamshot()
except FullyKioskError as err:
raise HomeAssistantError(err) from err
else:
return image_bytes
async def async_turn_on(self) -> None:
"""Turn on camera."""

View File

@@ -142,10 +142,10 @@ class HiveClimateEntity(HiveEntity, ClimateEntity):
self.device = await self.hive.heating.getClimate(self.device)
self._attr_available = self.device["deviceData"].get("online")
if self._attr_available:
self._attr_hvac_mode = HIVE_TO_HASS_STATE[self.device["status"]["mode"]]
self._attr_hvac_action = HIVE_TO_HASS_HVAC_ACTION[
self._attr_hvac_mode = HIVE_TO_HASS_STATE.get(self.device["status"]["mode"])
self._attr_hvac_action = HIVE_TO_HASS_HVAC_ACTION.get(
self.device["status"]["action"]
]
)
self._attr_current_temperature = self.device["status"][
"current_temperature"
]
@@ -154,5 +154,6 @@ class HiveClimateEntity(HiveEntity, ClimateEntity):
self._attr_max_temp = self.device["max_temp"]
if self.device["status"]["boost"] == "ON":
self._attr_preset_mode = PRESET_BOOST
self._attr_hvac_mode = HVACMode.HEAT
else:
self._attr_preset_mode = PRESET_NONE

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.51", "babel==2.15.0"]
"requirements": ["holidays==0.53", "babel==2.15.0"]
}

View File

@@ -156,7 +156,6 @@ SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = {
key="GAS_POWER",
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
device_class=SensorDeviceClass.GAS,
state_class=SensorStateClass.MEASUREMENT,
),
"GAS_ENERGY_COUNTER": SensorEntityDescription(
key="GAS_ENERGY_COUNTER",

View File

@@ -216,14 +216,13 @@ class HomematicipGenericEntity(Entity):
@property
def unique_id(self) -> str:
"""Return a unique ID."""
suffix = ""
if self._post is not None:
suffix = f"_{self._post}"
unique_id = f"{self.__class__.__name__}_{self._device.id}"
if self._is_multi_channel:
return f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}{suffix}"
unique_id = (
f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}"
)
return f"{self.__class__.__name__}_{self._device.id}{suffix}"
return unique_id
@property
def icon(self) -> str | None:

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from homematicip.aio.device import (
@@ -36,7 +35,6 @@ from homematicip.base.functionalChannels import FunctionalChannel
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
@@ -163,19 +161,28 @@ async def async_setup_entry(
for ch in get_channels_from_device(
device, FunctionalChannelType.ENERGY_SENSORS_INTERFACE_CHANNEL
):
if ch.connectedEnergySensorType not in SENSORS_ESI:
continue
if ch.connectedEnergySensorType == ESI_CONNECTED_SENSOR_TYPE_IEC:
if ch.currentPowerConsumption is not None:
entities.append(HmipEsiIecPowerConsumption(hap, device))
if ch.energyCounterOneType != ESI_TYPE_UNKNOWN:
entities.append(HmipEsiIecEnergyCounterHighTariff(hap, device))
if ch.energyCounterTwoType != ESI_TYPE_UNKNOWN:
entities.append(HmipEsiIecEnergyCounterLowTariff(hap, device))
if ch.energyCounterThreeType != ESI_TYPE_UNKNOWN:
entities.append(
HmipEsiIecEnergyCounterInputSingleTariff(hap, device)
)
new_entities = [
HmipEsiSensorEntity(hap, device, ch.index, description)
for description in SENSORS_ESI[ch.connectedEnergySensorType]
]
if ch.connectedEnergySensorType == ESI_CONNECTED_SENSOR_TYPE_GAS:
if ch.currentGasFlow is not None:
entities.append(HmipEsiGasCurrentGasFlow(hap, device))
if ch.gasVolume is not None:
entities.append(HmipEsiGasGasVolume(hap, device))
entities.extend(
entity
for entity in new_entities
if entity.entity_description.exists_fn(ch)
)
if ch.connectedEnergySensorType == ESI_CONNECTED_SENSOR_TYPE_LED:
if ch.currentPowerConsumption is not None:
entities.append(HmipEsiLedCurrentPowerConsumption(hap, device))
entities.append(HmipEsiLedEnergyCounterHighTariff(hap, device))
async_add_entities(entities)
@@ -434,132 +441,185 @@ class HomematicpTemperatureExternalSensorDelta(HomematicipGenericEntity, SensorE
return self._device.temperatureExternalDelta
@dataclass(kw_only=True, frozen=True)
class HmipEsiSensorEntityDescription(SensorEntityDescription):
"""SensorEntityDescription for HmIP Sensors."""
value_fn: Callable[[AsyncEnergySensorsInterface], StateType]
exists_fn: Callable[[FunctionalChannel], bool]
type_fn: Callable[[AsyncEnergySensorsInterface], str]
SENSORS_ESI = {
ESI_CONNECTED_SENSOR_TYPE_IEC: [
HmipEsiSensorEntityDescription(
key=ESI_TYPE_CURRENT_POWER_CONSUMPTION,
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda device: device.functional_channel.currentPowerConsumption,
exists_fn=lambda channel: channel.currentPowerConsumption is not None,
type_fn=lambda device: "CurrentPowerConsumption",
),
HmipEsiSensorEntityDescription(
key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda device: device.functional_channel.energyCounterOne,
exists_fn=lambda channel: channel.energyCounterOneType != ESI_TYPE_UNKNOWN,
type_fn=lambda device: device.functional_channel.energyCounterOneType,
),
HmipEsiSensorEntityDescription(
key=ESI_TYPE_ENERGY_COUNTER_USAGE_LOW_TARIFF,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda device: device.functional_channel.energyCounterTwo,
exists_fn=lambda channel: channel.energyCounterTwoType != ESI_TYPE_UNKNOWN,
type_fn=lambda device: device.functional_channel.energyCounterTwoType,
),
HmipEsiSensorEntityDescription(
key=ESI_TYPE_ENERGY_COUNTER_INPUT_SINGLE_TARIFF,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda device: device.functional_channel.energyCounterThree,
exists_fn=lambda channel: channel.energyCounterThreeType
!= ESI_TYPE_UNKNOWN,
type_fn=lambda device: device.functional_channel.energyCounterThreeType,
),
],
ESI_CONNECTED_SENSOR_TYPE_LED: [
HmipEsiSensorEntityDescription(
key=ESI_TYPE_CURRENT_POWER_CONSUMPTION,
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda device: device.functional_channel.currentPowerConsumption,
exists_fn=lambda channel: channel.currentPowerConsumption is not None,
type_fn=lambda device: "CurrentPowerConsumption",
),
HmipEsiSensorEntityDescription(
key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda device: device.functional_channel.energyCounterOne,
exists_fn=lambda channel: channel.energyCounterOne is not None,
type_fn=lambda device: ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
),
],
ESI_CONNECTED_SENSOR_TYPE_GAS: [
HmipEsiSensorEntityDescription(
key=ESI_TYPE_CURRENT_GAS_FLOW,
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda device: device.functional_channel.currentGasFlow,
exists_fn=lambda channel: channel.currentGasFlow is not None,
type_fn=lambda device: "CurrentGasFlow",
),
HmipEsiSensorEntityDescription(
key=ESI_TYPE_CURRENT_GAS_VOLUME,
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
device_class=SensorDeviceClass.VOLUME,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda device: device.functional_channel.gasVolume,
exists_fn=lambda channel: channel.gasVolume is not None,
type_fn=lambda device: "GasVolume",
),
],
}
class HmipEsiSensorEntity(HomematicipGenericEntity, SensorEntity):
"""EntityDescription for HmIP-ESI Sensors."""
entity_description: HmipEsiSensorEntityDescription
def __init__(
self,
hap: HomematicipHAP,
device: HomematicipGenericEntity,
channel_index: int,
entity_description: HmipEsiSensorEntityDescription,
key: str,
value_fn: Callable[[FunctionalChannel], StateType],
type_fn: Callable[[FunctionalChannel], str],
) -> None:
"""Initialize Sensor Entity."""
super().__init__(
hap=hap,
device=device,
channel=channel_index,
post=entity_description.key,
channel=1,
post=key,
is_multi_channel=False,
)
self.entity_description = entity_description
self._value_fn = value_fn
self._type_fn = type_fn
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the esi sensor."""
state_attr = super().extra_state_attributes
state_attr[ATTR_ESI_TYPE] = self.entity_description.type_fn(self)
state_attr[ATTR_ESI_TYPE] = self._type_fn(self.functional_channel)
return state_attr
@property
def native_value(self) -> str | None:
"""Return the state of the sensor."""
return str(self.entity_description.value_fn(self))
return str(self._value_fn(self.functional_channel))
class HmipEsiIecPowerConsumption(HmipEsiSensorEntity):
"""Representation of the Hmip-ESI IEC currentPowerConsumption sensor."""
_attr_device_class = SensorDeviceClass.POWER
_attr_native_unit_of_measurement = UnitOfPower.WATT
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
super().__init__(
hap,
device,
key="CurrentPowerConsumption",
value_fn=lambda channel: channel.currentPowerConsumption,
type_fn=lambda channel: "CurrentPowerConsumption",
)
class HmipEsiIecEnergyCounterHighTariff(HmipEsiSensorEntity):
"""Representation of the Hmip-ESI IEC energyCounterOne sensor."""
_attr_device_class = SensorDeviceClass.ENERGY
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
_attr_state_class = SensorStateClass.TOTAL_INCREASING
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
super().__init__(
hap,
device,
key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
value_fn=lambda channel: channel.energyCounterOne,
type_fn=lambda channel: channel.energyCounterOneType,
)
class HmipEsiIecEnergyCounterLowTariff(HmipEsiSensorEntity):
"""Representation of the Hmip-ESI IEC energyCounterTwo sensor."""
_attr_device_class = SensorDeviceClass.ENERGY
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
_attr_state_class = SensorStateClass.TOTAL_INCREASING
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
super().__init__(
hap,
device,
key=ESI_TYPE_ENERGY_COUNTER_USAGE_LOW_TARIFF,
value_fn=lambda channel: channel.energyCounterTwo,
type_fn=lambda channel: channel.energyCounterTwoType,
)
class HmipEsiIecEnergyCounterInputSingleTariff(HmipEsiSensorEntity):
"""Representation of the Hmip-ESI IEC energyCounterThree sensor."""
_attr_device_class = SensorDeviceClass.ENERGY
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
_attr_state_class = SensorStateClass.TOTAL_INCREASING
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
super().__init__(
hap,
device,
key=ESI_TYPE_ENERGY_COUNTER_INPUT_SINGLE_TARIFF,
value_fn=lambda channel: channel.energyCounterThree,
type_fn=lambda channel: channel.energyCounterThreeType,
)
class HmipEsiGasCurrentGasFlow(HmipEsiSensorEntity):
"""Representation of the Hmip-ESI Gas currentGasFlow sensor."""
_attr_device_class = SensorDeviceClass.VOLUME_FLOW_RATE
_attr_native_unit_of_measurement = UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
super().__init__(
hap,
device,
key="CurrentGasFlow",
value_fn=lambda channel: channel.currentGasFlow,
type_fn=lambda channel: "CurrentGasFlow",
)
class HmipEsiGasGasVolume(HmipEsiSensorEntity):
"""Representation of the Hmip-ESI Gas gasVolume sensor."""
_attr_device_class = SensorDeviceClass.GAS
_attr_native_unit_of_measurement = UnitOfVolume.CUBIC_METERS
_attr_state_class = SensorStateClass.TOTAL_INCREASING
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
super().__init__(
hap,
device,
key="GasVolume",
value_fn=lambda channel: channel.gasVolume,
type_fn=lambda channel: "GasVolume",
)
class HmipEsiLedCurrentPowerConsumption(HmipEsiSensorEntity):
"""Representation of the Hmip-ESI LED currentPowerConsumption sensor."""
_attr_device_class = SensorDeviceClass.POWER
_attr_native_unit_of_measurement = UnitOfPower.WATT
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
super().__init__(
hap,
device,
key="CurrentPowerConsumption",
value_fn=lambda channel: channel.currentPowerConsumption,
type_fn=lambda channel: "CurrentPowerConsumption",
)
class HmipEsiLedEnergyCounterHighTariff(HmipEsiSensorEntity):
"""Representation of the Hmip-ESI LED energyCounterOne sensor."""
_attr_device_class = SensorDeviceClass.ENERGY
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
_attr_state_class = SensorStateClass.TOTAL_INCREASING
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
super().__init__(
hap,
device,
key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
value_fn=lambda channel: channel.energyCounterOne,
type_fn=lambda channel: ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
)
class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity, SensorEntity):

View File

@@ -184,6 +184,8 @@ RESTRICTED_REASONS: list = [
RestrictedReasons.WEEK_SCHEDULE.lower(),
]
STATE_NO_WORK_AREA_ACTIVE = "no_work_area_active"
@callback
def _get_work_area_names(data: MowerAttributes) -> list[str]:
@@ -191,16 +193,21 @@ def _get_work_area_names(data: MowerAttributes) -> list[str]:
if TYPE_CHECKING:
# Sensor does not get created if it is None
assert data.work_areas is not None
return [data.work_areas[work_area_id].name for work_area_id in data.work_areas]
work_area_list = [
data.work_areas[work_area_id].name for work_area_id in data.work_areas
]
work_area_list.append(STATE_NO_WORK_AREA_ACTIVE)
return work_area_list
@callback
def _get_current_work_area_name(data: MowerAttributes) -> str:
"""Return the name of the current work area."""
if data.mower.work_area_id is None:
return STATE_NO_WORK_AREA_ACTIVE
if TYPE_CHECKING:
# Sensor does not get created if values are None
assert data.work_areas is not None
assert data.mower.work_area_id is not None
return data.work_areas[data.mower.work_area_id].name

View File

@@ -252,7 +252,8 @@
"work_area": {
"name": "Work area",
"state": {
"my_lawn": "My lawn"
"my_lawn": "My lawn",
"no_work_area_active": "No work area active"
}
}
},

View File

@@ -6,7 +6,7 @@
"description": "Enter your credentials",
"data": {
"username": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]",
"password": "App-specific password",
"with_family": "With family"
}
},
@@ -14,7 +14,7 @@
"title": "[%key:common::config_flow::title::reauth%]",
"description": "Your previously entered password for {username} is no longer working. Update your password to keep using this integration.",
"data": {
"password": "[%key:common::config_flow::data::password%]"
"password": "App-specific password"
}
},
"trusted_device": {

View File

@@ -12,5 +12,5 @@
"documentation": "https://www.home-assistant.io/integrations/idasen_desk",
"iot_class": "local_push",
"quality_scale": "silver",
"requirements": ["idasen-ha==2.6.1"]
"requirements": ["idasen-ha==2.6.2"]
}

View File

@@ -28,5 +28,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/inkbird",
"iot_class": "local_push",
"requirements": ["inkbird-ble==0.5.7"]
"requirements": ["inkbird-ble==0.5.8"]
}

View File

@@ -8,6 +8,7 @@ from typing import Any
from pyecotrend_ista import KeycloakError, LoginError, PyEcotrendIsta, ServerError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
@@ -21,6 +22,8 @@ _LOGGER = logging.getLogger(__name__)
class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Ista EcoTrend data update coordinator."""
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant, ista: PyEcotrendIsta) -> None:
"""Initialize ista EcoTrend data update coordinator."""
super().__init__(
@@ -35,11 +38,14 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]):
async def _async_update_data(self):
"""Fetch ista EcoTrend data."""
if not self.details:
self.details = await self.async_get_details()
try:
await self.hass.async_add_executor_job(self.ista.login)
if not self.details:
self.details = await self.async_get_details()
return await self.hass.async_add_executor_job(self.get_consumption_data)
except ServerError as e:
raise UpdateFailed(
"Unable to connect and retrieve data from ista EcoTrend, try again later"
@@ -48,7 +54,9 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]):
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="authentication_exception",
translation_placeholders={CONF_EMAIL: self.ista._email}, # noqa: SLF001
translation_placeholders={
CONF_EMAIL: self.config_entry.data[CONF_EMAIL]
},
) from e
def get_consumption_data(self) -> dict[str, Any]:
@@ -61,26 +69,16 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]):
async def async_get_details(self) -> dict[str, Any]:
"""Retrieve details of consumption units."""
try:
result = await self.hass.async_add_executor_job(
self.ista.get_consumption_unit_details
result = await self.hass.async_add_executor_job(
self.ista.get_consumption_unit_details
)
return {
consumption_unit: next(
details
for details in result["consumptionUnits"]
if details["id"] == consumption_unit
)
except ServerError as e:
raise UpdateFailed(
"Unable to connect and retrieve data from ista EcoTrend, try again later"
) from e
except (LoginError, KeycloakError) as e:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="authentication_exception",
translation_placeholders={CONF_EMAIL: self.ista._email}, # noqa: SLF001
) from e
else:
return {
consumption_unit: next(
details
for details in result["consumptionUnits"]
if details["id"] == consumption_unit
)
for consumption_unit in self.ista.get_uuids()
}
for consumption_unit in self.ista.get_uuids()
}

View File

@@ -97,7 +97,11 @@ class JellyfinConfigFlow(ConfigFlow, domain=DOMAIN):
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
step_id="user",
data_schema=self.add_suggested_values_to_schema(
STEP_USER_DATA_SCHEMA, user_input
),
errors=errors,
)
async def async_step_reauth(

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Any
from knocki import KnockiClient, KnockiConnectionError
from knocki import KnockiClient, KnockiConnectionError, KnockiInvalidAuthError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@@ -45,6 +45,8 @@ class KnockiConfigFlow(ConfigFlow, domain=DOMAIN):
raise
except KnockiConnectionError:
errors["base"] = "cannot_connect"
except KnockiInvalidAuthError:
errors["base"] = "invalid_auth"
except Exception: # noqa: BLE001
LOGGER.exception("Error logging into the Knocki API")
errors["base"] = "unknown"

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "cloud_push",
"loggers": ["knocki"],
"requirements": ["knocki==0.2.0"]
"requirements": ["knocki==0.3.1"]
}

View File

@@ -641,12 +641,10 @@ class KodiEntity(MediaPlayerEntity):
if self.state == MediaPlayerState.OFF:
return state_attr
hdr_type = (
self._item.get("streamdetails", {}).get("video", [{}])[0].get("hdrtype")
)
if hdr_type == "":
state_attr["dynamic_range"] = "sdr"
else:
state_attr["dynamic_range"] = "sdr"
if (video_details := self._item.get("streamdetails", {}).get("video")) and (
hdr_type := video_details[0].get("hdrtype")
):
state_attr["dynamic_range"] = hdr_type
return state_attr

View File

@@ -1,7 +1,7 @@
{
"domain": "lifx",
"name": "LIFX",
"codeowners": [],
"codeowners": ["@Djelibeybi"],
"config_flow": true,
"dependencies": ["network"],
"dhcp": [
@@ -48,7 +48,7 @@
"iot_class": "local_polling",
"loggers": ["aiolifx", "aiolifx_effects", "bitstring"],
"requirements": [
"aiolifx==1.0.2",
"aiolifx==1.0.5",
"aiolifx-effects==0.3.2",
"aiolifx-themes==0.4.15"
]

View File

@@ -145,4 +145,20 @@ DISCOVERY_SCHEMAS = [
required_attributes=(clusters.BooleanState.Attributes.StateValue,),
device_type=(device_types.RainSensor,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="LockDoorStateSensor",
device_class=BinarySensorDeviceClass.DOOR,
# pylint: disable=unnecessary-lambda
measurement_to_ha=lambda x: {
clusters.DoorLock.Enums.DoorStateEnum.kDoorOpen: True,
clusters.DoorLock.Enums.DoorStateEnum.kDoorJammed: True,
clusters.DoorLock.Enums.DoorStateEnum.kDoorForcedOpen: True,
clusters.DoorLock.Enums.DoorStateEnum.kDoorClosed: False,
}.get(x),
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.DoorLock.Attributes.DoorState,),
),
]

View File

@@ -350,6 +350,7 @@ DISCOVERY_SCHEMAS = [
clusters.Thermostat.Attributes.TemperatureSetpointHold,
clusters.Thermostat.Attributes.UnoccupiedCoolingSetpoint,
clusters.Thermostat.Attributes.UnoccupiedHeatingSetpoint,
clusters.OnOff.Attributes.OnOff,
),
device_type=(device_types.Thermostat, device_types.RoomAirConditioner),
),

View File

@@ -313,6 +313,7 @@ DISCOVERY_SCHEMAS = [
clusters.FanControl.Attributes.RockSetting,
clusters.FanControl.Attributes.WindSetting,
clusters.FanControl.Attributes.AirflowDirection,
clusters.OnOff.Attributes.OnOff,
),
),
]

View File

@@ -446,6 +446,8 @@ DISCOVERY_SCHEMAS = [
device_types.DimmablePlugInUnit,
device_types.ExtendedColorLight,
device_types.OnOffLight,
device_types.DimmerSwitch,
device_types.ColorDimmerSwitch,
),
),
# Additional schema to match (HS Color) lights with incorrect/missing device type

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
from typing import Any
from chip.clusters import Objects as clusters
@@ -38,6 +39,7 @@ class MatterLock(MatterEntity, LockEntity):
"""Representation of a Matter lock."""
features: int | None = None
_optimistic_timer: asyncio.TimerHandle | None = None
@property
def code_format(self) -> str | None:
@@ -90,6 +92,15 @@ class MatterLock(MatterEntity, LockEntity):
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the lock with pin if needed."""
if not self._attr_is_locked:
# optimistically signal locking to state machine
self._attr_is_locking = True
self.async_write_ha_state()
# the lock should acknowledge the command with an attribute update
# but bad things may happen, so guard against it with a timer.
self._optimistic_timer = self.hass.loop.call_later(
30, self._reset_optimistic_state
)
code: str | None = kwargs.get(ATTR_CODE)
code_bytes = code.encode() if code else None
await self.send_device_command(
@@ -98,6 +109,15 @@ class MatterLock(MatterEntity, LockEntity):
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the lock with pin if needed."""
if self._attr_is_locked:
# optimistically signal unlocking to state machine
self._attr_is_unlocking = True
self.async_write_ha_state()
# the lock should acknowledge the command with an attribute update
# but bad things may happen, so guard against it with a timer.
self._optimistic_timer = self.hass.loop.call_later(
30, self._reset_optimistic_state
)
code: str | None = kwargs.get(ATTR_CODE)
code_bytes = code.encode() if code else None
if self.supports_unbolt:
@@ -114,6 +134,14 @@ class MatterLock(MatterEntity, LockEntity):
async def async_open(self, **kwargs: Any) -> None:
"""Open the door latch."""
# optimistically signal opening to state machine
self._attr_is_opening = True
self.async_write_ha_state()
# the lock should acknowledge the command with an attribute update
# but bad things may happen, so guard against it with a timer.
self._optimistic_timer = self.hass.loop.call_later(
30 if self._attr_is_locked else 5, self._reset_optimistic_state
)
code: str | None = kwargs.get(ATTR_CODE)
code_bytes = code.encode() if code else None
await self.send_device_command(
@@ -135,42 +163,39 @@ class MatterLock(MatterEntity, LockEntity):
clusters.DoorLock.Attributes.LockState
)
# always reset the optimisically (un)locking state on state update
self._reset_optimistic_state(write_state=False)
LOGGER.debug("Lock state: %s for %s", lock_state, self.entity_id)
if lock_state is clusters.DoorLock.Enums.DlLockState.kUnlatched:
self._attr_is_locked = False
self._attr_is_open = True
if lock_state is clusters.DoorLock.Enums.DlLockState.kLocked:
self._attr_is_locked = True
self._attr_is_locking = False
self._attr_is_unlocking = False
elif lock_state is clusters.DoorLock.Enums.DlLockState.kUnlocked:
self._attr_is_open = False
elif lock_state in (
clusters.DoorLock.Enums.DlLockState.kUnlocked,
clusters.DoorLock.Enums.DlLockState.kNotFullyLocked,
):
self._attr_is_locked = False
self._attr_is_locking = False
self._attr_is_unlocking = False
elif lock_state is clusters.DoorLock.Enums.DlLockState.kNotFullyLocked:
if self.is_locked is True:
self._attr_is_unlocking = True
elif self.is_locked is False:
self._attr_is_locking = True
self._attr_is_open = False
else:
# According to the matter docs a null state can happen during device startup.
# Treat any other state as unknown.
# NOTE: A null state can happen during device startup.
self._attr_is_locked = None
self._attr_is_locking = None
self._attr_is_unlocking = None
self._attr_is_open = None
if self.supports_door_position_sensor:
door_state = self.get_matter_attribute_value(
clusters.DoorLock.Attributes.DoorState
)
assert door_state is not None
LOGGER.debug("Door state: %s for %s", door_state, self.entity_id)
self._attr_is_jammed = (
door_state is clusters.DoorLock.Enums.DoorStateEnum.kDoorJammed
)
self._attr_is_open = (
door_state is clusters.DoorLock.Enums.DoorStateEnum.kDoorOpen
)
@callback
def _reset_optimistic_state(self, write_state: bool = True) -> None:
if self._optimistic_timer and not self._optimistic_timer.cancelled():
self._optimistic_timer.cancel()
self._optimistic_timer = None
self._attr_is_locking = False
self._attr_is_unlocking = False
self._attr_is_opening = False
if write_state:
self.async_write_ha_state()
DISCOVERY_SCHEMAS = [

View File

@@ -114,6 +114,7 @@ DISCOVERY_SCHEMAS = [
device_types.ColorTemperatureLight,
device_types.DimmableLight,
device_types.ExtendedColorLight,
device_types.DimmerSwitch,
device_types.ColorDimmerSwitch,
device_types.OnOffLight,
device_types.AirPurifier,

View File

@@ -60,7 +60,8 @@ class MealieMealplanCalendarEntity(MealieEntity, CalendarEntity):
mealplans = self.coordinator.data[self._entry_type]
if not mealplans:
return None
return _get_event_from_mealplan(mealplans[0])
sorted_mealplans = sorted(mealplans, key=lambda x: x.mealplan_date)
return _get_event_from_mealplan(sorted_mealplans[0])
async def async_get_events(
self, hass: HomeAssistant, start_date: datetime, end_date: datetime

View File

@@ -3,8 +3,11 @@
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"host": "[%key:common::config_flow::data::url%]",
"api_token": "[%key:common::config_flow::data::api_token%]"
},
"data_description": {
"host": "The URL of your Mealie instance."
}
}
},

View File

@@ -8,6 +8,6 @@
"iot_class": "calculated",
"loggers": ["yt_dlp"],
"quality_scale": "internal",
"requirements": ["yt-dlp==2024.07.01"],
"requirements": ["yt-dlp==2024.07.16"],
"single_config_entry": true
}

View File

@@ -721,10 +721,15 @@ async def webhook_get_config(
"""Handle a get config webhook."""
hass_config = hass.config.as_dict()
device: dr.DeviceEntry = hass.data[DOMAIN][DATA_DEVICES][
config_entry.data[CONF_WEBHOOK_ID]
]
resp = {
"latitude": hass_config["latitude"],
"longitude": hass_config["longitude"],
"elevation": hass_config["elevation"],
"hass_device_id": device.id,
"unit_system": hass_config["unit_system"],
"location_name": hass_config["location_name"],
"time_zone": hass_config["time_zone"],

View File

@@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pymodbus"],
"quality_scale": "platinum",
"requirements": ["pymodbus==3.6.8"]
"requirements": ["pymodbus==3.6.9"]
}

View File

@@ -2,6 +2,7 @@
"domain": "mpd",
"name": "Music Player Daemon (MPD)",
"codeowners": [],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/mpd",
"iot_class": "local_polling",
"loggers": ["mpd"],

View File

@@ -73,7 +73,7 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
}
),
cv.has_at_least_one_key("auth"),
cv.has_at_least_one_key(CONF_API_KEY, CONF_PASSWORD),
)
},
extra=vol.ALLOW_EXTRA,

View File

@@ -470,3 +470,8 @@ class OpenThermGatewayDevice:
async_dispatcher_send(self.hass, self.update_signal, status)
self.gateway.subscribe(handle_report)
@property
def connected(self):
"""Report whether or not we are connected to the gateway."""
return self.gateway.connection.connected

View File

@@ -48,6 +48,7 @@ class OpenThermBinarySensor(BinarySensorEntity):
_attr_should_poll = False
_attr_entity_registry_enabled_default = False
_attr_available = False
def __init__(self, gw_dev, var, source, device_class, friendly_name_format):
"""Initialize the binary sensor."""
@@ -85,14 +86,10 @@ class OpenThermBinarySensor(BinarySensorEntity):
_LOGGER.debug("Removing OpenTherm Gateway binary sensor %s", self._attr_name)
self._unsub_updates()
@property
def available(self):
"""Return availability of the sensor."""
return self._attr_is_on is not None
@callback
def receive_report(self, status):
"""Handle status updates from the component."""
self._attr_available = self._gateway.connected
state = status[self._source].get(self._var)
self._attr_is_on = None if state is None else bool(state)
self.async_write_ha_state()

View File

@@ -138,7 +138,7 @@ class OpenThermClimate(ClimateEntity):
@callback
def receive_report(self, status):
"""Receive and handle a new report from the Gateway."""
self._attr_available = status != gw_vars.DEFAULT_STATUS
self._attr_available = self._gateway.connected
ch_active = status[gw_vars.BOILER].get(gw_vars.DATA_SLAVE_CH_ACTIVE)
flame_on = status[gw_vars.BOILER].get(gw_vars.DATA_SLAVE_FLAME_ON)
cooling_active = status[gw_vars.BOILER].get(gw_vars.DATA_SLAVE_COOLING_ACTIVE)

View File

@@ -45,6 +45,7 @@ class OpenThermSensor(SensorEntity):
_attr_should_poll = False
_attr_entity_registry_enabled_default = False
_attr_available = False
def __init__(
self,
@@ -94,14 +95,10 @@ class OpenThermSensor(SensorEntity):
_LOGGER.debug("Removing OpenTherm Gateway sensor %s", self._attr_name)
self._unsub_updates()
@property
def available(self):
"""Return availability of the sensor."""
return self._attr_native_value is not None
@callback
def receive_report(self, status):
"""Handle status updates from the component."""
self._attr_available = self._gateway.connected
value = status[self._source].get(self._var)
self._attr_native_value = value
self.async_write_ha_state()

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/opower",
"iot_class": "cloud_polling",
"loggers": ["opower"],
"requirements": ["opower==0.4.7"]
"requirements": ["opower==0.5.2"]
}

View File

@@ -385,6 +385,6 @@ class PhilipsTVLightEntity(PhilipsJsEntity, LightEntity):
"""Return true if entity is available."""
if not super().available:
return False
if not self.coordinator.api.on:
if not self._tv.on:
return False
return self.coordinator.api.powerstate == "On"
return True

View File

@@ -30,7 +30,7 @@ class PyLoadData:
speed: float
download: bool
reconnect: bool
captcha: bool
captcha: bool | None = None
free_space: int

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["pyloadapi"],
"quality_scale": "platinum",
"requirements": ["PyLoadAPI==1.2.0"]
"requirements": ["PyLoadAPI==1.3.2"]
}

View File

@@ -167,7 +167,7 @@ class RAVEnDataCoordinator(DataUpdateCoordinator):
await device.synchronize()
self._device_info = await device.get_device_info()
except:
await device.close()
await device.abort()
raise
self._raven_device = device

View File

@@ -6,7 +6,7 @@
"dependencies": ["usb"],
"documentation": "https://www.home-assistant.io/integrations/rainforest_raven",
"iot_class": "local_polling",
"requirements": ["aioraven==0.6.0"],
"requirements": ["aioraven==0.7.0"],
"usb": [
{
"vid": "0403",

View File

@@ -44,5 +44,95 @@
"title": "[%key:component::random::config::step::sensor::title%]"
}
}
},
"selector": {
"binary_sensor_device_class": {
"options": {
"battery": "[%key:component::binary_sensor::entity_component::battery::name%]",
"battery_charging": "[%key:component::binary_sensor::entity_component::battery_charging::name%]",
"carbon_monoxide": "[%key:component::binary_sensor::entity_component::carbon_monoxide::name%]",
"connectivity": "[%key:component::binary_sensor::entity_component::connectivity::name%]",
"cold": "[%key:component::binary_sensor::entity_component::cold::name%]",
"door": "[%key:component::binary_sensor::entity_component::door::name%]",
"garage_door": "[%key:component::binary_sensor::entity_component::garage_door::name%]",
"gas": "[%key:component::binary_sensor::entity_component::gas::name%]",
"heat": "[%key:component::binary_sensor::entity_component::heat::name%]",
"light": "[%key:component::binary_sensor::entity_component::light::name%]",
"lock": "[%key:component::binary_sensor::entity_component::lock::name%]",
"moisture": "[%key:component::binary_sensor::entity_component::moisture::name%]",
"motion": "[%key:component::binary_sensor::entity_component::motion::name%]",
"moving": "[%key:component::binary_sensor::entity_component::moving::name%]",
"occupancy": "[%key:component::binary_sensor::entity_component::occupancy::name%]",
"opening": "[%key:component::binary_sensor::entity_component::opening::name%]",
"plug": "[%key:component::binary_sensor::entity_component::plug::name%]",
"power": "[%key:component::binary_sensor::entity_component::power::name%]",
"presence": "[%key:component::binary_sensor::entity_component::presence::name%]",
"problem": "[%key:component::binary_sensor::entity_component::problem::name%]",
"running": "[%key:component::binary_sensor::entity_component::running::name%]",
"safety": "[%key:component::binary_sensor::entity_component::safety::name%]",
"smoke": "[%key:component::binary_sensor::entity_component::smoke::name%]",
"sound": "[%key:component::binary_sensor::entity_component::sound::name%]",
"tamper": "[%key:component::binary_sensor::entity_component::tamper::name%]",
"update": "[%key:component::binary_sensor::entity_component::update::name%]",
"vibration": "[%key:component::binary_sensor::entity_component::vibration::name%]",
"window": "[%key:component::binary_sensor::entity_component::window::name%]"
}
},
"sensor_device_class": {
"options": {
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
"atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]",
"battery": "[%key:component::sensor::entity_component::battery::name%]",
"carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
"carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
"conductivity": "[%key:component::sensor::entity_component::conductivity::name%]",
"current": "[%key:component::sensor::entity_component::current::name%]",
"data_rate": "[%key:component::sensor::entity_component::data_rate::name%]",
"data_size": "[%key:component::sensor::entity_component::data_size::name%]",
"date": "[%key:component::sensor::entity_component::date::name%]",
"distance": "[%key:component::sensor::entity_component::distance::name%]",
"duration": "[%key:component::sensor::entity_component::duration::name%]",
"energy": "[%key:component::sensor::entity_component::energy::name%]",
"energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]",
"frequency": "[%key:component::sensor::entity_component::frequency::name%]",
"gas": "[%key:component::sensor::entity_component::gas::name%]",
"humidity": "[%key:component::sensor::entity_component::humidity::name%]",
"illuminance": "[%key:component::sensor::entity_component::illuminance::name%]",
"irradiance": "[%key:component::sensor::entity_component::irradiance::name%]",
"moisture": "[%key:component::sensor::entity_component::moisture::name%]",
"monetary": "[%key:component::sensor::entity_component::monetary::name%]",
"nitrogen_dioxide": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
"nitrogen_monoxide": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]",
"nitrous_oxide": "[%key:component::sensor::entity_component::nitrous_oxide::name%]",
"ozone": "[%key:component::sensor::entity_component::ozone::name%]",
"ph": "[%key:component::sensor::entity_component::ph::name%]",
"pm1": "[%key:component::sensor::entity_component::pm1::name%]",
"pm10": "[%key:component::sensor::entity_component::pm10::name%]",
"pm25": "[%key:component::sensor::entity_component::pm25::name%]",
"power": "[%key:component::sensor::entity_component::power::name%]",
"power_factor": "[%key:component::sensor::entity_component::power_factor::name%]",
"precipitation": "[%key:component::sensor::entity_component::precipitation::name%]",
"precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]",
"pressure": "[%key:component::sensor::entity_component::pressure::name%]",
"reactive_power": "[%key:component::sensor::entity_component::reactive_power::name%]",
"signal_strength": "[%key:component::sensor::entity_component::signal_strength::name%]",
"sound_pressure": "[%key:component::sensor::entity_component::sound_pressure::name%]",
"speed": "[%key:component::sensor::entity_component::speed::name%]",
"sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]",
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
"volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
"voltage": "[%key:component::sensor::entity_component::voltage::name%]",
"volume": "[%key:component::sensor::entity_component::volume::name%]",
"volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]",
"volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]",
"water": "[%key:component::sensor::entity_component::water::name%]",
"weight": "[%key:component::sensor::entity_component::weight::name%]",
"wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]"
}
}
}
}

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["vehicle==2.2.1"]
"requirements": ["vehicle==2.2.2"]
}

View File

@@ -1178,7 +1178,15 @@ class Recorder(threading.Thread):
def _handle_database_error(self, err: Exception) -> bool:
"""Handle a database error that may result in moving away the corrupt db."""
if isinstance(err.__cause__, sqlite3.DatabaseError):
if (
(cause := err.__cause__)
and isinstance(cause, sqlite3.DatabaseError)
and (cause_str := str(cause))
# Make sure we do not move away a database when its only locked
# externally by another process. sqlite does not give us a named
# exception for this so we have to check the error message.
and ("malformed" in cause_str or "not a database" in cause_str)
):
_LOGGER.exception(
"Unrecoverable sqlite3 database corruption detected: %s", err
)

View File

@@ -24,7 +24,7 @@ from sqlalchemy.exc import (
SQLAlchemyError,
)
from sqlalchemy.orm.session import Session
from sqlalchemy.schema import AddConstraint, DropConstraint
from sqlalchemy.schema import AddConstraint, CreateTable, DropConstraint
from sqlalchemy.sql.expression import true
from sqlalchemy.sql.lambdas import StatementLambdaElement
@@ -313,11 +313,9 @@ def _create_index(
index = index_list[0]
_LOGGER.debug("Creating %s index", index_name)
_LOGGER.warning(
(
"Adding index `%s` to table `%s`. Note: this can take several "
"minutes on large databases and slow computers. Please "
"be patient!"
),
"Adding index `%s` to table `%s`. Note: this can take several "
"minutes on large databases and slow computers. Please "
"be patient!",
index_name,
table_name,
)
@@ -331,7 +329,7 @@ def _create_index(
"Index %s already exists on %s, continuing", index_name, table_name
)
_LOGGER.debug("Finished creating %s", index_name)
_LOGGER.warning("Finished adding index `%s` to table `%s`", index_name, table_name)
def _execute_or_collect_error(
@@ -364,11 +362,9 @@ def _drop_index(
DO NOT USE THIS FUNCTION IN ANY OPERATION THAT TAKES USER INPUT.
"""
_LOGGER.warning(
(
"Dropping index `%s` from table `%s`. Note: this can take several "
"minutes on large databases and slow computers. Please "
"be patient!"
),
"Dropping index `%s` from table `%s`. Note: this can take several "
"minutes on large databases and slow computers. Please "
"be patient!",
index_name,
table_name,
)
@@ -377,8 +373,8 @@ def _drop_index(
index_to_drop = get_index_by_name(session, table_name, index_name)
if index_to_drop is None:
_LOGGER.debug(
"The index %s on table %s no longer exists", index_name, table_name
_LOGGER.warning(
"The index `%s` on table `%s` no longer exists", index_name, table_name
)
return
@@ -395,18 +391,16 @@ def _drop_index(
f"DROP INDEX {index_to_drop}",
):
if _execute_or_collect_error(session_maker, query, errors):
_LOGGER.debug(
"Finished dropping index %s from table %s", index_name, table_name
_LOGGER.warning(
"Finished dropping index `%s` from table `%s`", index_name, table_name
)
return
if not quiet:
_LOGGER.warning(
(
"Failed to drop index `%s` from table `%s`. Schema "
"Migration will continue; this is not a "
"critical operation: %s"
),
"Failed to drop index `%s` from table `%s`. Schema "
"Migration will continue; this is not a "
"critical operation: %s",
index_name,
table_name,
errors,
@@ -1738,14 +1732,15 @@ def cleanup_legacy_states_event_ids(instance: Recorder) -> bool:
# Only drop the index if there are no more event_ids in the states table
# ex all NULL
assert instance.engine is not None, "engine should never be None"
if instance.dialect_name != SupportedDialect.SQLITE:
if instance.dialect_name == SupportedDialect.SQLITE:
# SQLite does not support dropping foreign key constraints
# so we can't drop the index at this time but we can avoid
# looking for legacy rows during purge
# so we have to rebuild the table
rebuild_sqlite_table(session_maker, instance.engine, States)
else:
_drop_foreign_key_constraints(
session_maker, instance.engine, TABLE_STATES, ["event_id"]
)
_drop_index(session_maker, "states", LEGACY_STATES_EVENT_ID_INDEX)
_drop_index(session_maker, "states", LEGACY_STATES_EVENT_ID_INDEX)
instance.use_legacy_events_index = False
return True
@@ -1894,3 +1889,68 @@ def _mark_migration_done(
migration_id=migration.migration_id, version=migration.migration_version
)
)
def rebuild_sqlite_table(
session_maker: Callable[[], Session], engine: Engine, table: type[Base]
) -> None:
"""Rebuild an SQLite table.
This must only be called after all migrations are complete
and the database is in a consistent state.
If the table is not migrated to the current schema this
will likely fail.
"""
table_table = cast(Table, table.__table__)
orig_name = table_table.name
temp_name = f"{table_table.name}_temp_{int(time())}"
_LOGGER.warning(
"Rebuilding SQLite table %s; This will take a while; Please be patient!",
orig_name,
)
try:
# 12 step SQLite table rebuild
# https://www.sqlite.org/lang_altertable.html
with session_scope(session=session_maker()) as session:
# Step 1 - Disable foreign keys
session.connection().execute(text("PRAGMA foreign_keys=OFF"))
# Step 2 - create a transaction
with session_scope(session=session_maker()) as session:
# Step 3 - we know all the indexes, triggers, and views associated with table X
new_sql = str(CreateTable(table_table).compile(engine)).strip("\n") + ";"
source_sql = f"CREATE TABLE {orig_name}"
replacement_sql = f"CREATE TABLE {temp_name}"
assert source_sql in new_sql, f"{source_sql} should be in new_sql"
new_sql = new_sql.replace(source_sql, replacement_sql)
# Step 4 - Create temp table
session.execute(text(new_sql))
column_names = ",".join([column.name for column in table_table.columns])
# Step 5 - Transfer content
sql = f"INSERT INTO {temp_name} SELECT {column_names} FROM {orig_name};" # noqa: S608
session.execute(text(sql))
# Step 6 - Drop the original table
session.execute(text(f"DROP TABLE {orig_name}"))
# Step 7 - Rename the temp table
session.execute(text(f"ALTER TABLE {temp_name} RENAME TO {orig_name}"))
# Step 8 - Recreate indexes
for index in table_table.indexes:
index.create(session.connection())
# Step 9 - Recreate views (there are none)
# Step 10 - Check foreign keys
session.execute(text("PRAGMA foreign_key_check"))
# Step 11 - Commit transaction
session.commit()
except SQLAlchemyError:
_LOGGER.exception("Error recreating SQLite table %s", table_table.name)
# Swallow the exception since we do not want to ever raise
# an integrity error as it would cause the database
# to be discarded and recreated from scratch
else:
_LOGGER.warning("Rebuilding SQLite table %s finished", orig_name)
finally:
with session_scope(session=session_maker()) as session:
# Step 12 - Re-enable foreign keys
session.connection().execute(text("PRAGMA foreign_keys=ON"))

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import datetime as dt
import logging
from reolink_aio.api import DUAL_LENS_MODELS
from reolink_aio.enums import VodRequestType
from homeassistant.components.camera import DOMAIN as CAM_DOMAIN, DynamicStreamSettings
@@ -184,6 +185,9 @@ class ReolinkVODMediaSource(MediaSource):
if device.name_by_user is not None:
device_name = device.name_by_user
if host.api.model in DUAL_LENS_MODELS:
device_name = f"{device_name} lens {ch}"
children.append(
BrowseMediaSource(
domain=DOMAIN,

View File

@@ -214,7 +214,8 @@
"unknown": "Unknown",
"locked": "Locked",
"air_drying_stopping": "Air drying stopping",
"egg_attack": "Cupid mode"
"egg_attack": "Cupid mode",
"mapping": "Mapping"
}
},
"total_cleaning_time": {
@@ -282,7 +283,8 @@
"deep": "Deep",
"deep_plus": "Deep+",
"custom": "Custom",
"fast": "Fast"
"fast": "Fast",
"smart_mode": "SmartPlan"
}
},
"mop_intensity": {
@@ -293,10 +295,12 @@
"mild": "Mild",
"medium": "Medium",
"moderate": "Moderate",
"max": "Max",
"high": "High",
"intense": "Intense",
"custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]",
"custom_water_flow": "Custom water flow"
"custom_water_flow": "Custom water flow",
"smart_mode": "[%key:component::roborock::entity::select::mop_mode::state::smart_mode%]"
}
}
},
@@ -338,13 +342,14 @@
"custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]",
"gentle": "Gentle",
"off": "[%key:common::state::off%]",
"max": "Max",
"max": "[%key:component::roborock::entity::select::mop_intensity::state::max%]",
"max_plus": "Max plus",
"medium": "Medium",
"quiet": "Quiet",
"silent": "Silent",
"standard": "[%key:component::roborock::entity::select::mop_mode::state::standard%]",
"turbo": "Turbo"
"turbo": "Turbo",
"smart_mode": "[%key:component::roborock::entity::select::mop_mode::state::smart_mode%]"
}
}
}

View File

@@ -61,6 +61,7 @@ from .utils import (
async_create_issue_unsupported_firmware,
get_block_device_sleep_period,
get_device_entry_gen,
get_host,
get_http_port,
get_rpc_device_wakeup_period,
update_device_fw_info,
@@ -147,7 +148,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice](
model=MODEL_NAMES.get(self.model, self.model),
sw_version=self.sw_version,
hw_version=f"gen{get_device_entry_gen(self.entry)} ({self.model})",
configuration_url=f"http://{self.entry.data[CONF_HOST]}:{get_http_port(self.entry.data)}",
configuration_url=f"http://{get_host(self.entry.data[CONF_HOST])}:{get_http_port(self.entry.data)}",
)
self.device_id = device_entry.id
@@ -667,6 +668,9 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
"""Handle device update."""
LOGGER.debug("Shelly %s handle update, type: %s", self.name, update_type)
if update_type is RpcUpdateType.ONLINE:
if self.device.connected:
LOGGER.debug("Device %s already connected", self.name)
return
self.entry.async_create_background_task(
self.hass,
self._async_device_connect_task(),

View File

@@ -960,14 +960,18 @@ RPC_SENSORS: Final = {
name="Analog input",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
removal_condition=lambda config, _status, key: (config[key]["enable"] is False),
removal_condition=lambda config, _, key: (
config[key]["type"] != "analog" or config[key]["enable"] is False
),
),
"analoginput_xpercent": RpcSensorDescription(
key="input",
sub_key="xpercent",
name="Analog value",
removal_condition=lambda config, status, key: (
config[key]["enable"] is False or status[key].get("xpercent") is None
config[key]["type"] != "analog"
or config[key]["enable"] is False
or status[key].get("xpercent") is None
),
),
"pulse_counter": RpcSensorDescription(
@@ -977,7 +981,9 @@ RPC_SENSORS: Final = {
native_unit_of_measurement="pulse",
state_class=SensorStateClass.TOTAL,
value=lambda status, _: status["total"],
removal_condition=lambda config, _status, key: (config[key]["enable"] is False),
removal_condition=lambda config, _status, key: (
config[key]["type"] != "count" or config[key]["enable"] is False
),
),
"counter_value": RpcSensorDescription(
key="input",
@@ -985,26 +991,29 @@ RPC_SENSORS: Final = {
name="Counter value",
value=lambda status, _: status["xtotal"],
removal_condition=lambda config, status, key: (
config[key]["enable"] is False
config[key]["type"] != "count"
or config[key]["enable"] is False
or status[key]["counts"].get("xtotal") is None
),
),
"counter_frequency": RpcSensorDescription(
key="input",
sub_key="counts",
sub_key="freq",
name="Pulse counter frequency",
native_unit_of_measurement=UnitOfFrequency.HERTZ,
state_class=SensorStateClass.MEASUREMENT,
value=lambda status, _: status["freq"],
removal_condition=lambda config, status, key: (config[key]["enable"] is False),
removal_condition=lambda config, _, key: (
config[key]["type"] != "count" or config[key]["enable"] is False
),
),
"counter_frequency_value": RpcSensorDescription(
key="input",
sub_key="counts",
sub_key="xfreq",
name="Pulse counter frequency value",
value=lambda status, _: status["xfreq"],
removal_condition=lambda config, status, key: (
config[key]["enable"] is False or status[key]["counts"].get("xfreq") is None
config[key]["type"] != "count"
or config[key]["enable"] is False
or status[key].get("xfreq") is None
),
),
}

View File

@@ -3,7 +3,7 @@
from __future__ import annotations
from datetime import datetime, timedelta
from ipaddress import IPv4Address
from ipaddress import IPv4Address, IPv6Address, ip_address
from types import MappingProxyType
from typing import Any, cast
@@ -482,6 +482,20 @@ def get_http_port(data: MappingProxyType[str, Any]) -> int:
return cast(int, data.get(CONF_PORT, DEFAULT_HTTP_PORT))
def get_host(host: str) -> str:
"""Get the device IP address or hostname."""
try:
ip_object = ip_address(host)
except ValueError:
# host contains hostname
return host
if isinstance(ip_object, IPv6Address):
return f"[{host}]"
return host
@callback
def async_remove_shelly_rpc_entities(
hass: HomeAssistant, domain: str, mac: str, keys: list[str]

View File

@@ -218,7 +218,9 @@ class SmhiWeather(WeatherEntity):
data.append(
{
ATTR_FORECAST_TIME: forecast.valid_time.isoformat(),
ATTR_FORECAST_TIME: forecast.valid_time.replace(
tzinfo=dt_util.UTC
).isoformat(),
ATTR_FORECAST_NATIVE_TEMP: forecast.temperature_max,
ATTR_FORECAST_NATIVE_TEMP_LOW: forecast.temperature_min,
ATTR_FORECAST_NATIVE_PRECIPITATION: forecast.total_precipitation,

View File

@@ -9,7 +9,7 @@ from typing import Any
from starline import StarlineApi, StarlineDevice
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util import dt as dt_util
@@ -65,9 +65,9 @@ class StarlineAccount:
)
self._api.set_slnet_token(slnet_token)
self._api.set_user_id(user_id)
self._hass.config_entries.async_update_entry(
self._config_entry,
data={
self._hass.add_job(
self._save_slnet_token,
{
**self._config_entry.data,
DATA_SLNET_TOKEN: slnet_token,
DATA_EXPIRES: slnet_token_expires,
@@ -77,6 +77,13 @@ class StarlineAccount:
except Exception as err: # noqa: BLE001
_LOGGER.error("Error updating SLNet token: %s", err)
@callback
def _save_slnet_token(self, data) -> None:
self._hass.config_entries.async_update_entry(
self._config_entry,
data=data,
)
def _update_data(self):
"""Update StarLine data."""
self._check_slnet_token(self._update_interval)

View File

@@ -48,6 +48,14 @@ class StreamWorkerError(Exception):
"""An exception thrown while processing a stream."""
def redact_av_error_string(err: av.AVError) -> str:
"""Return an error string with credentials redacted from the url."""
parts = [str(err.type), err.strerror]
if err.filename is not None:
parts.append(redact_credentials(err.filename))
return ", ".join(parts)
class StreamEndedError(StreamWorkerError):
"""Raised when the stream is complete, exposed for facilitating testing."""
@@ -517,8 +525,7 @@ def stream_worker(
container = av.open(source, options=pyav_options, timeout=SOURCE_TIMEOUT)
except av.AVError as err:
raise StreamWorkerError(
f"Error opening stream ({err.type}, {err.strerror})"
f" {redact_credentials(str(source))}"
f"Error opening stream ({redact_av_error_string(err)})"
) from err
try:
video_stream = container.streams.video[0]
@@ -593,7 +600,7 @@ def stream_worker(
except av.AVError as ex:
container.close()
raise StreamWorkerError(
f"Error demuxing stream while finding first packet: {ex!s}"
f"Error demuxing stream while finding first packet ({redact_av_error_string(ex)})"
) from ex
muxer = StreamMuxer(
@@ -618,7 +625,9 @@ def stream_worker(
except StopIteration as ex:
raise StreamEndedError("Stream ended; no additional packets") from ex
except av.AVError as ex:
raise StreamWorkerError(f"Error demuxing stream: {ex!s}") from ex
raise StreamWorkerError(
f"Error demuxing stream ({redact_av_error_string(ex)})"
) from ex
muxer.mux_packet(packet)

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/sunweg/",
"iot_class": "cloud_polling",
"loggers": ["sunweg"],
"requirements": ["sunweg==3.0.1"]
"requirements": ["sunweg==3.0.2"]
}

View File

@@ -71,7 +71,9 @@ class SuplaCoverEntity(SuplaEntity, CoverEntity):
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
await self.async_action("REVEAL", percentage=kwargs.get(ATTR_POSITION))
await self.async_action(
"REVEAL_PARTIALLY", percentage=kwargs.get(ATTR_POSITION)
)
@property
def is_closed(self) -> bool | None:

View File

@@ -39,5 +39,5 @@
"documentation": "https://www.home-assistant.io/integrations/switchbot",
"iot_class": "local_push",
"loggers": ["switchbot"],
"requirements": ["PySwitchbot==0.48.0"]
"requirements": ["PySwitchbot==0.48.1"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/systemmonitor",
"iot_class": "local_push",
"loggers": ["psutil"],
"requirements": ["psutil-home-assistant==0.0.1", "psutil==5.9.8"]
"requirements": ["psutil-home-assistant==0.0.1", "psutil==6.0.0"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["tailscale==0.6.0"]
"requirements": ["tailscale==0.6.1"]
}

View File

@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["pytedee_async"],
"quality_scale": "platinum",
"requirements": ["pytedee-async==0.2.17"]
"requirements": ["pytedee-async==0.2.20"]
}

View File

@@ -42,6 +42,7 @@ class TessieBaseEntity(
self.key = key
self._attr_translation_key = key
super().__init__(coordinator)
self._async_update_attrs()
@property
def _value(self) -> Any:
@@ -132,7 +133,6 @@ class TessieEnergyEntity(TessieBaseEntity):
self._attr_device_info = data.device
super().__init__(coordinator, key)
self._async_update_attrs()
class TessieWallConnectorEntity(TessieBaseEntity):

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
from kasa import (
@@ -52,6 +53,8 @@ from .const import (
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
STEP_AUTH_DATA_SCHEMA = vol.Schema(
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
)
@@ -88,15 +91,10 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
)
@callback
def _update_config_if_entry_in_setup_error(
def _get_config_updates(
self, entry: ConfigEntry, host: str, config: dict
) -> ConfigFlowResult | None:
"""If discovery encounters a device that is in SETUP_ERROR or SETUP_RETRY update the device config."""
if entry.state not in (
ConfigEntryState.SETUP_ERROR,
ConfigEntryState.SETUP_RETRY,
):
return None
) -> dict | None:
"""Return updates if the host or device config has changed."""
entry_data = entry.data
entry_config_dict = entry_data.get(CONF_DEVICE_CONFIG)
if entry_config_dict == config and entry_data[CONF_HOST] == host:
@@ -110,11 +108,31 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
!= config.get(CONF_CONNECTION_TYPE)
):
updates.pop(CONF_CREDENTIALS_HASH, None)
return self.async_update_reload_and_abort(
entry,
data=updates,
reason="already_configured",
)
_LOGGER.debug(
"Connection type changed for %s from %s to: %s",
host,
entry_config_dict.get(CONF_CONNECTION_TYPE),
config.get(CONF_CONNECTION_TYPE),
)
return updates
@callback
def _update_config_if_entry_in_setup_error(
self, entry: ConfigEntry, host: str, config: dict
) -> ConfigFlowResult | None:
"""If discovery encounters a device that is in SETUP_ERROR or SETUP_RETRY update the device config."""
if entry.state not in (
ConfigEntryState.SETUP_ERROR,
ConfigEntryState.SETUP_RETRY,
):
return None
if updates := self._get_config_updates(entry, host, config):
return self.async_update_reload_and_abort(
entry,
data=updates,
reason="already_configured",
)
return None
async def _async_handle_discovery(
self, host: str, formatted_mac: str, config: dict | None = None
@@ -454,7 +472,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
password = user_input[CONF_PASSWORD]
credentials = Credentials(username, password)
try:
await self._async_try_discover_and_update(
device = await self._async_try_discover_and_update(
host,
credentials=credentials,
raise_on_progress=True,
@@ -467,6 +485,11 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
placeholders["error"] = str(ex)
else:
await set_credentials(self.hass, username, password)
config = device.config.to_dict(exclude_credentials=True)
if updates := self._get_config_updates(reauth_entry, host, config):
self.hass.config_entries.async_update_entry(
reauth_entry, data=updates
)
self.hass.async_create_task(
self._async_reload_requires_auth_entries(), eager_start=False
)

View File

@@ -382,16 +382,21 @@ class TPLinkLightEffectEntity(TPLinkLightEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
brightness, transition = self._async_extract_brightness_transition(**kwargs)
if ATTR_EFFECT in kwargs:
if (
(effect := kwargs.get(ATTR_EFFECT))
# Effect is unlikely to be LIGHT_EFFECTS_OFF but check for it anyway
and effect not in {LightEffect.LIGHT_EFFECTS_OFF, EFFECT_OFF}
and effect in self._effect_module.effect_list
):
await self._effect_module.set_effect(
kwargs[ATTR_EFFECT], brightness=brightness, transition=transition
)
elif ATTR_COLOR_TEMP_KELVIN in kwargs:
if self.effect:
if self.effect and self.effect != EFFECT_OFF:
# If there is an effect in progress
# we have to clear the effect
# before we can set a color temp
await self._light_module.set_hsv(0, 0, brightness)
await self._effect_module.set_effect(LightEffect.LIGHT_EFFECTS_OFF)
await self._async_set_color_temp(
kwargs[ATTR_COLOR_TEMP_KELVIN], brightness, transition
)

View File

@@ -181,7 +181,7 @@
"macaddress": "1C61B4*"
},
{
"hostname": "l5*",
"hostname": "l[59]*",
"macaddress": "5CE931*"
},
{
@@ -189,9 +189,13 @@
"macaddress": "3C52A1*"
},
{
"hostname": "l5*",
"hostname": "l[59]*",
"macaddress": "5C628B*"
},
{
"hostname": "l[59]*",
"macaddress": "14EBB6*"
},
{
"hostname": "tp*",
"macaddress": "5C628B*"
@@ -297,5 +301,5 @@
"iot_class": "local_polling",
"loggers": ["kasa"],
"quality_scale": "platinum",
"requirements": ["python-kasa[speedups]==0.7.0.2"]
"requirements": ["python-kasa[speedups]==0.7.0.5"]
}

View File

@@ -8,7 +8,7 @@ from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
import secrets
from typing import Any
from typing import TYPE_CHECKING, Any
import aiounifi
from aiounifi.interfaces.api_handlers import ItemEvent
@@ -44,6 +44,17 @@ from .entity import (
async_wlan_device_info_fn,
)
if TYPE_CHECKING:
from .hub import UnifiHub
@callback
def async_port_power_cycle_available_fn(hub: UnifiHub, obj_id: str) -> bool:
"""Check if port allows power cycle action."""
if not async_device_available_fn(hub, obj_id):
return False
return bool(hub.api.ports[obj_id].poe_enable)
async def async_restart_device_control_fn(
api: aiounifi.Controller, obj_id: str
@@ -96,7 +107,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiButtonEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
device_class=ButtonDeviceClass.RESTART,
api_handler_fn=lambda api: api.ports,
available_fn=async_device_available_fn,
available_fn=async_port_power_cycle_available_fn,
control_fn=async_power_cycle_port_control_fn,
device_info_fn=async_device_device_info_fn,
name_fn=lambda port: f"{port.name} Power Cycle",

View File

@@ -164,13 +164,12 @@ class UnifiFlowHandler(ConfigFlow, domain=UNIFI_DOMAIN):
config_entry = self.reauth_config_entry
abort_reason = "reauth_successful"
if (
config_entry is not None
and config_entry.state is not ConfigEntryState.NOT_LOADED
):
hub = config_entry.runtime_data
if hub and hub.available:
if config_entry:
if (
config_entry.state is ConfigEntryState.LOADED
and (hub := config_entry.runtime_data)
and hub.available
):
return self.async_abort(reason="already_configured")
return self.async_update_reload_and_abort(

View File

@@ -284,7 +284,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
name="Tracking: person",
icon="mdi:walk",
entity_category=EntityCategory.DIAGNOSTIC,
ufp_required_field="is_ptz",
ufp_required_field="feature_flags.is_ptz",
ufp_value="is_person_tracking_enabled",
ufp_perm=PermRequired.NO_WRITE,
),

View File

@@ -189,7 +189,6 @@ class BaseProtectEntity(Entity):
self._async_get_ufp_enabled = description.get_ufp_enabled
self._async_set_device_info()
self._async_update_device_from_protect(device)
self._state_getters = tuple(
partial(attrgetter(attr), self) for attr in self._state_attrs
)
@@ -264,6 +263,7 @@ class BaseProtectEntity(Entity):
self.async_on_remove(
self.data.async_subscribe(self.device.mac, self._async_updated_event)
)
self._async_update_device_from_protect(self.device)
class ProtectDeviceEntity(BaseProtectEntity):

View File

@@ -319,7 +319,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
name="Tracking: person",
icon="mdi:walk",
entity_category=EntityCategory.CONFIG,
ufp_required_field="is_ptz",
ufp_required_field="feature_flags.is_ptz",
ufp_value="is_person_tracking_enabled",
ufp_set_method="set_person_track",
ufp_perm=PermRequired.WRITE,

View File

@@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
file = config_entry.data[CONF_FILE_PATH]
upb = upb_lib.UpbPim({"url": url, "UPStartExportFile": file})
upb.connect()
await upb.async_connect()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][config_entry.entry_id] = {"upb": upb}

View File

@@ -39,12 +39,13 @@ async def _validate_input(data):
url = _make_url_from_data(data)
upb = upb_lib.UpbPim({"url": url, "UPStartExportFile": file_path})
await upb.async_connect(_connected_callback)
if not upb.config_ok:
_LOGGER.error("Missing or invalid UPB file: %s", file_path)
raise InvalidUpbFile
upb.connect(_connected_callback)
with suppress(TimeoutError):
async with asyncio.timeout(VALIDATE_TIMEOUT):
await connected_event.wait()

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/upb",
"iot_class": "local_push",
"loggers": ["upb_lib"],
"requirements": ["upb-lib==0.5.7"]
"requirements": ["upb-lib==0.5.8"]
}

View File

@@ -66,12 +66,16 @@ class VelbusCover(VelbusEntity, CoverEntity):
@property
def is_opening(self) -> bool:
"""Return if the cover is opening."""
return self._channel.is_opening()
if opening := self._channel.is_opening():
self._assumed_closed = False
return opening
@property
def is_closing(self) -> bool:
"""Return if the cover is closing."""
return self._channel.is_closing()
if closing := self._channel.is_closing():
self._assumed_closed = True
return closing
@property
def current_cover_position(self) -> int | None:
@@ -89,13 +93,11 @@ class VelbusCover(VelbusEntity, CoverEntity):
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
await self._channel.open()
self._assumed_closed = False
@api_call
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
await self._channel.close()
self._assumed_closed = True
@api_call
async def async_stop_cover(self, **kwargs: Any) -> None:

View File

@@ -13,7 +13,7 @@
"velbus-packet",
"velbus-protocol"
],
"requirements": ["velbus-aio==2024.5.1"],
"requirements": ["velbus-aio==2024.7.5"],
"usb": [
{
"vid": "10CF",

View File

@@ -7,7 +7,7 @@
"iot_class": "local_push",
"loggers": ["aiowebostv"],
"quality_scale": "platinum",
"requirements": ["aiowebostv==0.4.1"],
"requirements": ["aiowebostv==0.4.2"],
"ssdp": [
{
"st": "urn:lge-com:service:webos-second-screen:1"

View File

@@ -239,7 +239,8 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity):
self._attr_assumed_state = True
if (
self._client.media_state is not None
self._client.is_on
and self._client.media_state is not None
and self._client.media_state.get("foregroundAppInfo") is not None
):
self._attr_assumed_state = False

View File

@@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"quality_scale": "internal",
"requirements": ["holidays==0.51"]
"requirements": ["holidays==0.53"]
}

View File

@@ -24,5 +24,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/xiaomi_ble",
"iot_class": "local_push",
"requirements": ["xiaomi-ble==0.30.0"]
"requirements": ["xiaomi-ble==0.30.2"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/youless",
"iot_class": "local_polling",
"loggers": ["youless_api"],
"requirements": ["youless-api==2.1.0"]
"requirements": ["youless-api==2.1.2"]
}

View File

@@ -24,7 +24,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 7
PATCH_VERSION: Final = "0"
PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)

View File

@@ -352,6 +352,7 @@ FLOWS = {
"motionblinds_ble",
"motioneye",
"motionmount",
"mpd",
"mqtt",
"mullvad",
"mutesync",

View File

@@ -827,7 +827,7 @@ DHCP: Final[list[dict[str, str | bool]]] = [
},
{
"domain": "tplink",
"hostname": "l5*",
"hostname": "l[59]*",
"macaddress": "5CE931*",
},
{
@@ -837,9 +837,14 @@ DHCP: Final[list[dict[str, str | bool]]] = [
},
{
"domain": "tplink",
"hostname": "l5*",
"hostname": "l[59]*",
"macaddress": "5C628B*",
},
{
"domain": "tplink",
"hostname": "l[59]*",
"macaddress": "14EBB6*",
},
{
"domain": "tplink",
"hostname": "tp*",

View File

@@ -3814,7 +3814,7 @@
"mpd": {
"name": "Music Player Daemon (MPD)",
"integration_type": "hub",
"config_flow": false,
"config_flow": true,
"iot_class": "local_polling"
},
"mqtt": {

View File

@@ -483,7 +483,7 @@ def _get_exposed_entities(
if attributes := {
attr_name: str(attr_value)
if isinstance(attr_value, (Enum, Decimal))
if isinstance(attr_value, (Enum, Decimal, int))
else attr_value
for attr_name, attr_value in state.attributes.items()
if attr_name in interesting_attributes

View File

@@ -102,6 +102,23 @@ BLOCKED_CUSTOM_INTEGRATIONS: dict[str, BlockedIntegration] = {
"mydolphin_plus": BlockedIntegration(
AwesomeVersion("1.0.13"), "crashes Home Assistant"
),
# Added in 2024.7.2 because of
# https://github.com/gcobb321/icloud3/issues/349
# Note: Current version 3.0.5.2, the fixed version is a guesstimate,
# as no solution is available at time of writing.
"icloud3": BlockedIntegration(
AwesomeVersion("3.0.5.3"), "prevents recorder from working"
),
# Added in 2024.7.2 because of
# https://github.com/custom-components/places/issues/289
"places": BlockedIntegration(
AwesomeVersion("2.7.1"), "prevents recorder from working"
),
# Added in 2024.7.2 because of
# https://github.com/enkama/hass-variables/issues/120
"variable": BlockedIntegration(
AwesomeVersion("3.4.4"), "prevents recorder from working"
),
}
DATA_COMPONENTS: HassKey[dict[str, ModuleType | ComponentProtocol]] = HassKey(

Some files were not shown because too many files have changed in this diff Show More