Compare commits

..

102 Commits

Author SHA1 Message Date
Franck Nijhof
be39a3cee0 Merge pull request #68825 from home-assistant/rc 2022-03-29 15:30:51 +02:00
epenet
826423d8ed Cleanup package constraints (#68833)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2022-03-29 14:42:40 +02:00
epenet
99c0ef9ab8 Pin click to fix typer issue (#68808)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2022-03-29 10:21:29 +02:00
Paulus Schoutsen
4041dd61c5 Bumped version to 2022.3.8 2022-03-29 00:11:29 -07:00
Keilin Bickar
fe36ff5fa9 Update sense library to 0.10.4 (#68816) 2022-03-29 00:11:12 -07:00
J. Nick Koston
9acd1471a0 Fix ignoring elkm1 discovery (#68750) 2022-03-29 00:11:11 -07:00
Martin Hjelmare
d1a6eb55b1 Increase zwave_js add-on start attempts before timeout (#68736) 2022-03-29 00:11:10 -07:00
J. Nick Koston
112732c673 Add option to connect to elkm1 non-secure when secure is discovered (#68735)
Co-authored-by: Glenn Waters <glenn@watrs.ca>
2022-03-29 00:11:09 -07:00
J. Nick Koston
45638c78ae Ensure solaredge can still be setup with an ignored entry (#68688) 2022-03-29 00:11:08 -07:00
J. Nick Koston
d5e80ed2a5 Fix screenlogic to get the macaddress from discovery (#68687) 2022-03-29 00:11:07 -07:00
kevdliu
9b3c3232be Revert "Take Abode camera snapshot before fetching latest image" (#68626) 2022-03-29 00:11:06 -07:00
Paulus Schoutsen
feea5af3d0 Merge pull request #68592 from home-assistant/rc 2022-03-23 21:04:33 -07:00
Michael
be5d816fbe Bump py-synologydsm-api to 1.0.7 (#68584) 2022-03-23 18:08:01 -07:00
Paulus Schoutsen
911de94345 Bumped version to 2022.3.7 2022-03-23 15:47:05 -07:00
Marcel van der Veldt
53fa6c138a Bump aiohue to version 4.4.1 (#68579) 2022-03-23 15:46:53 -07:00
Marcel van der Veldt
85e6b3950c Bump aiohue to 4.4.0 (#68556) 2022-03-23 15:46:52 -07:00
J. Nick Koston
6fd4355314 Filter IPv6 addresses from AppleTV zeroconf discovery (#68530) 2022-03-23 15:46:14 -07:00
Marcel van der Veldt
050600375d Simplify Hue error handling a bit (#68529) 2022-03-23 15:46:13 -07:00
Paulus Schoutsen
1114877062 Hue handle HTTP errors (#68396) 2022-03-23 15:46:12 -07:00
jjlawren
454cb44ee8 Add cooldown timer before Sonos resubscriptions (#68521) 2022-03-23 15:45:21 -07:00
Keilin Bickar
9636435ff2 Add support for general API exception in Sense integration (#68517) 2022-03-23 15:45:20 -07:00
Erik Montnemery
f85781dc51 Fix targeting all or none entities in service calls (#68513)
* Fix targeting all or none entities in service calls

* Add test
2022-03-23 15:45:10 -07:00
Paulus Schoutsen
49edaf2f68 Merge pull request #68493 from home-assistant/rc 2022-03-21 21:57:00 -07:00
Paulus Schoutsen
2be9798fb8 Bumped version to 2022.3.6 2022-03-21 20:42:13 -07:00
J. Nick Koston
3bf0a64e21 Fix tplink color temp conversion (#68484) 2022-03-21 20:42:09 -07:00
Paulus Schoutsen
23e9aa6ad2 Handle Hue discovery errors (#68392) 2022-03-21 20:42:08 -07:00
J. Nick Koston
a8e1f57058 Filter IPv6 addreses from enphase_envoy discovery (#68362) 2022-03-21 20:42:07 -07:00
Marcel van der Veldt
caee432901 Hue integration: update errors that should be supressed (#68337) 2022-03-21 20:42:07 -07:00
epenet
df5c09e483 Bump renault-api to 0.1.10 (#68260)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2022-03-21 20:42:06 -07:00
Franck Nijhof
38eb007f63 Update opensensemap-api to 0.2.0 (#68193) 2022-03-21 20:41:31 -07:00
Marc Mueller
7dd9bfa92f Fix point by adding authlib constraint (#68176)
* Fix point by pinning authlib

* Use constraint
2022-03-21 20:41:31 -07:00
Marc Mueller
54b7f13a54 Add missing await [velbus] (#68153) 2022-03-21 20:41:30 -07:00
Michael
774f2b9b82 Respect disable_new_entities for new device_tracker entities (#68148) 2022-03-21 20:41:29 -07:00
Antonio Larrosa
bc14385317 Fix finding matrix room that is already joined (#67967)
After some debugging, it seems room.canonical_alias contains the
room alias that matches the room_id_or_alias value but is not
contained in room.aliases (which is empty). As a result, the
matrix component thought the room wasn't alread joined, joins
again, and this replaces the previous room which had the listener.
This resulted in the component callback not being called for new
messages in the room.

This fixes #66372
2022-03-21 20:41:28 -07:00
Numa Perez
9352ed1286 Fix lyric climate (#67018)
* Fixed the issues related to auto mode

I was having the same issues as described in #63403, specifically, the error stating that Mode 7 is not valid, only Heat, Cool, Off when trying to do anything while the thermostat is set to Auto. This error originates with the way the Lyric API handles the modes. Basically, when one queries the changeableValues dict, you get a mode=Auto, as well as a heatCoolMode, which is set to either Heat, Cool, Off. Per the documentation, heatCoolMode contains the "heat cool mode when system switch is in Auto mode". It would make sense that when changing the thermostat settings, mode=Auto should be valid, but it's not. The way the API understands that the mode should be set to Auto when changing the thermostat settings is by setting the autoChangeoverActive variable to true, not the mode itself. This require changes in the async_set_hvac_mode, async_set_temperature, and async_set_preset_mode functions. Related to this issue, I got rid of the references to hasDualSetpointStatus, as it seems that it always remains false in the API, even when the mode is set to auto, so again, the key variable for this is autoChangeoverActive.

While I was working on this I also noticed another issue. The support flag SUPPORT_TARGET_TEMPERATURE_RANGE had not been included, which did not allow for the temperature range to be available, thus invalidating the target_temperature_low and target_temperature_high functions. I added this flag and sorted out which set point (heat vs cool) should be called for each of them so things work as expected in Lovelace. I have tested all of these functionalities and they all work great on my end, so I thought I'd share.

* Update climate.py

* Update climate.py

Fixed two additional issues: 1) When the system is turned off from Auto, the heatCoolMode variable becomes 'Off', so when you try to restart the system back to Auto, nothing happens. 2) I now prevent the async_set_temperature function from being called with a new set point when the system is Off.
All changes tested and functional.

* Update climate.py

* Update climate.py

Return SUPPORT_PRESET_MODE flag only for LCC models (i.e. they have the "thermostatSetpointStatus" variable defined). TCC models do not support this feature

* Update climate.py

After playing with the official Honeywell API, I realized it doesn't like to received commands with missing data, i.e., it always wants to get a mode, coolSetpoint, heatSetpoint, and autoChangeoverActive variables. This was causing some random issues with changing modes, especially from coming from off, so I modified the async_set_temperature, and async_set_hvac_mode fuctions to always send all pertinent variables.

* Update climate.py

* Update climate.py

* Update climate.py

* Update climate.py

* Clean code and test everything

Alright, sorry for the multiple commits, fixing this properly took a fair bit of testing. I went ahead and cleaned up the code and made the following big picture changes: 
1) The integration now supports the Auto mode appropriately, to include the temperature range. 
2) There's a bug that actually manifests when using the native app. When the system is 'Off' and you try to turn it on to 'Auto', it will turn on briefly but will go back to 'Off' after a few seconds. When checking the web api, this appears to be related to the fact that the heatCoolMode variable seems to continue to store 'Off', even if the mode accurately displays 'Auto', and the autoChangeoverActive=True. So to overcome that inherent limitation, when the system is 'Off' and the user turns it to 'Auto', I first turn it to Heat, wait 3 seconds, and then turn it to 'Auto', which seems to work well.

* Update climate.py

* Fixed errors

* Fixed comments that were resulting in error.

* Update climate.py

* Update homeassistant/components/lyric/climate.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/lyric/climate.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update climate.py

I removed a blank line in 268 and another one at the end of the document. I also fixed the outdents of await commands after the _LOGGER.error calls, not sure what else may be driving the flake8 and black errors. Any guidance is much appreciated @MartinHjelmare

* Update climate.py

* Update climate.py

corrected some indents that I think were the culprit of the flake8 errors

* Update climate.py

I used VS Code to fix locate the flake8 errors. I ran black on it, so I'm hoping that will fix the last lingering black error.

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-03-21 20:41:28 -07:00
epenet
f7fd781a27 Fix TypeError in SamsungTV (#68235)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2022-03-18 09:26:19 +01:00
Paulus Schoutsen
b1153720c0 Merge pull request #68159 from home-assistant/rc 2022-03-15 00:11:48 -07:00
Franck Nijhof
27d275e6f7 Fix Efergy tests (#68086) 2022-03-14 23:22:59 -07:00
Paulus Schoutsen
1191c095f8 Bumped version to 2022.3.5 2022-03-14 22:46:08 -07:00
Jan Bouwhuis
b86d115764 Fix MQTT false positive deprecation warnings (#68117) 2022-03-14 22:45:53 -07:00
Frank
479a230da7 Update home_connect to 0.7.0 (#68089) 2022-03-14 22:45:52 -07:00
J. Nick Koston
7aecd69e3b Bump pyisy to 3.0.5 (#68069)
* Bump pyisy to 3.0.4

- Fixes #66003

- Changelog: https://github.com/automicus/PyISY/compare/v3.0.1...v3.0.4

* again
2022-03-14 22:45:52 -07:00
Sean Vig
69587dd50a Bump amcrest version to 1.9.7 (#68055) 2022-03-14 22:45:51 -07:00
Christopher Thornton
6d8bd6af4d Default somfy_mylink shade's _attr_is_closed to None (#68053)
The base `Cover` entity requires an explicit value for
`_attr_is_closed`.

Since the `SomfyShade` is an assumed state, we don't know
by default whether the shade is open or not, so we need to
explicitly return `None` for `_attr_is_closed`
2022-03-14 22:45:50 -07:00
Shay Levy
31b19e09b5 Fix Shelly EM/3EM invalid energy value after reboot (#68052) 2022-03-14 22:45:49 -07:00
Sean Vig
a42ba9e10a Fix turning amcrest camera on and off (#68050) 2022-03-14 22:45:49 -07:00
J. Nick Koston
a285478cf8 Filter IPv6 addresses from doorbird discovery (#68031) 2022-03-14 22:45:48 -07:00
Zack Barett
c95d55e6d6 20220301.2 (#68130) 2022-03-14 10:07:58 -07:00
epenet
c0860931b3 Fix WebSocketTimeoutException in SamsungTV (#68114)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2022-03-14 09:32:14 -07:00
Paulus Schoutsen
898af3e04c Merge pull request #68001 from home-assistant/rc 2022-03-11 17:11:03 -08:00
Diogo Gomes
3de341099f Bump pymediaroom (#68016) 2022-03-11 15:45:40 -08:00
Paulus Schoutsen
7fb76c68bb Bumped version to 2022.3.4 2022-03-11 09:25:55 -08:00
Guido Schmitz
7de5e070fb Bump pysabnzbd to 1.1.1 (#67971) 2022-03-11 09:24:50 -08:00
Tom Harris
1bfb01e0d1 Rollback pyinsteon (#67956) 2022-03-11 09:24:50 -08:00
Erik Montnemery
ca664ab5a5 Correct local import of paho-mqtt (#67944)
* Correct local import of paho-mqtt

* Remove MqttClientSetup.mqtt class attribute

* Remove reference to MqttClientSetup.mqtt
2022-03-11 09:24:49 -08:00
Franck Nijhof
5a39e63d25 Update radios to 0.1.1 (#67902) 2022-03-11 09:24:48 -08:00
Joakim Plate
c608cafebd Make sure blueprint cache is flushed on script reload (#67899) 2022-03-11 09:24:47 -08:00
Shay Levy
07e70c81b0 Fix shelly duo scene restore (#67871) 2022-03-11 09:24:46 -08:00
J. Nick Koston
cad397d6a7 Add missing callback decorator to sun (#67840) 2022-03-11 09:24:45 -08:00
Raman Gupta
c22af2c82a Bump zwave-js-server-python to 0.35.2 (#67839) 2022-03-11 09:24:45 -08:00
Richard de Boer
f5b6d93706 Support playing local "file" media on Kodi (#67832)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-03-11 09:24:44 -08:00
cheng2wei
28b3edf6b2 Fix discord embed class initialization (#67831) 2022-03-11 09:24:43 -08:00
Paulus Schoutsen
737c502e94 Merge pull request #67838 from home-assistant/rc 2022-03-07 21:51:30 -08:00
Paulus Schoutsen
a1abcbc7eb Bumped version to 2022.3.3 2022-03-07 20:45:49 -08:00
J. Nick Koston
b09ab2dafb Prevent scene from restoring unavailable states (#67836) 2022-03-07 20:45:44 -08:00
Teemu R
4e6fc3615b Bump python-miio version to 0.5.11 (#67824) 2022-03-07 20:45:43 -08:00
Bram Kragten
580c998552 Update frontend to 20220301.1 (#67812) 2022-03-07 20:45:25 -08:00
Franck Nijhof
97ba17d1ec Catch Elgato connection errors (#67799) 2022-03-07 20:44:09 -08:00
J. Nick Koston
8d7cdceb75 Handle fan_modes being set to None in homekit (#67790) 2022-03-07 20:44:08 -08:00
Simone Chemelli
dfa1c3abb3 Fix profile name update for Shelly Valve (#67778) 2022-03-07 20:44:08 -08:00
Simone Chemelli
c807c57a9b Fix internet access switch for old discovery (#67777) 2022-03-07 20:44:07 -08:00
J. Nick Koston
f4ec7e0902 Prevent polling from recreating an entity after removal (#67750) 2022-03-07 20:44:06 -08:00
G Johansson
814c96834e Fix temperature stepping in Sensibo (#67737)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-03-07 20:44:05 -08:00
muppet3000
87492e6b3e Fix timezone for growatt lastdataupdate (#67684)
* Added timezone for growatt lastdataupdate (#67646)

* Growatt lastdataupdate set to local timezone
2022-03-07 20:44:05 -08:00
Jan Bouwhuis
4aaafb0a99 Fix false positive MQTT climate deprecation warnings for defaults (#67661)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-03-07 20:44:04 -08:00
Paulus Schoutsen
2aecdd3d6d Merge pull request #67730 from home-assistant/rc 2022-03-06 12:11:16 -08:00
Jc2k
76336df91a Fix regression with homekit_controller + Aqara motion/vibration sensors (#67740) 2022-03-06 08:45:56 -08:00
Paulus Schoutsen
88e0380aa2 Bumped version to 2022.3.2 2022-03-06 00:07:45 -08:00
Avi Miller
10a2c97cab Update aiolifx dependency to resolve log flood (#67721) 2022-03-06 00:07:41 -08:00
J. Nick Koston
92c3c08a10 Add missing disconnect in elkm1 config flow validation (#67716) 2022-03-06 00:07:40 -08:00
J. Nick Koston
4f8b69d985 Ensure elkm1 can be manually configured when discovered instance is not used (#67712) 2022-03-06 00:07:39 -08:00
Martin Hjelmare
f5aaf44e50 Bump pydroid-ipcam to 1.3.1 (#67655)
* Bump pydroid-ipcam to 1.3.1

* Remove loop and set ssl to False
2022-03-06 00:07:39 -08:00
Erik Montnemery
f3c85b3459 Fix reload of media player groups (#67653) 2022-03-06 00:07:38 -08:00
Franck Nijhof
d7348718e0 Fix Fan template loosing percentage/preset (#67648)
Co-authored-by: J. Nick Koston <nick@koston.org>
2022-03-06 00:07:37 -08:00
Simone Chemelli
2a6d5ea7bd Improve logging for Fritz switches creation (#67640) 2022-03-06 00:07:37 -08:00
Simone Chemelli
5ae83e3c40 Allign logic for Fritz sensors and binary_sensors (#67623) 2022-03-06 00:07:36 -08:00
G Johansson
5657a9e6bd Fix sql false warning (#67614) 2022-03-06 00:07:35 -08:00
J. Nick Koston
b290e62170 Handle elkm1 login case with username and insecure login (#67602) 2022-03-06 00:07:35 -08:00
epenet
679ddbd1be Downgrade Renault warning (#67601)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2022-03-06 00:07:34 -08:00
Teemu R
b54652a849 Remove use of deprecated xiaomi_miio classes (#67590) 2022-03-06 00:07:33 -08:00
Joakim Plate
24013ad94c rfxtrx: bump to 0.28 (#67530) 2022-03-06 00:07:32 -08:00
Chris Talkington
9849b86a84 Suppress roku power off timeout errors (#67414) 2022-03-06 00:07:32 -08:00
Simone Chemelli
8bbf55c85d Add unique_id to Fritz diagnostics (#67384)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-03-06 00:07:31 -08:00
Paulus Schoutsen
0541c708da Merge pull request #67588 from home-assistant/rc 2022-03-03 18:49:44 -08:00
Paulus Schoutsen
ba40d62081 Bumped version to 2022.3.1 2022-03-03 15:53:54 -08:00
J. Nick Koston
73765a1f29 Add guards for HomeKit version/names that break apple watches (#67585) 2022-03-03 15:53:46 -08:00
muppet3000
b5b945ab4d Fix data type for growatt lastdataupdate (#67511) (#67582)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-03-03 15:53:46 -08:00
Emory Penney
d361643500 Bump pyobihai (#67571) 2022-03-03 15:53:45 -08:00
Paulus Schoutsen
eff7a12557 Highlight in logs it is a custom component when setup fails (#67559)
Co-authored-by: Joakim Sørensen <ludeeus@ludeeus.dev>
2022-03-03 15:53:44 -08:00
Jan Bouwhuis
63f8e9ee08 Fix MQTT config flow with advanced parameters (#67556)
* Fix MQTT config flow with advanced parameters

* Add test
2022-03-03 15:53:44 -08:00
Simone Chemelli
ee0bdaa2de Check if UPnP is enabled on Fritz device (#67512)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-03-03 15:48:24 -08:00
jjlawren
48d9e9a83c Bump soco to 0.26.4 (#67498) 2022-03-03 15:47:50 -08:00
132 changed files with 1757 additions and 472 deletions

View File

@@ -88,8 +88,6 @@ class AbodeCamera(AbodeDevice, Camera):
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Get a camera image."""
if not self.capture():
return None
self.refresh_image()
if self._response:

View File

@@ -52,6 +52,7 @@ from .const import (
DATA_AMCREST,
DEVICES,
DOMAIN,
RESOLUTION_LIST,
SERVICE_EVENT,
SERVICE_UPDATE,
)
@@ -76,8 +77,6 @@ RECHECK_INTERVAL = timedelta(minutes=1)
NOTIFICATION_ID = "amcrest_notification"
NOTIFICATION_TITLE = "Amcrest Camera Setup"
RESOLUTION_LIST = {"high": 0, "low": 1}
SCAN_INTERVAL = timedelta(seconds=10)
AUTHENTICATION_LIST = {"basic": "basic"}

View File

@@ -35,6 +35,7 @@ from .const import (
DATA_AMCREST,
DEVICES,
DOMAIN,
RESOLUTION_TO_STREAM,
SERVICE_UPDATE,
SNAPSHOT_TIMEOUT,
)
@@ -533,13 +534,14 @@ class AmcrestCam(Camera):
return
async def _async_get_video(self) -> bool:
stream = {0: "Main", 1: "Extra"}
return await self._api.async_is_video_enabled(
channel=0, stream=stream[self._resolution]
channel=0, stream=RESOLUTION_TO_STREAM[self._resolution]
)
async def _async_set_video(self, enable: bool) -> None:
await self._api.async_set_video_enabled(enable, channel=0)
await self._api.async_set_video_enabled(
enable, channel=0, stream=RESOLUTION_TO_STREAM[self._resolution]
)
async def _async_enable_video(self, enable: bool) -> None:
"""Enable or disable camera video stream."""
@@ -548,7 +550,7 @@ class AmcrestCam(Camera):
# recording on if video stream is being turned off.
if self.is_recording and not enable:
await self._async_enable_recording(False)
await self._async_change_setting(enable, "video", "is_streaming")
await self._async_change_setting(enable, "video", "_attr_is_streaming")
if self._control_light:
await self._async_change_light()
@@ -585,10 +587,14 @@ class AmcrestCam(Camera):
)
async def _async_get_audio(self) -> bool:
return await self._api.async_audio_enabled
return await self._api.async_is_audio_enabled(
channel=0, stream=RESOLUTION_TO_STREAM[self._resolution]
)
async def _async_set_audio(self, enable: bool) -> None:
await self._api.async_set_audio_enabled(enable)
await self._api.async_set_audio_enabled(
enable, channel=0, stream=RESOLUTION_TO_STREAM[self._resolution]
)
async def _async_enable_audio(self, enable: bool) -> None:
"""Enable or disable audio stream."""

View File

@@ -13,3 +13,6 @@ SNAPSHOT_TIMEOUT = 20
SERVICE_EVENT = "event"
SERVICE_UPDATE = "update"
RESOLUTION_LIST = {"high": 0, "low": 1}
RESOLUTION_TO_STREAM = {0: "Main", 1: "Extra"}

View File

@@ -2,7 +2,7 @@
"domain": "amcrest",
"name": "Amcrest",
"documentation": "https://www.home-assistant.io/integrations/amcrest",
"requirements": ["amcrest==1.9.4"],
"requirements": ["amcrest==1.9.7"],
"dependencies": ["ffmpeg"],
"codeowners": ["@flacjacket"],
"iot_class": "local_polling",

View File

@@ -204,13 +204,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
# Init ip webcam
cam = PyDroidIPCam(
hass.loop,
websession,
host,
cam_config[CONF_PORT],
username=username,
password=password,
timeout=cam_config[CONF_TIMEOUT],
ssl=False,
)
if switches is None:

View File

@@ -2,7 +2,7 @@
"domain": "android_ip_webcam",
"name": "Android IP Webcam",
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
"requirements": ["pydroid-ipcam==0.8"],
"requirements": ["pydroid-ipcam==1.3.1"],
"codeowners": [],
"iot_class": "local_polling"
}

View File

@@ -19,6 +19,7 @@ from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PIN
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.network import is_ipv6_address
from .const import CONF_CREDENTIALS, CONF_IDENTIFIERS, CONF_START_OFF, DOMAIN
@@ -166,6 +167,8 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) -> data_entry_flow.FlowResult:
"""Handle device found via zeroconf."""
host = discovery_info.host
if is_ipv6_address(host):
return self.async_abort(reason="ipv6_not_supported")
self._async_abort_entries_match({CONF_ADDRESS: host})
service_type = discovery_info.type[:-1] # Remove leading .
name = discovery_info.name.replace(f".{service_type}.", "")

View File

@@ -48,6 +48,7 @@
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"abort": {
"ipv6_not_supported": "IPv6 is not supported.",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"device_did_not_pair": "No attempt to finish pairing process was made from the device.",

View File

@@ -2,13 +2,12 @@
"config": {
"abort": {
"already_configured": "Device is already configured",
"already_configured_device": "Device is already configured",
"already_in_progress": "Configuration flow is already in progress",
"backoff": "Device does not accept pairing requests at this time (you might have entered an invalid PIN code too many times), try again later.",
"device_did_not_pair": "No attempt to finish pairing process was made from the device.",
"device_not_found": "Device was not found during discovery, please try adding it again.",
"inconsistent_device": "Expected protocols were not found during discovery. This normally indicates a problem with multicast DNS (Zeroconf). Please try adding the device again.",
"invalid_config": "The configuration for this device is incomplete. Please try adding it again.",
"ipv6_not_supported": "IPv6 is not supported.",
"no_devices_found": "No devices found on the network",
"reauth_successful": "Re-authentication was successful",
"setup_failed": "Failed to set up device.",
@@ -18,7 +17,6 @@
"already_configured": "Device is already configured",
"invalid_auth": "Invalid authentication",
"no_devices_found": "No devices found on the network",
"no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.",
"unknown": "Unexpected error"
},
"flow_title": "{name} ({type})",
@@ -72,6 +70,5 @@
"description": "Configure general device settings"
}
}
},
"title": "Apple TV"
}
}

View File

@@ -149,9 +149,19 @@ def _async_register_mac(
return
# Make sure entity has a config entry and was disabled by the
# default disable logic in the integration.
# default disable logic in the integration and new entities
# are allowed to be added.
if (
entity_entry.config_entry_id is None
or (
(
config_entry := hass.config_entries.async_get_entry(
entity_entry.config_entry_id
)
)
is not None
and config_entry.pref_disable_new_entities
)
or entity_entry.disabled_by != er.RegistryEntryDisabler.INTEGRATION
):
return

View File

@@ -20,9 +20,13 @@ _LOGGER = logging.getLogger(__name__)
ATTR_EMBED = "embed"
ATTR_EMBED_AUTHOR = "author"
ATTR_EMBED_COLOR = "color"
ATTR_EMBED_DESCRIPTION = "description"
ATTR_EMBED_FIELDS = "fields"
ATTR_EMBED_FOOTER = "footer"
ATTR_EMBED_TITLE = "title"
ATTR_EMBED_THUMBNAIL = "thumbnail"
ATTR_EMBED_URL = "url"
ATTR_IMAGES = "images"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_TOKEN): cv.string})
@@ -64,10 +68,16 @@ class DiscordNotificationService(BaseNotificationService):
embeds: list[nextcord.Embed] = []
if ATTR_EMBED in data:
embedding = data[ATTR_EMBED]
title = embedding.get(ATTR_EMBED_TITLE) or nextcord.Embed.Empty
description = embedding.get(ATTR_EMBED_DESCRIPTION) or nextcord.Embed.Empty
color = embedding.get(ATTR_EMBED_COLOR) or nextcord.Embed.Empty
url = embedding.get(ATTR_EMBED_URL) or nextcord.Embed.Empty
fields = embedding.get(ATTR_EMBED_FIELDS) or []
if embedding:
embed = nextcord.Embed(**embedding)
embed = nextcord.Embed(
title=title, description=description, color=color, url=url
)
for field in fields:
embed.add_field(**field)
if ATTR_EMBED_FOOTER in embedding:

View File

@@ -12,7 +12,7 @@ from homeassistant.components import zeroconf
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.util.network import is_link_local
from homeassistant.util.network import is_ipv4_address, is_link_local
from .const import CONF_EVENTS, DOMAIN, DOORBIRD_OUI
from .util import get_mac_address_from_doorstation_info
@@ -103,6 +103,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="not_doorbird_device")
if is_link_local(ip_address(host)):
return self.async_abort(reason="link_local_address")
if not is_ipv4_address(host):
return self.async_abort(reason="not_ipv4_address")
await self.async_set_unique_id(macaddress)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})

View File

@@ -3,7 +3,8 @@
"abort": {
"already_configured": "Device is already configured",
"link_local_address": "Link local addresses are not supported",
"not_doorbird_device": "This device is not a DoorBird"
"not_doorbird_device": "This device is not a DoorBird",
"not_ipv4_address": "Only IPv4 addresess are supported"
},
"error": {
"cannot_connect": "Failed to connect",

View File

@@ -1,13 +1,13 @@
"""Support for Elgato Lights."""
from typing import NamedTuple
from elgato import Elgato, Info, State
from elgato import Elgato, ElgatoConnectionError, Info, State
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
@@ -31,12 +31,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
session=session,
)
async def _async_update_data() -> State:
"""Fetch Elgato data."""
try:
return await elgato.state()
except ElgatoConnectionError as err:
raise UpdateFailed(err) from err
coordinator: DataUpdateCoordinator[State] = DataUpdateCoordinator(
hass,
LOGGER,
name=f"{DOMAIN}_{entry.data[CONF_HOST]}",
update_interval=SCAN_INTERVAL,
update_method=elgato.state,
update_method=_async_update_data,
)
await coordinator.async_config_entry_first_refresh()

View File

@@ -279,9 +279,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
keypad.add_callback(_element_changed)
try:
if not await async_wait_for_elk_to_sync(
elk, LOGIN_TIMEOUT, SYNC_TIMEOUT, bool(conf[CONF_USERNAME])
):
if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, SYNC_TIMEOUT):
return False
except asyncio.TimeoutError as exc:
raise ConfigEntryNotReady(f"Timed out connecting to {conf[CONF_HOST]}") from exc
@@ -334,7 +332,6 @@ async def async_wait_for_elk_to_sync(
elk: elkm1.Elk,
login_timeout: int,
sync_timeout: int,
password_auth: bool,
) -> bool:
"""Wait until the elk has finished sync. Can fail login or timeout."""
@@ -354,18 +351,23 @@ async def async_wait_for_elk_to_sync(
login_event.set()
sync_event.set()
def first_response(*args, **kwargs):
_LOGGER.debug("ElkM1 received first response (VN)")
login_event.set()
def sync_complete():
sync_event.set()
success = True
elk.add_handler("login", login_status)
# VN is the first command sent for panel, when we get
# it back we now we are logged in either with or without a password
elk.add_handler("VN", first_response)
elk.add_handler("sync_complete", sync_complete)
events = []
if password_auth:
events.append(("login", login_event, login_timeout))
events.append(("sync_complete", sync_event, sync_timeout))
for name, event, timeout in events:
for name, event, timeout in (
("login", login_event, login_timeout),
("sync_complete", sync_event, sync_timeout),
):
_LOGGER.debug("Waiting for %s event for %s seconds", name, timeout)
try:
async with async_timeout.timeout(timeout):

View File

@@ -37,7 +37,9 @@ from .discovery import (
CONF_DEVICE = "device"
NON_SECURE_PORT = 2101
SECURE_PORT = 2601
STANDARD_PORTS = {NON_SECURE_PORT, SECURE_PORT}
_LOGGER = logging.getLogger(__name__)
@@ -48,6 +50,7 @@ PROTOCOL_MAP = {
"serial": "serial://",
}
VALIDATE_TIMEOUT = 35
BASE_SCHEMA = {
@@ -60,6 +63,11 @@ ALL_PROTOCOLS = [*SECURE_PROTOCOLS, "non-secure", "serial"]
DEFAULT_SECURE_PROTOCOL = "secure"
DEFAULT_NON_SECURE_PROTOCOL = "non-secure"
PORT_PROTOCOL_MAP = {
NON_SECURE_PORT: DEFAULT_NON_SECURE_PROTOCOL,
SECURE_PORT: DEFAULT_SECURE_PROTOCOL,
}
async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str]:
"""Validate the user input allows us to connect.
@@ -81,10 +89,11 @@ async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str
)
elk.connect()
if not await async_wait_for_elk_to_sync(
elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT, bool(userid)
):
raise InvalidAuth
try:
if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT):
raise InvalidAuth
finally:
elk.disconnect()
short_mac = _short_mac(mac) if mac else None
if prefix and prefix != short_mac:
@@ -96,6 +105,13 @@ async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str
return {"title": device_name, CONF_HOST: url, CONF_PREFIX: slugify(prefix)}
def _address_from_discovery(device: ElkSystem):
"""Append the port only if its non-standard."""
if device.port in STANDARD_PORTS:
return device.ip_address
return f"{device.ip_address}:{device.port}"
def _make_url_from_data(data):
if host := data.get(CONF_HOST):
return host
@@ -108,7 +124,7 @@ def _make_url_from_data(data):
def _placeholders_from_device(device: ElkSystem) -> dict[str, str]:
return {
"mac_address": _short_mac(device.mac_address),
"host": f"{device.ip_address}:{device.port}",
"host": _address_from_discovery(device),
}
@@ -165,6 +181,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
for progress in self._async_in_progress():
if progress.get("context", {}).get(CONF_HOST) == host:
return self.async_abort(reason="already_in_progress")
# Handled ignored case since _async_current_entries
# is called with include_ignore=False
self._abort_if_unique_id_configured()
if not device.port:
if discovered_device := await async_discover_device(self.hass, host):
self._discovered_device = discovered_device
@@ -227,7 +246,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try:
info = await validate_input(user_input, self.unique_id)
except asyncio.TimeoutError:
return {CONF_HOST: "cannot_connect"}, None
return {"base": "cannot_connect"}, None
except InvalidAuth:
return {CONF_PASSWORD: "invalid_auth"}, None
except Exception: # pylint: disable=broad-except
@@ -254,26 +273,26 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
device = self._discovered_device
assert device is not None
if user_input is not None:
user_input[CONF_ADDRESS] = f"{device.ip_address}:{device.port}"
user_input[CONF_ADDRESS] = _address_from_discovery(device)
if self._async_current_entries():
user_input[CONF_PREFIX] = _short_mac(device.mac_address)
else:
user_input[CONF_PREFIX] = ""
if device.port != SECURE_PORT:
user_input[CONF_PROTOCOL] = DEFAULT_NON_SECURE_PROTOCOL
errors, result = await self._async_create_or_error(user_input, False)
if not errors:
return result
base_schmea = BASE_SCHEMA.copy()
if device.port == SECURE_PORT:
base_schmea[
vol.Required(CONF_PROTOCOL, default=DEFAULT_SECURE_PROTOCOL)
] = vol.In(SECURE_PROTOCOLS)
default_proto = PORT_PROTOCOL_MAP.get(device.port, DEFAULT_SECURE_PROTOCOL)
return self.async_show_form(
step_id="discovered_connection",
data_schema=vol.Schema(base_schmea),
data_schema=vol.Schema(
{
**BASE_SCHEMA,
vol.Required(CONF_PROTOCOL, default=default_proto): vol.In(
ALL_PROTOCOLS
),
}
),
errors=errors,
description_placeholders=_placeholders_from_device(device),
)
@@ -287,9 +306,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if device := await async_discover_device(
self.hass, user_input[CONF_ADDRESS]
):
await self.async_set_unique_id(dr.format_mac(device.mac_address))
await self.async_set_unique_id(
dr.format_mac(device.mac_address), raise_on_progress=False
)
self._abort_if_unique_id_configured()
user_input[CONF_ADDRESS] = f"{device.ip_address}:{device.port}"
# Ignore the port from discovery since its always going to be
# 2601 if secure is turned on even though they may want insecure
user_input[CONF_ADDRESS] = device.ip_address
errors, result = await self._async_create_or_error(user_input, False)
if not errors:
return result
@@ -324,10 +347,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if is_ip_address(host) and (
device := await async_discover_device(self.hass, host)
):
await self.async_set_unique_id(dr.format_mac(device.mac_address))
await self.async_set_unique_id(
dr.format_mac(device.mac_address), raise_on_progress=False
)
self._abort_if_unique_id_configured()
return (await self._async_create_or_error(user_input, True))[1]
errors, result = await self._async_create_or_error(user_input, True)
if errors:
return self.async_abort(reason=list(errors.values())[0])
return result
def _url_already_configured(self, url):
"""See if we already have a elkm1 matching user input configured."""

View File

@@ -38,6 +38,8 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"already_configured": "An ElkM1 with this prefix is already configured",

View File

@@ -4,7 +4,9 @@
"address_already_configured": "An ElkM1 with this address is already configured",
"already_configured": "An ElkM1 with this prefix is already configured",
"already_in_progress": "Configuration flow is already in progress",
"cannot_connect": "Failed to connect"
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"error": {
"cannot_connect": "Failed to connect",
@@ -37,13 +39,7 @@
},
"user": {
"data": {
"address": "The IP address or domain or serial port if connecting via serial.",
"device": "Device",
"password": "Password",
"prefix": "A unique prefix (leave blank if you only have one ElkM1).",
"protocol": "Protocol",
"temperature_unit": "The temperature unit ElkM1 uses.",
"username": "Username"
"device": "Device"
},
"description": "Choose a discovered system or 'Manual Entry' if no devices have been discovered.",
"title": "Connect to Elk-M1 Control"

View File

@@ -2,7 +2,7 @@
"domain": "emulated_kasa",
"name": "Emulated Kasa",
"documentation": "https://www.home-assistant.io/integrations/emulated_kasa",
"requirements": ["sense_energy==0.10.2"],
"requirements": ["sense_energy==0.10.4"],
"codeowners": ["@kbickar"],
"quality_scale": "internal",
"iot_class": "local_push",

View File

@@ -16,6 +16,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.util.network import is_ipv4_address
from .const import DOMAIN
@@ -86,6 +87,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> FlowResult:
"""Handle a flow initialized by zeroconf discovery."""
if not is_ipv4_address(discovery_info.host):
return self.async_abort(reason="not_ipv4_address")
serial = discovery_info.properties["serialnum"]
await self.async_set_unique_id(serial)
self.ip_address = discovery_info.host

View File

@@ -2,7 +2,8 @@
"config": {
"abort": {
"already_configured": "Device is already configured",
"reauth_successful": "Re-authentication was successful"
"reauth_successful": "Re-authentication was successful",
"not_ipv4_address": "Only IPv4 addresess are supported"
},
"error": {
"cannot_connect": "Failed to connect",

View File

@@ -33,6 +33,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except FRITZ_EXCEPTIONS as ex:
raise ConfigEntryNotReady from ex
if (
"X_AVM-DE_UPnP1" in avm_wrapper.connection.services
and not (await avm_wrapper.async_get_upnp_configuration())["NewEnable"]
):
raise ConfigEntryAuthFailed("Missing UPnP configuration")
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = avm_wrapper

View File

@@ -1,6 +1,7 @@
"""AVM FRITZ!Box connectivity sensor."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
@@ -14,8 +15,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .common import AvmWrapper, FritzBoxBaseEntity
from .const import DOMAIN, MeshRoles
from .common import AvmWrapper, ConnectionInfo, FritzBoxBaseEntity
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -24,7 +25,7 @@ _LOGGER = logging.getLogger(__name__)
class FritzBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes Fritz sensor entity."""
exclude_mesh_role: MeshRoles = MeshRoles.SLAVE
is_suitable: Callable[[ConnectionInfo], bool] = lambda info: info.wan_enabled
SENSOR_TYPES: tuple[FritzBinarySensorEntityDescription, ...] = (
@@ -45,7 +46,7 @@ SENSOR_TYPES: tuple[FritzBinarySensorEntityDescription, ...] = (
name="Firmware Update",
device_class=BinarySensorDeviceClass.UPDATE,
entity_category=EntityCategory.DIAGNOSTIC,
exclude_mesh_role=MeshRoles.NONE,
is_suitable=lambda info: True,
),
)
@@ -57,10 +58,12 @@ async def async_setup_entry(
_LOGGER.debug("Setting up FRITZ!Box binary sensors")
avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id]
connection_info = await avm_wrapper.async_get_connection_info()
entities = [
FritzBoxBinarySensor(avm_wrapper, entry.title, description)
for description in SENSOR_TYPES
if (description.exclude_mesh_role != avm_wrapper.mesh_role)
if description.is_suitable(connection_info)
]
async_add_entities(entities, True)

View File

@@ -392,6 +392,8 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
)
self.mesh_role = MeshRoles.NONE
for mac, info in hosts.items():
if info.ip_address:
info.wan_access = self._get_wan_access(info.ip_address)
if self.manage_device_info(info, mac, consider_home):
new_device = True
self.send_signal_device_update(new_device)
@@ -630,6 +632,11 @@ class AvmWrapper(FritzBoxTools):
)
return {}
async def async_get_upnp_configuration(self) -> dict[str, Any]:
"""Call X_AVM-DE_UPnP service."""
return await self.hass.async_add_executor_job(self.get_upnp_configuration)
async def async_get_wan_link_properties(self) -> dict[str, Any]:
"""Call WANCommonInterfaceConfig service."""
@@ -637,6 +644,22 @@ class AvmWrapper(FritzBoxTools):
partial(self.get_wan_link_properties)
)
async def async_get_connection_info(self) -> ConnectionInfo:
"""Return ConnectionInfo data."""
link_properties = await self.async_get_wan_link_properties()
connection_info = ConnectionInfo(
connection=link_properties.get("NewWANAccessType", "").lower(),
mesh_role=self.mesh_role,
wan_enabled=self.device_is_router,
)
_LOGGER.debug(
"ConnectionInfo for FritzBox %s: %s",
self.host,
connection_info,
)
return connection_info
async def async_get_port_mapping(self, con_type: str, index: int) -> dict[str, Any]:
"""Call GetGenericPortMappingEntry action."""
@@ -698,6 +721,11 @@ class AvmWrapper(FritzBoxTools):
partial(self.set_allow_wan_access, ip_address, turn_on)
)
def get_upnp_configuration(self) -> dict[str, Any]:
"""Call X_AVM-DE_UPnP service."""
return self._service_call_action("X_AVM-DE_UPnP", "1", "GetInfo")
def get_ontel_num_deflections(self) -> dict[str, Any]:
"""Call GetNumberOfDeflections action from X_AVM-DE_OnTel service."""
@@ -960,3 +988,12 @@ class FritzBoxBaseEntity:
name=self._device_name,
sw_version=self._avm_wrapper.current_firmware,
)
@dataclass
class ConnectionInfo:
"""Fritz sensor connection information class."""
connection: str
mesh_role: MeshRoles
wan_enabled: bool

View File

@@ -29,6 +29,7 @@ from .const import (
ERROR_AUTH_INVALID,
ERROR_CANNOT_CONNECT,
ERROR_UNKNOWN,
ERROR_UPNP_NOT_CONFIGURED,
)
_LOGGER = logging.getLogger(__name__)
@@ -79,6 +80,12 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
return ERROR_UNKNOWN
if (
"X_AVM-DE_UPnP1" in self.avm_wrapper.connection.services
and not (await self.avm_wrapper.async_get_upnp_configuration())["NewEnable"]
):
return ERROR_UPNP_NOT_CONFIGURED
return None
async def async_check_configured_entry(self) -> ConfigEntry | None:

View File

@@ -46,6 +46,7 @@ DEFAULT_USERNAME = ""
ERROR_AUTH_INVALID = "invalid_auth"
ERROR_CANNOT_CONNECT = "cannot_connect"
ERROR_UPNP_NOT_CONFIGURED = "upnp_not_configured"
ERROR_UNKNOWN = "unknown_error"
FRITZ_SERVICES = "fritz_services"
@@ -56,6 +57,7 @@ SERVICE_SET_GUEST_WIFI_PW = "set_guest_wifi_password"
SWITCH_TYPE_DEFLECTION = "CallDeflection"
SWITCH_TYPE_PORTFORWARD = "PortForward"
SWITCH_TYPE_PROFILE = "Profile"
SWITCH_TYPE_WIFINETWORK = "WiFiNetwork"
UPTIME_DEVIATION = 5

View File

@@ -22,6 +22,9 @@ async def async_get_config_entry_diagnostics(
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
"device_info": {
"model": avm_wrapper.model,
"unique_id": avm_wrapper.unique_id.replace(
avm_wrapper.unique_id[6:11], "XX:XX"
),
"current_firmware": avm_wrapper.current_firmware,
"latest_firmware": avm_wrapper.latest_firmware,
"update_available": avm_wrapper.update_available,

View File

@@ -28,8 +28,8 @@ from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import utcnow
from .common import AvmWrapper, FritzBoxBaseEntity
from .const import DOMAIN, DSL_CONNECTION, UPTIME_DEVIATION, MeshRoles
from .common import AvmWrapper, ConnectionInfo, FritzBoxBaseEntity
from .const import DOMAIN, DSL_CONNECTION, UPTIME_DEVIATION
_LOGGER = logging.getLogger(__name__)
@@ -134,15 +134,6 @@ def _retrieve_link_attenuation_received_state(
return status.attenuation[1] / 10 # type: ignore[no-any-return]
@dataclass
class ConnectionInfo:
"""Fritz sensor connection information class."""
connection: str
mesh_role: MeshRoles
wan_enabled: bool
@dataclass
class FritzRequireKeysMixin:
"""Fritz sensor data class."""
@@ -283,18 +274,7 @@ async def async_setup_entry(
_LOGGER.debug("Setting up FRITZ!Box sensors")
avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id]
link_properties = await avm_wrapper.async_get_wan_link_properties()
connection_info = ConnectionInfo(
connection=link_properties.get("NewWANAccessType", "").lower(),
mesh_role=avm_wrapper.mesh_role,
wan_enabled=avm_wrapper.device_is_router,
)
_LOGGER.debug(
"ConnectionInfo for FritzBox %s: %s",
avm_wrapper.host,
connection_info,
)
connection_info = await avm_wrapper.async_get_connection_info()
entities = [
FritzBoxSensor(avm_wrapper, entry.title, description)

View File

@@ -36,6 +36,7 @@
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"upnp_not_configured": "Missing UPnP settings on device.",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"

View File

@@ -30,6 +30,7 @@ from .const import (
DOMAIN,
SWITCH_TYPE_DEFLECTION,
SWITCH_TYPE_PORTFORWARD,
SWITCH_TYPE_PROFILE,
SWITCH_TYPE_WIFINETWORK,
WIFI_STANDARD,
MeshRoles,
@@ -185,6 +186,7 @@ def profile_entities_list(
data_fritz: FritzData,
) -> list[FritzBoxProfileSwitch]:
"""Add new tracker entities from the AVM device."""
_LOGGER.debug("Setting up %s switches", SWITCH_TYPE_PROFILE)
new_profiles: list[FritzBoxProfileSwitch] = []
@@ -198,11 +200,15 @@ def profile_entities_list(
if device_filter_out_from_trackers(
mac, device, data_fritz.profile_switches.values()
):
_LOGGER.debug(
"Skipping profile switch creation for device %s", device.hostname
)
continue
new_profiles.append(FritzBoxProfileSwitch(avm_wrapper, device))
data_fritz.profile_switches[avm_wrapper.unique_id].add(mac)
_LOGGER.debug("Creating %s profile switches", len(new_profiles))
return new_profiles

View File

@@ -9,7 +9,8 @@
"already_configured": "Device is already configured",
"already_in_progress": "Configuration flow is already in progress",
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication"
"invalid_auth": "Invalid authentication",
"upnp_not_configured": "Missing UPnP settings on device."
},
"flow_title": "{name}",
"step": {
@@ -51,4 +52,4 @@
}
}
}
}
}

View File

@@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
"home-assistant-frontend==20220301.0"
"home-assistant-frontend==20220301.2"
],
"dependencies": [
"api",
@@ -13,7 +13,8 @@
"diagnostics",
"http",
"lovelace",
"onboarding", "search",
"onboarding",
"search",
"system_log",
"websocket_api"
],

View File

@@ -59,11 +59,12 @@ SERVICE_SET = "set"
SERVICE_REMOVE = "remove"
PLATFORMS = [
Platform.LIGHT,
Platform.COVER,
Platform.NOTIFY,
Platform.FAN,
Platform.BINARY_SENSOR,
Platform.COVER,
Platform.FAN,
Platform.LIGHT,
Platform.MEDIA_PLAYER,
Platform.NOTIFY,
]
REG_KEY = f"{DOMAIN}_registry"

View File

@@ -221,12 +221,9 @@ class GrowattData:
# Create datetime from the latest entry
date_now = dt.now().date()
last_updated_time = dt.parse_time(str(sorted_keys[-1]))
combined_timestamp = datetime.datetime.combine(
date_now, last_updated_time
mix_detail["lastdataupdate"] = datetime.datetime.combine(
date_now, last_updated_time, dt.DEFAULT_TIME_ZONE
)
# Convert datetime to UTC
combined_timestamp_utc = dt.as_utc(combined_timestamp)
mix_detail["lastdataupdate"] = combined_timestamp_utc.isoformat()
# Dashboard data is largely inaccurate for mix system but it is the only call with the ability to return the combined
# imported from grid value that is the combination of charging AND load consumption

View File

@@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/home_connect",
"dependencies": ["http"],
"codeowners": ["@DavidMStraub"],
"requirements": ["homeconnect==0.6.3"],
"requirements": ["homeconnect==0.7.0"],
"config_flow": true,
"iot_class": "cloud_push",
"loggers": ["homeconnect"]

View File

@@ -274,7 +274,7 @@ class HomeAccessory(Accessory):
if self.config.get(ATTR_SW_VERSION) is not None:
sw_version = format_version(self.config[ATTR_SW_VERSION])
if sw_version is None:
sw_version = __version__
sw_version = format_version(__version__)
hw_version = None
if self.config.get(ATTR_HW_VERSION) is not None:
hw_version = format_version(self.config[ATTR_HW_VERSION])
@@ -289,7 +289,9 @@ class HomeAccessory(Accessory):
serv_info = self.get_service(SERV_ACCESSORY_INFO)
char = self.driver.loader.get_char(CHAR_HARDWARE_REVISION)
serv_info.add_characteristic(char)
serv_info.configure_char(CHAR_HARDWARE_REVISION, value=hw_version)
serv_info.configure_char(
CHAR_HARDWARE_REVISION, value=hw_version[:MAX_VERSION_LENGTH]
)
self.iid_manager.assign(char)
char.broker = self
@@ -532,7 +534,7 @@ class HomeBridge(Bridge):
"""Initialize a Bridge object."""
super().__init__(driver, name)
self.set_info_service(
firmware_revision=__version__,
firmware_revision=format_version(__version__),
manufacturer=MANUFACTURER,
model=BRIDGE_MODEL,
serial_number=BRIDGE_SERIAL_NUMBER,

View File

@@ -285,20 +285,19 @@ class Thermostat(HomeAccessory):
CHAR_CURRENT_HUMIDITY, value=50
)
fan_modes = self.fan_modes = {
fan_mode.lower(): fan_mode
for fan_mode in attributes.get(ATTR_FAN_MODES, [])
}
fan_modes = {}
self.ordered_fan_speeds = []
if (
features & SUPPORT_FAN_MODE
and fan_modes
and PRE_DEFINED_FAN_MODES.intersection(fan_modes)
):
self.ordered_fan_speeds = [
speed for speed in ORDERED_FAN_SPEEDS if speed in fan_modes
]
self.fan_chars.append(CHAR_ROTATION_SPEED)
if features & SUPPORT_FAN_MODE:
fan_modes = {
fan_mode.lower(): fan_mode
for fan_mode in attributes.get(ATTR_FAN_MODES) or []
}
if fan_modes and PRE_DEFINED_FAN_MODES.intersection(fan_modes):
self.ordered_fan_speeds = [
speed for speed in ORDERED_FAN_SPEEDS if speed in fan_modes
]
self.fan_chars.append(CHAR_ROTATION_SPEED)
if FAN_AUTO in fan_modes and (FAN_ON in fan_modes or self.ordered_fan_speeds):
self.fan_chars.append(CHAR_TARGET_FAN_STATE)

View File

@@ -100,6 +100,7 @@ _LOGGER = logging.getLogger(__name__)
NUMBERS_ONLY_RE = re.compile(r"[^\d.]+")
VERSION_RE = re.compile(r"([0-9]+)(\.[0-9]+)?(\.[0-9]+)?")
MAX_VERSION_PART = 2**32 - 1
MAX_PORT = 65535
@@ -363,7 +364,15 @@ def convert_to_float(state):
return None
def cleanup_name_for_homekit(name: str | None) -> str | None:
def coerce_int(state: str) -> int:
"""Return int."""
try:
return int(state)
except (ValueError, TypeError):
return 0
def cleanup_name_for_homekit(name: str | None) -> str:
"""Ensure the name of the device will not crash homekit."""
#
# This is not a security measure.
@@ -371,7 +380,7 @@ def cleanup_name_for_homekit(name: str | None) -> str | None:
# UNICODE_EMOJI is also not allowed but that
# likely isn't a problem
if name is None:
return None
return "None" # None crashes apple watches
return name.translate(HOMEKIT_CHAR_TRANSLATIONS)[:MAX_NAME_LENGTH]
@@ -420,13 +429,23 @@ def get_aid_storage_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str):
)
def _format_version_part(version_part: str) -> str:
return str(max(0, min(MAX_VERSION_PART, coerce_int(version_part))))
def format_version(version):
"""Extract the version string in a format homekit can consume."""
split_ver = str(version).replace("-", ".")
split_ver = str(version).replace("-", ".").replace(" ", ".")
num_only = NUMBERS_ONLY_RE.sub("", split_ver)
if match := VERSION_RE.search(num_only):
return match.group(0)
return None
if (match := VERSION_RE.search(num_only)) is None:
return None
value = ".".join(map(_format_version_part, match.group(0).split(".")))
return None if _is_zero_but_true(value) else value
def _is_zero_but_true(value):
"""Zero but true values can crash apple watches."""
return convert_to_float(value) == 0
def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str):

View File

@@ -3,7 +3,7 @@
"name": "HomeKit Controller",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"requirements": ["aiohomekit==0.7.15"],
"requirements": ["aiohomekit==0.7.16"],
"zeroconf": ["_hap._tcp.local."],
"after_dependencies": ["zeroconf"],
"codeowners": ["@Jc2k", "@bdraco"],

View File

@@ -6,6 +6,7 @@ from collections.abc import Callable
import logging
from typing import Any
import aiohttp
from aiohttp import client_exceptions
from aiohue import HueBridgeV1, HueBridgeV2, LinkButtonNotPressed, Unauthorized
from aiohue.errors import AiohueException, BridgeBusy
@@ -14,7 +15,7 @@ import async_timeout
from homeassistant import core
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_HOST, Platform
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import aiohttp_client
from .const import CONF_API_VERSION, DOMAIN
@@ -116,22 +117,23 @@ class HueBridge:
self.authorized = True
return True
async def async_request_call(
self, task: Callable, *args, allowed_errors: list[str] | None = None, **kwargs
) -> Any:
"""Send request to the Hue bridge, optionally omitting error(s)."""
async def async_request_call(self, task: Callable, *args, **kwargs) -> Any:
"""Send request to the Hue bridge."""
try:
return await task(*args, **kwargs)
except AiohueException as err:
# The (new) Hue api can be a bit fanatic with throwing errors
# some of which we accept in certain conditions
# handle that here. Note that these errors are strings and do not have
# an identifier or something.
if allowed_errors is not None and str(err) in allowed_errors:
# The (new) Hue api can be a bit fanatic with throwing errors so
# we have some logic to treat some responses as warning only.
msg = f"Request failed: {err}"
if "may not have effect" in str(err):
# log only
self.logger.debug("Ignored error/warning from Hue API: %s", str(err))
self.logger.debug(msg)
return None
raise err
raise HomeAssistantError(msg) from err
except aiohttp.ClientError as err:
raise HomeAssistantError(
f"Request failed due connection error: {err}"
) from err
async def async_reset(self) -> bool:
"""Reset this bridge to default state.

View File

@@ -6,6 +6,7 @@ import logging
from typing import Any
from urllib.parse import urlparse
import aiohttp
from aiohue import LinkButtonNotPressed, create_app_key
from aiohue.discovery import DiscoveredHueBridge, discover_bridge, discover_nupnp
from aiohue.util import normalize_bridge_id
@@ -70,9 +71,12 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self, host: str, bridge_id: str | None = None
) -> DiscoveredHueBridge:
"""Return a DiscoveredHueBridge object."""
bridge = await discover_bridge(
host, websession=aiohttp_client.async_get_clientsession(self.hass)
)
try:
bridge = await discover_bridge(
host, websession=aiohttp_client.async_get_clientsession(self.hass)
)
except aiohttp.ClientError:
return None
if bridge_id is not None:
bridge_id = normalize_bridge_id(bridge_id)
assert bridge_id == bridge.id

View File

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

View File

@@ -37,13 +37,6 @@ from .helpers import (
normalize_hue_transition,
)
ALLOWED_ERRORS = [
"device (groupedLight) has communication issues, command (on) may not have effect",
'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',
]
async def async_setup_entry(
hass: HomeAssistant,
@@ -175,10 +168,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
and flash is None
):
await self.bridge.async_request_call(
self.controller.set_state,
id=self.resource.id,
on=True,
allowed_errors=ALLOWED_ERRORS,
self.controller.set_state, id=self.resource.id, on=True
)
return
@@ -194,7 +184,6 @@ 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,
allowed_errors=ALLOWED_ERRORS,
)
for light in self.controller.get_lights(self.resource.id)
]
@@ -214,10 +203,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
# 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,
self.controller.set_state, id=self.resource.id, on=False
)
return
@@ -229,7 +215,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
light.id,
on=False,
transition_time=transition,
allowed_errors=ALLOWED_ERRORS,
)
for light in self.controller.get_lights(self.resource.id)
]

View File

@@ -36,11 +36,6 @@ from .helpers import (
normalize_hue_transition,
)
ALLOWED_ERRORS = [
"device (light) has communication issues, command (on) may not have effect",
'device (light) is "soft off", command (on) may not have effect',
]
async def async_setup_entry(
hass: HomeAssistant,
@@ -178,7 +173,6 @@ class HueLight(HueBaseEntity, LightEntity):
color_xy=xy_color,
color_temp=color_temp,
transition_time=transition,
allowed_errors=ALLOWED_ERRORS,
)
async def async_turn_off(self, **kwargs: Any) -> None:
@@ -198,7 +192,6 @@ class HueLight(HueBaseEntity, LightEntity):
id=self.resource.id,
on=False,
transition_time=transition,
allowed_errors=ALLOWED_ERRORS,
)
async def async_set_flash(self, flash: str) -> None:

View File

@@ -3,7 +3,7 @@
"name": "Insteon",
"documentation": "https://www.home-assistant.io/integrations/insteon",
"requirements": [
"pyinsteon==1.0.16"
"pyinsteon==1.0.13"
],
"codeowners": [
"@teharris1"

View File

@@ -2,7 +2,7 @@
"domain": "isy994",
"name": "Universal Devices ISY994",
"documentation": "https://www.home-assistant.io/integrations/isy994",
"requirements": ["pyisy==3.0.1"],
"requirements": ["pyisy==3.0.5"],
"codeowners": ["@bdraco", "@shbatm"],
"config_flow": true,
"ssdp": [

View File

@@ -717,6 +717,8 @@ class KodiEntity(MediaPlayerEntity):
await self._kodi.play_channel(int(media_id))
elif media_type_lower == MEDIA_TYPE_PLAYLIST:
await self._kodi.play_playlist(int(media_id))
elif media_type_lower == "file":
await self._kodi.play_file(media_id)
elif media_type_lower == "directory":
await self._kodi.play_directory(media_id)
elif media_type_lower in [

View File

@@ -3,7 +3,7 @@
"name": "LIFX",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/lifx",
"requirements": ["aiolifx==0.7.0", "aiolifx_effects==0.2.2"],
"requirements": ["aiolifx==0.7.1", "aiolifx_effects==0.2.2"],
"homekit": {
"models": ["LIFX"]
},

View File

@@ -1,6 +1,7 @@
"""Support for Honeywell Lyric climate platform."""
from __future__ import annotations
import asyncio
import logging
from time import localtime, strftime, time
@@ -22,6 +23,7 @@ from homeassistant.components.climate.const import (
HVAC_MODE_OFF,
SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE
@@ -45,7 +47,11 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
# Only LCC models support presets
SUPPORT_FLAGS_LCC = (
SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE_RANGE
)
SUPPORT_FLAGS_TCC = SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE
LYRIC_HVAC_ACTION_OFF = "EquipmentOff"
LYRIC_HVAC_ACTION_HEAT = "Heat"
@@ -166,7 +172,11 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
@property
def supported_features(self) -> int:
"""Return the list of supported features."""
return SUPPORT_FLAGS
if self.device.changeableValues.thermostatSetpointStatus:
support_flags = SUPPORT_FLAGS_LCC
else:
support_flags = SUPPORT_FLAGS_TCC
return support_flags
@property
def temperature_unit(self) -> str:
@@ -200,25 +210,28 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
device = self.device
if not device.hasDualSetpointStatus:
if (
not device.changeableValues.autoChangeoverActive
and HVAC_MODES[device.changeableValues.mode] != HVAC_MODE_OFF
):
if self.hvac_mode == HVAC_MODE_COOL:
return device.changeableValues.coolSetpoint
return device.changeableValues.heatSetpoint
return None
@property
def target_temperature_low(self) -> float | None:
"""Return the upper bound temperature we try to reach."""
def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach."""
device = self.device
if device.hasDualSetpointStatus:
if device.changeableValues.autoChangeoverActive:
return device.changeableValues.coolSetpoint
return None
@property
def target_temperature_high(self) -> float | None:
"""Return the upper bound temperature we try to reach."""
def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach."""
device = self.device
if device.hasDualSetpointStatus:
if device.changeableValues.autoChangeoverActive:
return device.changeableValues.heatSetpoint
return None
@@ -256,11 +269,11 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
async def async_set_temperature(self, **kwargs) -> None:
"""Set new target temperature."""
device = self.device
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
device = self.device
if device.hasDualSetpointStatus:
if device.changeableValues.autoChangeoverActive:
if target_temp_low is None or target_temp_high is None:
raise HomeAssistantError(
"Could not find target_temp_low and/or target_temp_high in arguments"
@@ -270,11 +283,13 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
await self._update_thermostat(
self.location,
device,
coolSetpoint=target_temp_low,
heatSetpoint=target_temp_high,
coolSetpoint=target_temp_high,
heatSetpoint=target_temp_low,
mode=HVAC_MODES[device.changeableValues.heatCoolMode],
)
except LYRIC_EXCEPTIONS as exception:
_LOGGER.error(exception)
await self.coordinator.async_refresh()
else:
temp = kwargs.get(ATTR_TEMPERATURE)
_LOGGER.debug("Set temperature: %s", temp)
@@ -289,15 +304,58 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
)
except LYRIC_EXCEPTIONS as exception:
_LOGGER.error(exception)
await self.coordinator.async_refresh()
await self.coordinator.async_refresh()
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set hvac mode."""
_LOGGER.debug("Set hvac mode: %s", hvac_mode)
_LOGGER.debug("HVAC mode: %s", hvac_mode)
try:
await self._update_thermostat(
self.location, self.device, mode=LYRIC_HVAC_MODES[hvac_mode]
)
if LYRIC_HVAC_MODES[hvac_mode] == LYRIC_HVAC_MODE_HEAT_COOL:
# If the system is off, turn it to Heat first then to Auto, otherwise it turns to
# Auto briefly and then reverts to Off (perhaps related to heatCoolMode). This is the
# behavior that happens with the native app as well, so likely a bug in the api itself
if HVAC_MODES[self.device.changeableValues.mode] == HVAC_MODE_OFF:
_LOGGER.debug(
"HVAC mode passed to lyric: %s",
HVAC_MODES[LYRIC_HVAC_MODE_COOL],
)
await self._update_thermostat(
self.location,
self.device,
mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
autoChangeoverActive=False,
)
# Sleep 3 seconds before proceeding
await asyncio.sleep(3)
_LOGGER.debug(
"HVAC mode passed to lyric: %s",
HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
)
await self._update_thermostat(
self.location,
self.device,
mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
autoChangeoverActive=True,
)
else:
_LOGGER.debug(
"HVAC mode passed to lyric: %s",
HVAC_MODES[self.device.changeableValues.mode],
)
await self._update_thermostat(
self.location, self.device, autoChangeoverActive=True
)
else:
_LOGGER.debug(
"HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode]
)
await self._update_thermostat(
self.location,
self.device,
mode=LYRIC_HVAC_MODES[hvac_mode],
autoChangeoverActive=False,
)
except LYRIC_EXCEPTIONS as exception:
_LOGGER.error(exception)
await self.coordinator.async_refresh()

View File

@@ -243,7 +243,10 @@ class MatrixBot:
room.update_aliases()
self._aliases_fetched_for.add(room.room_id)
if room_id_or_alias in room.aliases:
if (
room_id_or_alias in room.aliases
or room_id_or_alias == room.canonical_alias
):
_LOGGER.debug(
"Already in room %s (known as %s)", room.room_id, room_id_or_alias
)

View File

@@ -2,7 +2,7 @@
"domain": "mediaroom",
"name": "Mediaroom",
"documentation": "https://www.home-assistant.io/integrations/mediaroom",
"requirements": ["pymediaroom==0.6.4.1"],
"requirements": ["pymediaroom==0.6.5.4"],
"codeowners": ["@dgomes"],
"iot_class": "local_polling",
"loggers": ["pymediaroom"]

View File

@@ -13,7 +13,7 @@ import logging
from operator import attrgetter
import ssl
import time
from typing import Any, Union, cast
from typing import TYPE_CHECKING, Any, Union, cast
import uuid
import attr
@@ -75,11 +75,16 @@ from .const import (
ATTR_TOPIC,
CONF_BIRTH_MESSAGE,
CONF_BROKER,
CONF_CERTIFICATE,
CONF_CLIENT_CERT,
CONF_CLIENT_KEY,
CONF_COMMAND_TOPIC,
CONF_ENCODING,
CONF_QOS,
CONF_RETAIN,
CONF_STATE_TOPIC,
CONF_TLS_INSECURE,
CONF_TLS_VERSION,
CONF_TOPIC,
CONF_WILL_MESSAGE,
DATA_MQTT_CONFIG,
@@ -94,6 +99,7 @@ from .const import (
DOMAIN,
MQTT_CONNECTED,
MQTT_DISCONNECTED,
PROTOCOL_31,
PROTOCOL_311,
)
from .discovery import LAST_DISCOVERY
@@ -107,6 +113,11 @@ from .models import (
)
from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic
if TYPE_CHECKING:
# Only import for paho-mqtt type checking here, imports are done locally
# because integrations should be able to optionally rely on MQTT.
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
_LOGGER = logging.getLogger(__name__)
_SENTINEL = object()
@@ -118,19 +129,19 @@ SERVICE_DUMP = "dump"
CONF_DISCOVERY_PREFIX = "discovery_prefix"
CONF_KEEPALIVE = "keepalive"
CONF_CERTIFICATE = "certificate"
CONF_CLIENT_KEY = "client_key"
CONF_CLIENT_CERT = "client_cert"
CONF_TLS_INSECURE = "tls_insecure"
CONF_TLS_VERSION = "tls_version"
PROTOCOL_31 = "3.1"
DEFAULT_PORT = 1883
DEFAULT_KEEPALIVE = 60
DEFAULT_PROTOCOL = PROTOCOL_311
DEFAULT_TLS_PROTOCOL = "auto"
DEFAULT_VALUES = {
CONF_PORT: DEFAULT_PORT,
CONF_WILL_MESSAGE: DEFAULT_WILL,
CONF_BIRTH_MESSAGE: DEFAULT_BIRTH,
CONF_DISCOVERY: DEFAULT_DISCOVERY,
}
ATTR_TOPIC_TEMPLATE = "topic_template"
ATTR_PAYLOAD_TEMPLATE = "payload_template"
@@ -186,7 +197,7 @@ CONFIG_SCHEMA_BASE = vol.Schema(
vol.Coerce(int), vol.Range(min=15)
),
vol.Optional(CONF_BROKER): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile),
@@ -203,9 +214,9 @@ CONFIG_SCHEMA_BASE = vol.Schema(
vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All(
cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])
),
vol.Optional(CONF_WILL_MESSAGE, default=DEFAULT_WILL): MQTT_WILL_BIRTH_SCHEMA,
vol.Optional(CONF_BIRTH_MESSAGE, default=DEFAULT_BIRTH): MQTT_WILL_BIRTH_SCHEMA,
vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean,
vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA,
vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA,
vol.Optional(CONF_DISCOVERY): cv.boolean,
# discovery_prefix must be a valid publish topic because if no
# state topic is specified, it will be created with the given prefix.
vol.Optional(
@@ -609,6 +620,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
def _merge_config(entry, conf):
"""Merge configuration.yaml config with config entry."""
# Base config on default values
conf = {**DEFAULT_VALUES, **conf}
return {**conf, **entry.data}
@@ -628,6 +641,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
override,
)
# Merge the configuration values from configuration.yaml
conf = _merge_config(entry, conf)
hass.data[DATA_MQTT] = MQTT(
@@ -757,6 +771,58 @@ class Subscription:
encoding: str | None = attr.ib(default="utf-8")
class MqttClientSetup:
"""Helper class to setup the paho mqtt client from config."""
def __init__(self, config: ConfigType) -> None:
"""Initialize the MQTT client setup helper."""
# We don't import on the top because some integrations
# should be able to optionally rely on MQTT.
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
if config[CONF_PROTOCOL] == PROTOCOL_31:
proto = mqtt.MQTTv31
else:
proto = mqtt.MQTTv311
if (client_id := config.get(CONF_CLIENT_ID)) is None:
# PAHO MQTT relies on the MQTT server to generate random client IDs.
# However, that feature is not mandatory so we generate our own.
client_id = mqtt.base62(uuid.uuid4().int, padding=22)
self._client = mqtt.Client(client_id, protocol=proto)
# Enable logging
self._client.enable_logger()
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
if username is not None:
self._client.username_pw_set(username, password)
if (certificate := config.get(CONF_CERTIFICATE)) == "auto":
certificate = certifi.where()
client_key = config.get(CONF_CLIENT_KEY)
client_cert = config.get(CONF_CLIENT_CERT)
tls_insecure = config.get(CONF_TLS_INSECURE)
if certificate is not None:
self._client.tls_set(
certificate,
certfile=client_cert,
keyfile=client_key,
tls_version=ssl.PROTOCOL_TLS,
)
if tls_insecure is not None:
self._client.tls_insecure_set(tls_insecure)
@property
def client(self) -> mqtt.Client:
"""Return the paho MQTT client."""
return self._client
class MQTT:
"""Home Assistant MQTT client."""
@@ -821,46 +887,7 @@ class MQTT:
def init_client(self):
"""Initialize paho client."""
# We don't import on the top because some integrations
# should be able to optionally rely on MQTT.
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
if self.conf[CONF_PROTOCOL] == PROTOCOL_31:
proto: int = mqtt.MQTTv31
else:
proto = mqtt.MQTTv311
if (client_id := self.conf.get(CONF_CLIENT_ID)) is None:
# PAHO MQTT relies on the MQTT server to generate random client IDs.
# However, that feature is not mandatory so we generate our own.
client_id = mqtt.base62(uuid.uuid4().int, padding=22)
self._mqttc = mqtt.Client(client_id, protocol=proto)
# Enable logging
self._mqttc.enable_logger()
username = self.conf.get(CONF_USERNAME)
password = self.conf.get(CONF_PASSWORD)
if username is not None:
self._mqttc.username_pw_set(username, password)
if (certificate := self.conf.get(CONF_CERTIFICATE)) == "auto":
certificate = certifi.where()
client_key = self.conf.get(CONF_CLIENT_KEY)
client_cert = self.conf.get(CONF_CLIENT_CERT)
tls_insecure = self.conf.get(CONF_TLS_INSECURE)
if certificate is not None:
self._mqttc.tls_set(
certificate,
certfile=client_cert,
keyfile=client_key,
tls_version=ssl.PROTOCOL_TLS,
)
if tls_insecure is not None:
self._mqttc.tls_insecure_set(tls_insecure)
self._mqttc = MqttClientSetup(self.conf).client
self._mqttc.on_connect = self._mqtt_on_connect
self._mqttc.on_disconnect = self._mqtt_on_disconnect
self._mqttc.on_message = self._mqtt_on_message

View File

@@ -271,7 +271,7 @@ _PLATFORM_SCHEMA_BASE = SCHEMA_BASE.extend(
vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_HOLD_LIST, default=list): cv.ensure_list,
vol.Optional(CONF_HOLD_LIST): cv.ensure_list,
vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(
@@ -298,7 +298,7 @@ _PLATFORM_SCHEMA_BASE = SCHEMA_BASE.extend(
),
vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean,
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean,
vol.Optional(CONF_SEND_IF_OFF): cv.boolean,
vol.Optional(CONF_ACTION_TEMPLATE): cv.template,
vol.Optional(CONF_ACTION_TOPIC): mqtt.valid_subscribe_topic,
# CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together
@@ -431,6 +431,12 @@ class MqttClimate(MqttEntity, ClimateEntity):
self._feature_preset_mode = False
self._optimistic_preset_mode = None
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
self._send_if_off = True
# AWAY and HOLD mode topics and templates are deprecated,
# support will be removed with release 2022.9
self._hold_list = []
MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
@staticmethod
@@ -499,6 +505,15 @@ class MqttClimate(MqttEntity, ClimateEntity):
self._command_templates = command_templates
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
if CONF_SEND_IF_OFF in config:
self._send_if_off = config[CONF_SEND_IF_OFF]
# AWAY and HOLD mode topics and templates are deprecated,
# support will be removed with release 2022.9
if CONF_HOLD_LIST in config:
self._hold_list = config[CONF_HOLD_LIST]
def _prepare_subscribe_topics(self): # noqa: C901
"""(Re)Subscribe to topics."""
topics = {}
@@ -806,7 +821,9 @@ class MqttClimate(MqttEntity, ClimateEntity):
):
presets.append(PRESET_AWAY)
presets.extend(self._config[CONF_HOLD_LIST])
# AWAY and HOLD mode topics and templates are deprecated,
# support will be removed with release 2022.9
presets.extend(self._hold_list)
if presets:
presets.insert(0, PRESET_NONE)
@@ -847,10 +864,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
setattr(self, attr, temp)
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
if (
self._config[CONF_SEND_IF_OFF]
or self._current_operation != HVAC_MODE_OFF
):
if self._send_if_off or self._current_operation != HVAC_MODE_OFF:
payload = self._command_templates[cmnd_template](temp)
await self._publish(cmnd_topic, payload)
@@ -890,7 +904,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
async def async_set_swing_mode(self, swing_mode):
"""Set new swing mode."""
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
if self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF:
if self._send_if_off or self._current_operation != HVAC_MODE_OFF:
payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE](
swing_mode
)
@@ -903,7 +917,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
async def async_set_fan_mode(self, fan_mode):
"""Set new target temperature."""
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
if self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF:
if self._send_if_off or self._current_operation != HVAC_MODE_OFF:
payload = self._command_templates[CONF_FAN_MODE_COMMAND_TEMPLATE](fan_mode)
await self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload)

View File

@@ -17,6 +17,7 @@ from homeassistant.const import (
)
from homeassistant.data_entry_flow import FlowResult
from . import MqttClientSetup
from .const import (
ATTR_PAYLOAD,
ATTR_QOS,
@@ -62,6 +63,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None:
can_connect = await self.hass.async_add_executor_job(
try_connection,
self.hass,
user_input[CONF_BROKER],
user_input[CONF_PORT],
user_input.get(CONF_USERNAME),
@@ -102,6 +104,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
data = self._hassio_discovery
can_connect = await self.hass.async_add_executor_job(
try_connection,
self.hass,
data[CONF_HOST],
data[CONF_PORT],
data.get(CONF_USERNAME),
@@ -152,6 +155,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
if user_input is not None:
can_connect = await self.hass.async_add_executor_job(
try_connection,
self.hass,
user_input[CONF_BROKER],
user_input[CONF_PORT],
user_input.get(CONF_USERNAME),
@@ -313,19 +317,22 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
)
def try_connection(broker, port, username, password, protocol="3.1"):
def try_connection(hass, broker, port, username, password, protocol="3.1"):
"""Test if we can connect to an MQTT broker."""
# pylint: disable-next=import-outside-toplevel
import paho.mqtt.client as mqtt
# We don't import on the top because some integrations
# should be able to optionally rely on MQTT.
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
if protocol == "3.1":
proto = mqtt.MQTTv31
else:
proto = mqtt.MQTTv311
client = mqtt.Client(protocol=proto)
if username and password:
client.username_pw_set(username, password)
# Get the config from configuration.yaml
yaml_config = hass.data.get(DATA_MQTT_CONFIG, {})
entry_config = {
CONF_BROKER: broker,
CONF_PORT: port,
CONF_USERNAME: username,
CONF_PASSWORD: password,
CONF_PROTOCOL: protocol,
}
client = MqttClientSetup({**yaml_config, **entry_config}).client
result = queue.Queue(maxsize=1)

View File

@@ -22,6 +22,12 @@ CONF_STATE_VALUE_TEMPLATE = "state_value_template"
CONF_TOPIC = "topic"
CONF_WILL_MESSAGE = "will_message"
CONF_CERTIFICATE = "certificate"
CONF_CLIENT_KEY = "client_key"
CONF_CLIENT_CERT = "client_cert"
CONF_TLS_INSECURE = "tls_insecure"
CONF_TLS_VERSION = "tls_version"
DATA_MQTT_CONFIG = "mqtt_config"
DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed"
@@ -56,4 +62,5 @@ MQTT_DISCONNECTED = "mqtt_disconnected"
PAYLOAD_EMPTY_JSON = "{}"
PAYLOAD_NONE = "None"
PROTOCOL_31 = "3.1"
PROTOCOL_311 = "3.1.1"

View File

@@ -2,7 +2,7 @@
"domain": "obihai",
"name": "Obihai",
"documentation": "https://www.home-assistant.io/integrations/obihai",
"requirements": ["pyobihai==1.3.1"],
"requirements": ["pyobihai==1.3.2"],
"codeowners": ["@dshokouhi"],
"iot_class": "local_polling",
"loggers": ["pyobihai"]

View File

@@ -43,7 +43,7 @@ async def async_setup_platform(
station_id = config[CONF_STATION_ID]
session = async_get_clientsession(hass)
osm_api = OpenSenseMapData(OpenSenseMap(station_id, hass.loop, session))
osm_api = OpenSenseMapData(OpenSenseMap(station_id, session))
await osm_api.async_update()

View File

@@ -2,7 +2,7 @@
"domain": "opensensemap",
"name": "openSenseMap",
"documentation": "https://www.home-assistant.io/integrations/opensensemap",
"requirements": ["opensensemap-api==0.1.5"],
"requirements": ["opensensemap-api==0.2.0"],
"codeowners": [],
"iot_class": "cloud_polling",
"loggers": ["opensensemap_api"]

View File

@@ -97,6 +97,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
token_saver=token_saver,
)
try:
# pylint: disable-next=fixme
# TODO Remove authlib constraint when refactoring this code
await session.ensure_active_token()
except ConnectTimeout as err:
_LOGGER.debug("Connection Timeout")

View File

@@ -3,7 +3,7 @@
"name": "Radio Browser",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/radio",
"requirements": ["radios==0.1.0"],
"requirements": ["radios==0.1.1"],
"codeowners": ["@frenck"],
"iot_class": "cloud_polling"
}

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/renault",
"requirements": [
"renault-api==0.1.9"
"renault-api==0.1.10"
],
"codeowners": [
"@epenet"

View File

@@ -104,7 +104,7 @@ class RenaultVehicleProxy:
coordinator = self.coordinators[key]
if coordinator.not_supported:
# Remove endpoint as it is not supported for this vehicle.
LOGGER.warning(
LOGGER.info(
"Ignoring endpoint %s as it is not supported for this vehicle: %s",
coordinator.name,
coordinator.last_exception,
@@ -112,7 +112,7 @@ class RenaultVehicleProxy:
del self.coordinators[key]
elif coordinator.access_denied:
# Remove endpoint as it is denied for this vehicle.
LOGGER.warning(
LOGGER.info(
"Ignoring endpoint %s as it is denied for this vehicle: %s",
coordinator.name,
coordinator.last_exception,

View File

@@ -2,7 +2,7 @@
"domain": "rfxtrx",
"name": "RFXCOM RFXtrx",
"documentation": "https://www.home-assistant.io/integrations/rfxtrx",
"requirements": ["pyRFXtrx==0.27.1"],
"requirements": ["pyRFXtrx==0.28.0"],
"codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"],
"config_flow": true,
"iot_class": "local_push",

View File

@@ -1,14 +1,6 @@
"""Support for Roku."""
from __future__ import annotations
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
import logging
from typing import Any, TypeVar
from rokuecp import RokuConnectionError, RokuError
from typing_extensions import Concatenate, ParamSpec
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
@@ -16,7 +8,6 @@ from homeassistant.helpers import config_validation as cv
from .const import DOMAIN
from .coordinator import RokuDataUpdateCoordinator
from .entity import RokuEntity
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
@@ -27,10 +18,6 @@ PLATFORMS = [
Platform.SELECT,
Platform.SENSOR,
]
_LOGGER = logging.getLogger(__name__)
_T = TypeVar("_T", bound="RokuEntity")
_P = ParamSpec("_P")
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -53,22 +40,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
def roku_exception_handler(
func: Callable[Concatenate[_T, _P], Awaitable[None]] # type: ignore[misc]
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: # type: ignore[misc]
"""Decorate Roku calls to handle Roku exceptions."""
@wraps(func)
async def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
try:
await func(self, *args, **kwargs)
except RokuConnectionError as error:
if self.available:
_LOGGER.error("Error communicating with API: %s", error)
except RokuError as error:
if self.available:
_LOGGER.error("Invalid response from API: %s", error)
return wrapper

View File

@@ -1,6 +1,21 @@
"""Helpers for Roku."""
from __future__ import annotations
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
import logging
from typing import Any, TypeVar
from rokuecp import RokuConnectionError, RokuConnectionTimeoutError, RokuError
from typing_extensions import Concatenate, ParamSpec
from .entity import RokuEntity
_LOGGER = logging.getLogger(__name__)
_T = TypeVar("_T", bound=RokuEntity)
_P = ParamSpec("_P")
def format_channel_name(channel_number: str, channel_name: str | None = None) -> str:
"""Format a Roku Channel name."""
@@ -8,3 +23,28 @@ def format_channel_name(channel_number: str, channel_name: str | None = None) ->
return f"{channel_name} ({channel_number})"
return channel_number
def roku_exception_handler(ignore_timeout: bool = False) -> Callable[..., Callable]:
"""Decorate Roku calls to handle Roku exceptions."""
def decorator(
func: Callable[Concatenate[_T, _P], Awaitable[None]], # type: ignore[misc]
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: # type: ignore[misc]
@wraps(func)
async def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
try:
await func(self, *args, **kwargs)
except RokuConnectionTimeoutError as error:
if not ignore_timeout and self.available:
_LOGGER.error("Error communicating with API: %s", error)
except RokuConnectionError as error:
if self.available:
_LOGGER.error("Error communicating with API: %s", error)
except RokuError as error:
if self.available:
_LOGGER.error("Invalid response from API: %s", error)
return wrapper
return decorator

View File

@@ -2,7 +2,7 @@
"domain": "roku",
"name": "Roku",
"documentation": "https://www.home-assistant.io/integrations/roku",
"requirements": ["rokuecp==0.14.1"],
"requirements": ["rokuecp==0.15.0"],
"homekit": {
"models": ["3810X", "4660X", "7820X", "C105X", "C135X"]
},

View File

@@ -51,7 +51,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_platform
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import roku_exception_handler
from .browse_media import async_browse_media
from .const import (
ATTR_ARTIST_NAME,
@@ -65,7 +64,7 @@ from .const import (
)
from .coordinator import RokuDataUpdateCoordinator
from .entity import RokuEntity
from .helpers import format_channel_name
from .helpers import format_channel_name, roku_exception_handler
_LOGGER = logging.getLogger(__name__)
@@ -289,7 +288,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
app.name for app in self.coordinator.data.apps if app.name is not None
)
@roku_exception_handler
@roku_exception_handler()
async def search(self, keyword: str) -> None:
"""Emulate opening the search screen and entering the search keyword."""
await self.coordinator.roku.search(keyword)
@@ -321,68 +320,68 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
media_content_type,
)
@roku_exception_handler
@roku_exception_handler()
async def async_turn_on(self) -> None:
"""Turn on the Roku."""
await self.coordinator.roku.remote("poweron")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler(ignore_timeout=True)
async def async_turn_off(self) -> None:
"""Turn off the Roku."""
await self.coordinator.roku.remote("poweroff")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_pause(self) -> None:
"""Send pause command."""
if self.state not in (STATE_STANDBY, STATE_PAUSED):
await self.coordinator.roku.remote("play")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_play(self) -> None:
"""Send play command."""
if self.state not in (STATE_STANDBY, STATE_PLAYING):
await self.coordinator.roku.remote("play")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_play_pause(self) -> None:
"""Send play/pause command."""
if self.state != STATE_STANDBY:
await self.coordinator.roku.remote("play")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_previous_track(self) -> None:
"""Send previous track command."""
await self.coordinator.roku.remote("reverse")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_next_track(self) -> None:
"""Send next track command."""
await self.coordinator.roku.remote("forward")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_mute_volume(self, mute: bool) -> None:
"""Mute the volume."""
await self.coordinator.roku.remote("volume_mute")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_volume_up(self) -> None:
"""Volume up media player."""
await self.coordinator.roku.remote("volume_up")
@roku_exception_handler
@roku_exception_handler()
async def async_volume_down(self) -> None:
"""Volume down media player."""
await self.coordinator.roku.remote("volume_down")
@roku_exception_handler
@roku_exception_handler()
async def async_play_media(
self, media_type: str, media_id: str, **kwargs: Any
) -> None:
@@ -487,7 +486,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_select_source(self, source: str) -> None:
"""Select input source."""
if source == "Home":

View File

@@ -9,10 +9,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import roku_exception_handler
from .const import DOMAIN
from .coordinator import RokuDataUpdateCoordinator
from .entity import RokuEntity
from .helpers import roku_exception_handler
async def async_setup_entry(
@@ -44,19 +44,19 @@ class RokuRemote(RokuEntity, RemoteEntity):
"""Return true if device is on."""
return not self.coordinator.data.state.standby
@roku_exception_handler
@roku_exception_handler()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
await self.coordinator.roku.remote("poweron")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler(ignore_timeout=True)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
await self.coordinator.roku.remote("poweroff")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
"""Send a command to one device."""
num_repeats = kwargs[ATTR_NUM_REPEATS]

View File

@@ -12,11 +12,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import roku_exception_handler
from .const import DOMAIN
from .coordinator import RokuDataUpdateCoordinator
from .entity import RokuEntity
from .helpers import format_channel_name
from .helpers import format_channel_name, roku_exception_handler
@dataclass
@@ -163,7 +162,7 @@ class RokuSelectEntity(RokuEntity, SelectEntity):
"""Return a set of selectable options."""
return self.entity_description.options_fn(self.coordinator.data)
@roku_exception_handler
@roku_exception_handler()
async def async_select_option(self, option: str) -> None:
"""Set the option."""
await self.entity_description.set_fn(

View File

@@ -2,7 +2,7 @@
"domain": "sabnzbd",
"name": "SABnzbd",
"documentation": "https://www.home-assistant.io/integrations/sabnzbd",
"requirements": ["pysabnzbd==1.1.0"],
"requirements": ["pysabnzbd==1.1.1"],
"dependencies": ["configurator"],
"after_dependencies": ["discovery"],
"codeowners": [],

View File

@@ -10,7 +10,7 @@ from samsungctl import Remote
from samsungctl.exceptions import AccessDenied, ConnectionClosed, UnhandledResponse
from samsungtvws import SamsungTVWS
from samsungtvws.exceptions import ConnectionFailure, HttpApiError
from websocket import WebSocketException
from websocket import WebSocketException, WebSocketTimeoutException
from homeassistant.const import (
CONF_HOST,
@@ -318,9 +318,10 @@ class SamsungTVWSBridge(SamsungTVBridge):
def _get_app_list(self) -> dict[str, str] | None:
"""Get installed app list."""
if self._app_list is None:
if remote := self._get_remote():
if self._app_list is None and (remote := self._get_remote()):
with contextlib.suppress(TypeError, WebSocketTimeoutException):
raw_app_list: list[dict[str, str]] = remote.app_list()
LOGGER.debug("Received app list: %s", raw_app_list)
self._app_list = {
app["name"]: app["appId"]
for app in sorted(raw_app_list, key=lambda app: app["name"])

View File

@@ -10,7 +10,7 @@ import voluptuous as vol
from homeassistant.components.light import ATTR_TRANSITION
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON
from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON, STATE_UNAVAILABLE
from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import RestoreEntity
@@ -117,7 +117,11 @@ class Scene(RestoreEntity):
"""Call when the scene is added to hass."""
await super().async_internal_added_to_hass()
state = await self.async_get_last_state()
if state is not None and state.state is not None:
if (
state is not None
and state.state is not None
and state.state != STATE_UNAVAILABLE
):
self.__last_activated = state.state
def activate(self, **kwargs: Any) -> None:

View File

@@ -79,7 +79,7 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
"""Handle dhcp discovery."""
mac = _extract_mac_from_name(discovery_info.hostname)
mac = format_mac(discovery_info.macaddress)
await self.async_set_unique_id(mac)
self._abort_if_unique_id_configured(
updates={CONF_IP_ADDRESS: discovery_info.ip}

View File

@@ -8,7 +8,7 @@
"dhcp": [
{"registered_devices": true},
{
"hostname": "pentair: *",
"hostname": "pentair*",
"macaddress": "00C033*"
}
],

View File

@@ -175,7 +175,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Call a service to reload scripts."""
if (conf := await component.async_prepare_reload()) is None:
return
async_get_blueprints(hass).async_reset_cache()
await _async_process_config(hass, conf, component)
async def turn_on_service(service: ServiceCall) -> None:

View File

@@ -12,7 +12,7 @@ from homeassistant import config_entries
from homeassistant.const import CONF_CODE, CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, DOMAIN, SENSE_TIMEOUT_EXCEPTIONS
from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, DOMAIN, SENSE_CONNECT_EXCEPTIONS
_LOGGER = logging.getLogger(__name__)
@@ -76,7 +76,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self.validate_input(user_input)
except SenseMFARequiredException:
return await self.async_step_validation()
except SENSE_TIMEOUT_EXCEPTIONS:
except SENSE_CONNECT_EXCEPTIONS:
errors["base"] = "cannot_connect"
except SenseAuthenticationException:
errors["base"] = "invalid_auth"
@@ -93,7 +93,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if user_input:
try:
await self._gateway.validate_mfa(user_input[CONF_CODE])
except SENSE_TIMEOUT_EXCEPTIONS:
except SENSE_CONNECT_EXCEPTIONS:
errors["base"] = "cannot_connect"
except SenseAuthenticationException:
errors["base"] = "invalid_auth"

View File

@@ -3,8 +3,11 @@
import asyncio
import socket
from sense_energy import SenseAPITimeoutException
from sense_energy.sense_exceptions import SenseWebsocketException
from sense_energy import (
SenseAPIException,
SenseAPITimeoutException,
SenseWebsocketException,
)
DOMAIN = "sense"
DEFAULT_TIMEOUT = 10
@@ -40,6 +43,11 @@ ICON = "mdi:flash"
SENSE_TIMEOUT_EXCEPTIONS = (asyncio.TimeoutError, SenseAPITimeoutException)
SENSE_EXCEPTIONS = (socket.gaierror, SenseWebsocketException)
SENSE_CONNECT_EXCEPTIONS = (
asyncio.TimeoutError,
SenseAPITimeoutException,
SenseAPIException,
)
MDI_ICONS = {
"ac": "air-conditioner",

View File

@@ -2,7 +2,7 @@
"domain": "sense",
"name": "Sense",
"documentation": "https://www.home-assistant.io/integrations/sense",
"requirements": ["sense_energy==0.10.2"],
"requirements": ["sense_energy==0.10.4"],
"codeowners": ["@kbickar"],
"config_flow": true,
"dhcp": [

View File

@@ -15,6 +15,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, TIMEOUT
MAX_POSSIBLE_STEP = 1000
class SensiboDataUpdateCoordinator(DataUpdateCoordinator):
"""A Sensibo Data Update Coordinator."""
@@ -74,7 +76,11 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator):
.get("values", [0, 1])
)
if temperatures_list:
temperature_step = temperatures_list[1] - temperatures_list[0]
diff = MAX_POSSIBLE_STEP
for i in range(len(temperatures_list) - 1):
if temperatures_list[i + 1] - temperatures_list[i] < diff:
diff = temperatures_list[i + 1] - temperatures_list[i]
temperature_step = diff
active_features = list(ac_states)
full_features = set()

View File

@@ -317,4 +317,14 @@ class BlockSleepingClimate(
if self.device_block and self.block:
_LOGGER.debug("Entity %s attached to blocks", self.name)
assert self.block.channel
self._preset_modes = [
PRESET_NONE,
*self.wrapper.device.settings["thermostats"][int(self.block.channel)][
"schedule_profile_names"
],
]
self.async_write_ha_state()

View File

@@ -336,7 +336,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity):
ATTR_RGBW_COLOR
]
if ATTR_EFFECT in kwargs:
if ATTR_EFFECT in kwargs and ATTR_COLOR_TEMP not in kwargs:
# Color effect change - used only in color mode, switch device mode to color
set_mode = "color"
if self.wrapper.model == "SHBLB-1":

View File

@@ -174,6 +174,7 @@ SENSORS: Final = {
value=lambda value: round(value / 1000, 2),
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
available=lambda block: cast(int, block.energy) != -1,
),
("emeter", "energyReturned"): BlockSensorDescription(
key="emeter|energyReturned",
@@ -182,6 +183,7 @@ SENSORS: Final = {
value=lambda value: round(value / 1000, 2),
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
available=lambda block: cast(int, block.energyReturned) != -1,
),
("light", "energy"): BlockSensorDescription(
key="light|energy",

View File

@@ -9,22 +9,13 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY, CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.util import slugify
from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN
@callback
def solaredge_entries(hass: HomeAssistant):
"""Return the site_ids for the domain."""
return {
(entry.data[CONF_SITE_ID])
for entry in hass.config_entries.async_entries(DOMAIN)
}
class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
@@ -34,9 +25,18 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Initialize the config flow."""
self._errors = {}
@callback
def _async_current_site_ids(self) -> set[str]:
"""Return the site_ids for the domain."""
return {
entry.data[CONF_SITE_ID]
for entry in self._async_current_entries(include_ignore=False)
if CONF_SITE_ID in entry.data
}
def _site_in_configuration_exists(self, site_id: str) -> bool:
"""Return True if site_id exists in configuration."""
return site_id in solaredge_entries(self.hass)
return site_id in self._async_current_site_ids()
def _check_site(self, site_id: str, api_key: str) -> bool:
"""Check if we can connect to the soleredge api service."""

View File

@@ -79,6 +79,7 @@ class SomfyShade(RestoreEntity, CoverEntity):
self._attr_unique_id = target_id
self._attr_name = name
self._reverse = reverse
self._attr_is_closed = None
self._attr_device_class = device_class
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._target_id)},

View File

@@ -3,7 +3,7 @@
"name": "Sonos",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sonos",
"requirements": ["soco==0.26.3"],
"requirements": ["soco==0.26.4"],
"dependencies": ["ssdp"],
"after_dependencies": ["plex", "spotify", "zeroconf", "media_source"],
"zeroconf": ["_sonos._tcp.local."],

View File

@@ -63,6 +63,7 @@ from .media import SonosMedia
from .statistics import ActivityStatistics, EventStatistics
NEVER_TIME = -1200.0
RESUB_COOLDOWN_SECONDS = 10.0
EVENT_CHARGING = {
"CHARGING": True,
"NOT_CHARGING": False,
@@ -126,6 +127,7 @@ class SonosSpeaker:
self._last_event_cache: dict[str, Any] = {}
self.activity_stats: ActivityStatistics = ActivityStatistics(self.zone_name)
self.event_stats: EventStatistics = EventStatistics(self.zone_name)
self._resub_cooldown_expires_at: float | None = None
# Scheduled callback handles
self._poll_timer: Callable | None = None
@@ -502,6 +504,16 @@ class SonosSpeaker:
@callback
def speaker_activity(self, source):
"""Track the last activity on this speaker, set availability and resubscribe."""
if self._resub_cooldown_expires_at:
if time.monotonic() < self._resub_cooldown_expires_at:
_LOGGER.debug(
"Activity on %s from %s while in cooldown, ignoring",
self.zone_name,
source,
)
return
self._resub_cooldown_expires_at = None
_LOGGER.debug("Activity on %s from %s", self.zone_name, source)
self._last_activity = time.monotonic()
self.activity_stats.activity(source, self._last_activity)
@@ -542,6 +554,10 @@ class SonosSpeaker:
if not self.available:
return
if self._resub_cooldown_expires_at is None and not self.hass.is_stopping:
self._resub_cooldown_expires_at = time.monotonic() + RESUB_COOLDOWN_SECONDS
_LOGGER.debug("Starting resubscription cooldown for %s", self.zone_name)
self.available = False
self.async_write_entity_states()

View File

@@ -172,7 +172,7 @@ class SQLSensor(SensorEntity):
else:
self._attr_native_value = data
if not data:
if data is None:
_LOGGER.warning("%s returned no results", self._query)
sess.close()

View File

@@ -101,6 +101,7 @@ class Sun(Entity):
self.rising = self.phase = None
self._next_change = None
@callback
def update_location(_event):
location, elevation = get_astral_location(self.hass)
if location == self.location:

View File

@@ -2,7 +2,7 @@
"domain": "synology_dsm",
"name": "Synology DSM",
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
"requirements": ["py-synologydsm-api==1.0.6"],
"requirements": ["py-synologydsm-api==1.0.7"],
"codeowners": ["@hacf-fr", "@Quentame", "@mib1185"],
"config_flow": true,
"ssdp": [

View File

@@ -277,8 +277,6 @@ class TemplateFan(TemplateEntity, FanEntity):
"""Turn off the fan."""
await self._off_script.async_run(context=self._context)
self._state = STATE_OFF
self._percentage = 0
self._preset_mode = None
async def async_set_percentage(self, percentage: int) -> None:
"""Set the percentage speed of the fan."""

View File

@@ -88,7 +88,10 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
# Handle turning to temp mode
if ATTR_COLOR_TEMP in kwargs:
color_tmp = mired_to_kelvin(int(kwargs[ATTR_COLOR_TEMP]))
# Handle temp conversion mireds -> kelvin being slightly outside of valid range
kelvin = mired_to_kelvin(int(kwargs[ATTR_COLOR_TEMP]))
kelvin_range = self.device.valid_temperature_range
color_tmp = max(kelvin_range.min, min(kelvin_range.max, kelvin))
_LOGGER.debug("Changing color temp to %s", color_tmp)
await self.device.set_color_temp(
color_tmp, brightness=brightness, transition=transition

View File

@@ -78,4 +78,4 @@ class VelbusCover(VelbusEntity, CoverEntity):
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
self._channel.set_position(100 - kwargs[ATTR_POSITION])
await self._channel.set_position(100 - kwargs[ATTR_POSITION])

View File

@@ -14,7 +14,6 @@ from miio import (
AirHumidifierMiot,
AirHumidifierMjjsq,
AirPurifier,
AirPurifierMB4,
AirPurifierMiot,
CleaningDetails,
CleaningSummary,
@@ -23,10 +22,8 @@ from miio import (
DNDStatus,
Fan,
Fan1C,
FanMiot,
FanP5,
FanP9,
FanP10,
FanP11,
FanZA5,
RoborockVacuum,
Timer,
@@ -52,7 +49,6 @@ from .const import (
KEY_DEVICE,
MODEL_AIRFRESH_A1,
MODEL_AIRFRESH_T2017,
MODEL_AIRPURIFIER_3C,
MODEL_FAN_1C,
MODEL_FAN_P5,
MODEL_FAN_P9,
@@ -111,10 +107,10 @@ AIR_MONITOR_PLATFORMS = [Platform.AIR_QUALITY, Platform.SENSOR]
MODEL_TO_CLASS_MAP = {
MODEL_FAN_1C: Fan1C,
MODEL_FAN_P10: FanP10,
MODEL_FAN_P11: FanP11,
MODEL_FAN_P9: FanMiot,
MODEL_FAN_P10: FanMiot,
MODEL_FAN_P11: FanMiot,
MODEL_FAN_P5: FanP5,
MODEL_FAN_P9: FanP9,
MODEL_FAN_ZA5: FanZA5,
}
@@ -314,8 +310,6 @@ async def async_create_miio_device_and_coordinator(
device = AirHumidifier(host, token, model=model)
migrate = True
# Airpurifiers and Airfresh
elif model == MODEL_AIRPURIFIER_3C:
device = AirPurifierMB4(host, token)
elif model in MODELS_PURIFIER_MIOT:
device = AirPurifierMiot(host, token)
elif model.startswith("zhimi.airpurifier."):

View File

@@ -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.5", "python-miio==0.5.10"],
"requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.11"],
"codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"],
"zeroconf": ["_miio._udp.local."],
"iot_class": "local_polling",

View File

@@ -51,7 +51,7 @@ DEFAULT_URL = "ws://localhost:3000"
TITLE = "Z-Wave JS"
ADDON_SETUP_TIMEOUT = 5
ADDON_SETUP_TIMEOUT_ROUNDS = 4
ADDON_SETUP_TIMEOUT_ROUNDS = 40
CONF_EMULATE_HARDWARE = "emulate_hardware"
CONF_LOG_LEVEL = "log_level"
SERVER_VERSION_TIMEOUT = 10

View File

@@ -3,7 +3,7 @@
"name": "Z-Wave JS",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
"requirements": ["zwave-js-server-python==0.35.1"],
"requirements": ["zwave-js-server-python==0.35.2"],
"codeowners": ["@home-assistant/z-wave"],
"dependencies": ["usb", "http", "websocket_api"],
"iot_class": "local_push",

View File

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

View File

@@ -81,7 +81,7 @@ DHCP: list[dict[str, str | bool]] = [
{'domain': 'samsungtv', 'macaddress': '4844F7*'},
{'domain': 'samsungtv', 'macaddress': '8CEA48*'},
{'domain': 'screenlogic', 'registered_devices': True},
{'domain': 'screenlogic', 'hostname': 'pentair: *', 'macaddress': '00C033*'},
{'domain': 'screenlogic', 'hostname': 'pentair*', 'macaddress': '00C033*'},
{'domain': 'sense', 'hostname': 'sense-*', 'macaddress': '009D6B*'},
{'domain': 'sense', 'hostname': 'sense-*', 'macaddress': 'DCEFCA*'},
{'domain': 'sense', 'hostname': 'sense-*', 'macaddress': 'A4D578*'},

View File

@@ -6,6 +6,7 @@ import asyncio
from collections.abc import Awaitable, Iterable, Mapping, MutableMapping
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum, auto
import functools as ft
import logging
import math
@@ -207,6 +208,19 @@ class EntityCategory(StrEnum):
SYSTEM = "system"
class EntityPlatformState(Enum):
"""The platform state of an entity."""
# Not Added: Not yet added to a platform, polling updates are written to the state machine
NOT_ADDED = auto()
# Added: Added to a platform, polling updates are written to the state machine
ADDED = auto()
# Removed: Removed from a platform, polling updates are not written to the state machine
REMOVED = auto()
def convert_to_entity_category(
value: EntityCategory | str | None, raise_report: bool = True
) -> EntityCategory | None:
@@ -294,7 +308,7 @@ class Entity(ABC):
_context_set: datetime | None = None
# If entity is added to an entity platform
_added = False
_platform_state = EntityPlatformState.NOT_ADDED
# Entity Properties
_attr_assumed_state: bool = False
@@ -553,6 +567,10 @@ class Entity(ABC):
@callback
def _async_write_ha_state(self) -> None:
"""Write the state to the state machine."""
if self._platform_state == EntityPlatformState.REMOVED:
# Polling returned after the entity has already been removed
return
if self.registry_entry and self.registry_entry.disabled_by:
if not self._disabled_reported:
self._disabled_reported = True
@@ -758,7 +776,7 @@ class Entity(ABC):
parallel_updates: asyncio.Semaphore | None,
) -> None:
"""Start adding an entity to a platform."""
if self._added:
if self._platform_state == EntityPlatformState.ADDED:
raise HomeAssistantError(
f"Entity {self.entity_id} cannot be added a second time to an entity platform"
)
@@ -766,7 +784,7 @@ class Entity(ABC):
self.hass = hass
self.platform = platform
self.parallel_updates = parallel_updates
self._added = True
self._platform_state = EntityPlatformState.ADDED
@callback
def add_to_platform_abort(self) -> None:
@@ -774,7 +792,7 @@ class Entity(ABC):
self.hass = None # type: ignore[assignment]
self.platform = None
self.parallel_updates = None
self._added = False
self._platform_state = EntityPlatformState.NOT_ADDED
async def add_to_platform_finish(self) -> None:
"""Finish adding an entity to a platform."""
@@ -792,12 +810,12 @@ class Entity(ABC):
If the entity doesn't have a non disabled entry in the entity registry,
or if force_remove=True, its state will be removed.
"""
if self.platform and not self._added:
if self.platform and self._platform_state != EntityPlatformState.ADDED:
raise HomeAssistantError(
f"Entity {self.entity_id} async_remove called twice"
)
self._added = False
self._platform_state = EntityPlatformState.REMOVED
if self._on_remove is not None:
while self._on_remove:

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