Compare commits

..

101 Commits

Author SHA1 Message Date
Franck Nijhof b1fb77cb4d 2024.4.1 (#114934) 2024-04-05 16:18:02 +02:00
Joost Lekkerkerker 95606135a6 Fix ROVA validation (#114938)
* Fix ROVA validation

* Fix ROVA validation
2024-04-05 14:53:21 +02:00
Aidan Timson 47d9879c0c Pin systembridgemodels to 4.0.4 (#114842) 2024-04-05 14:53:17 +02:00
Franck Nijhof e3c111b1dd Bump version to 2024.4.1 2024-04-05 12:34:07 +02:00
Joost Lekkerkerker 9937743863 Fix cast dashboard in media browser (#114924) 2024-04-05 12:33:49 +02:00
Joost Lekkerkerker ed3daed869 Create right import issues in Downloader (#114922)
* Create right import issues in Downloader

* Create right import issues in Downloader

* Create right import issues in Downloader

* Create right import issues in Downloader

* Fix

* Fix

* Fix

* Fix

* Apply suggestions from code review

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

* Fix

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2024-04-05 12:33:46 +02:00
Åke Strandberg 5d5dc24b33 Show correct model string in myuplink (#114921) 2024-04-05 12:33:43 +02:00
J. Nick Koston c39d6f0730 Reduce august polling frequency (#114904)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2024-04-05 12:33:40 +02:00
J. Nick Koston 87ffd5ac56 Ensure all tables have the default table args in the db_schema (#114895) 2024-04-05 12:33:36 +02:00
Bram Kragten 71877fdeda Update frontend to 20240404.1 (#114890) 2024-04-05 12:33:33 +02:00
Robert Svensson 2434a22e4e Fix Axis reconfigure step not providing protocols as alternatives but as string (#114889) 2024-04-05 12:33:30 +02:00
Jeef 618fa08ab2 Bump weatherflow4py to 0.2.20 (#114888) 2024-04-05 12:33:27 +02:00
Robert Svensson 96003e3562 Fix Axis camera platform support HTTPS (#114886) 2024-04-05 12:33:24 +02:00
Bram Kragten 411e55d059 Update frontend to 20240404.0 (#114859) 2024-04-05 12:33:21 +02:00
Joost Lekkerkerker 58533f02af Fix Downloader YAML import (#114844) 2024-04-05 12:33:18 +02:00
Joost Lekkerkerker aa14793479 Avoid blocking IO in downloader initialization (#114841)
* Avoid blocking IO in downloader initialization

* Avoid blocking IO in downloader initialization
2024-04-05 12:33:15 +02:00
J. Nick Koston 0191d3e41b Refactor ConfigStore to avoid needing to pass config_dir (#114827)
Co-authored-by: Erik <erik@montnemery.com>
2024-04-05 12:33:12 +02:00
tronikos 319f76cdc8 Bump opower to 0.4.3 (#114826)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-04-05 12:33:09 +02:00
J. Nick Koston 530725bbfa Handle ambiguous script actions by using action map order (#114825) 2024-04-05 12:33:06 +02:00
Lex Li d8ae7d6955 Fix type cast in snmp (#114795) 2024-04-05 12:33:03 +02:00
cdheiser 3d0bafbdc9 Fix Lutron light brightness values (#114794)
Fix brightness values in light.py

Bugfix to set the brightness to 0-100 which is what Lutron expects.
2024-04-05 12:33:00 +02:00
Aaron Bach ef8e54877f Fix unhandled KeyError during Notion setup (#114787) 2024-04-05 12:32:57 +02:00
Manuel Dipolt a39e1a6428 Update romy to 0.0.10 (#114785) 2024-04-05 12:32:53 +02:00
Marc Mueller 450be67406 Update romy to 0.0.9 (#114360) 2024-04-05 10:14:00 +02:00
Åke Strandberg 25289e0ca1 Bump myuplink dependency to 0.6.0 (#114767) 2024-04-05 10:06:39 +02:00
Álvaro Fernández Rojas d983fa6da7 Update aioairzone-cloud to v0.4.7 (#114761) 2024-04-05 10:06:35 +02:00
Franck Nijhof b61397656c 2024.4.0 (#114764) 2024-04-03 20:38:11 +02:00
Jan-Philipp Benecke 590546a9a5 Use setup_test_component_platform helper for sensor entity component tests instead of hass.components (#114316)
* Use `setup_test_component_platform` helper for sensor entity component tests instead of `hass.components`

* Missing file

* Fix import

* Remove invalid device class
2024-04-03 20:00:56 +02:00
IngoK1 9ba4d26abd Fix for Sonos URL encoding problem #102557 (#109518)
* Fix for URL encoding problem #102557

Fixes the problem "Cannot play media with spaces in folder names to Sonos #102557" removing the encoding of the strings in the music library.

* Fix type casting problem

* Update media_browser.py to fix pr check findings

Added required casting for all unquote statements to avoid further casting findings in the pr checks

* Update media_browser.py

Checked on linting, lets give it another try

* Update media_browser.py

Updated ruff run

* Update media_browser.py - added version run through ruff

* Update media_browser.py - ruff changes

* Apply ruff formatting

* Update homeassistant/components/sonos/media_browser.py

Co-authored-by: jjlawren <jjlawren@users.noreply.github.com>

* Update homeassistant/components/sonos/media_browser.py

Co-authored-by: jjlawren <jjlawren@users.noreply.github.com>

* Update homeassistant/components/sonos/media_browser.py

Co-authored-by: jjlawren <jjlawren@users.noreply.github.com>

* Update homeassistant/components/sonos/media_browser.py

Co-authored-by: jjlawren <jjlawren@users.noreply.github.com>

---------

Co-authored-by: computeq-admin <51021172+computeq-admin@users.noreply.github.com>
Co-authored-by: Jason Lawrence <jjlawren@users.noreply.github.com>
2024-04-03 19:31:02 +02:00
Franck Nijhof aa33da546d Bump version to 2024.4.0 2024-04-03 19:09:39 +02:00
Franck Nijhof 3845523a27 Bump version to 2024.4.0b9 2024-04-03 17:55:24 +02:00
Michael 6a7fad0228 Fix Synology DSM setup in case no Surveillance Station permission (#114757) 2024-04-03 17:55:12 +02:00
Bram Kragten 33f07ce035 Update frontend to 20240403.1 (#114756) 2024-04-03 17:55:09 +02:00
Michael Hansen 4302c5c273 Bump intents (#114755) 2024-04-03 17:55:05 +02:00
Robert Resch b2df1b1c03 Allow passing area/device/entity IDs to floor_id and floor_name (#114748) 2024-04-03 17:55:01 +02:00
Franck Nijhof 0aa134459b Bump version to 2024.4.0b8 2024-04-03 15:35:53 +02:00
Bram Kragten 0ca3700c16 Update frontend to 20240403.0 (#114747) 2024-04-03 15:35:40 +02:00
Joost Lekkerkerker 35ff633d99 Avoid blocking IO in downloader config flow (#114741) 2024-04-03 15:35:36 +02:00
Joost Lekkerkerker 7a2f6ce430 Fix Downloader config flow (#114718) 2024-04-03 15:35:32 +02:00
David F. Mulcahey 7cb603a226 Import zha quirks in the executor (#114685) 2024-04-03 15:35:29 +02:00
Jonas Fors Lellky 43562289e4 Bump flexit_bacnet to 2.2.1 (#114641) 2024-04-03 15:35:26 +02:00
Lenn 79fa7caa41 Rename Motionblinds BLE integration to Motionblinds Bluetooth (#114584) 2024-04-03 15:35:20 +02:00
Franck Nijhof 8bdb27c88b Bump version to 2024.4.0b7 2024-04-03 00:14:07 +02:00
Bram Kragten f676448f27 Update frontend to 20240402.2 (#114683) 2024-04-03 00:13:57 +02:00
J. Nick Koston 639c4a843b Avoid trying to load platform that are known to not exist in async_prepare_setup_platform (#114659) 2024-04-03 00:13:53 +02:00
G Johansson 02dee34338 Bump holidays to 0.46 (#114657) 2024-04-03 00:13:49 +02:00
Maciej Bieniek 4e0290ce0e Add missing state to the Tractive tracker state sensor (#114654)
Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
2024-04-03 00:13:45 +02:00
Robert Svensson fa2f49693c Bump aiounifi to v74 (#114649) 2024-04-03 00:13:39 +02:00
Pete Sage 2ce784105d Fix Sonos play imported playlists (#113934) 2024-04-03 00:13:35 +02:00
Franck Nijhof 85fb4a27a3 Bump version to 2024.4.0b6 2024-04-02 17:35:01 +02:00
Bram Kragten 8cbedbe26b Update frontend to 20240402.1 (#114646) 2024-04-02 17:34:29 +02:00
Steven B 5bd52da13a Bump ring_doorbell integration to 0.8.9 (#114631) 2024-04-02 17:33:24 +02:00
dotvav d53848aae4 Fix Overkiz Hitachi OVP air-to-air heat pump (#114611) 2024-04-02 17:23:51 +02:00
puddly 4e0d6f287e Reduce ZHA OTA logbook entries and extraneous updates (#114591) 2024-04-02 17:23:45 +02:00
Franck Nijhof 5af5f3694e Bump version to 2024.4.0b5 2024-04-02 12:28:20 +02:00
Bram Kragten b539b25682 Update frontend to 20240402.0 (#114627) 2024-04-02 12:28:07 +02:00
Fexiven ca31479d29 Fix Starlink integration startup issue (#114615) 2024-04-02 12:28:04 +02:00
Franck Nijhof 92dfec3c98 Add floor selector (#114614) 2024-04-02 12:28:00 +02:00
max2697 230c29edbe Bump opower to 0.4.2 (#114608) 2024-04-02 12:27:57 +02:00
Jack Boswell 559fe65471 Catch potential ValueError when getting or setting Starlink sleep values (#114607) 2024-04-02 12:27:54 +02:00
mkmer 384d10a51d Add diagnostic platform to Whirlpool (#114578)
* Add diagnostic platform and tests

* lowercase variable

* Correc doc string
2024-04-02 12:27:50 +02:00
Brett Adams e5a620545c Fix battery heater in Tessie (#114568) 2024-04-02 12:27:47 +02:00
Maciej Bieniek 7b84e86f89 Improve Shelly RPC device update progress (#114566)
Co-authored-by: Shay Levy <levyshay1@gmail.com>
Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
2024-04-02 12:27:44 +02:00
Joost Lekkerkerker 18b6de567d Bump roombapy to 1.8.1 (#114478)
* Bump roombapy to 1.7.0

* Bump

* Bump

* Fix
2024-04-02 12:27:40 +02:00
Pete Sage a6076a0d33 Display sonos album title with URL encoding (#113693)
* unescape the title

When extracting the title from the item_id, it needs to be unescaped.

* sort imports
2024-04-02 12:27:36 +02:00
Paulus Schoutsen 7164993562 Bump version to 2024.4.0b4 2024-04-02 01:51:42 +00:00
mkmer bc21836e7e Bump whirlpool-sixth-sense to 0.18.7 (#114606)
Bump sixth-sense to 0.18.7
2024-04-02 01:51:35 +00:00
J. Nick Koston 52612b10fd Avoid storing raw extracted traceback in system_log (#114603)
This is never actually used and takes up quite a bit of ram
2024-04-02 01:51:35 +00:00
J. Nick Koston 623d85ecaa Fix memory leak when importing a platform fails (#114602)
* Fix memory leak when importing a platform fails

re-raising ImportError would trigger a memory leak

* fixes, coverage

* Apply suggestions from code review
2024-04-02 01:51:33 +00:00
J. Nick Koston 43631d5944 Add missing platforms_exist guard to check_config (#114600)
* Add missing platforms_exist guard to check_config

related issue #112811

When the exception hits, the config will end up being saved in the traceback
so the memory is never released.

This matches the check_config code to homeassistant.config to avoid having
the exception thrown.

* patch

* merge branch
2024-04-02 01:51:33 +00:00
J. Nick Koston 112aab47fb Bump zeroconf to 0.132.0 (#114596)
changelog: https://github.com/python-zeroconf/python-zeroconf/compare/0.131.0...0.132.0
2024-04-02 01:51:32 +00:00
Martin Hjelmare ea13f102e0 Fix reolink media source data access (#114593)
* Add test

* Fix reolink media source data access
2024-04-02 01:51:31 +00:00
jjlawren bb33725e7f Bump plexapi to 4.15.11 (#114581) 2024-04-02 01:51:31 +00:00
Michael bd6890ab83 Filter out ignored entries in ssdp step of AVM Fritz!SmartHome (#114574)
filter out ignored entries in ssdp step
2024-04-02 01:51:30 +00:00
Michael 25c611ffc4 Reduce usage of executer threads in AVM Fritz!Tools (#114570)
* call entity state update calls in one executer task

* remove not needed wrapping

* mark as "non-public" method

* add guard against changes on _entity_update_functions
2024-04-02 01:51:29 +00:00
Maikel Punie fc24b61859 Bump velbusaio to 2024.4.0 (#114569)
Bump valbusaio to 2024.4.0
2024-04-02 01:51:28 +00:00
Joost Lekkerkerker 71588b5c22 Fix wrong icons (#114567)
* Fix wrong icons

* Fix wrong icons
2024-04-02 01:51:27 +00:00
Robert Svensson 14dfb6a255 Bump axis to v60 (#114544)
* Improve Axis MQTT support

* Bump axis to v60
2024-04-02 01:51:27 +00:00
G Johansson ef97255d9c Fix server update from breaking setup in Speedtest.NET (#114524) 2024-04-02 01:51:26 +00:00
J. Nick Koston e8afdd67d0 Fix workday doing blocking I/O in the event loop (#114492) 2024-04-02 01:51:25 +00:00
J. Nick Koston 008e4413b5 Fix late load of anyio doing blocking I/O in the event loop (#114491)
* Fix late load of anyio doing blocking I/O in the event loop

httpx loads anyio which loads the asyncio backend in the event loop
as soon as httpx makes the first request

* tweak
2024-04-02 01:51:24 +00:00
dotvav c373d40e34 Fix Overkiz Hitachi OVP air-to-air heat pump (#114487)
Unpack command parameters instead of passing a list
2024-04-02 01:51:24 +00:00
J. Nick Koston bdf51553ef Improve sonos test synchronization (#114468) 2024-04-02 01:51:23 +00:00
Michael Hansen f2edc15687 Add initial support for floors to intents (#114456)
* Add initial support for floors to intents

* Fix climate intent

* More tests

* No return value

* Add requested changes

* Reuse event handler
2024-04-02 01:51:22 +00:00
J. Nick Koston 286a09d737 Mark executor jobs as background unless created from a tracked task (#114450)
* Mark executor jobs as background unless created from a tracked task

If the current task is not tracked the executor job should not
be a background task to avoid delaying startup and shutdown.

Currently any executor job created in a untracked task or
background task would end up being tracked and delaying
startup/shutdown

* import exec has the same issue

* Avoid tracking import executor jobs

There is no reason to track these jobs as they are always awaited
and we do not want to support fire and forget import executor jobs

* fix xiaomi_miio

* lots of fire time changed without background await

* revert changes moved to other PR

* more

* more

* more

* m

* m

* p

* fix fire and forget tests

* scrape

* sonos

* system

* more

* capture callback before block

* coverage

* more

* more races

* more races

* more

* missed some

* more fixes

* missed some more

* fix

* remove unneeded

* one more race

* two
2024-04-02 01:51:21 +00:00
Shay Levy e8ee2fd25c Cleanup Shelly RGBW light entities (#114410) 2024-04-02 01:51:21 +00:00
Franck Nijhof 11b8b01cde Bump version to 2024.4.0b3 2024-03-29 22:22:45 +01:00
Paul Bottein 4f761c25d8 Update frontend to 20240329.1 (#114459) 2024-03-29 22:22:37 +01:00
J. Nick Koston 953ceb0d8d Avoid tracking import executor jobs (#114453) 2024-03-29 22:22:33 +01:00
Franck Nijhof e53672250f Bump version to 2024.4.0b2 2024-03-29 19:35:52 +01:00
Paul Bottein 84901f1983 Update frontend to 20240329.0 (#114452) 2024-03-29 19:35:44 +01:00
Steven Looman e4d973e8a2 Bump async-upnp-client to 0.38.3 (#114447) 2024-03-29 19:35:40 +01:00
epenet cdd7ce435a Log warnings in Renault initialisation (#114445) 2024-03-29 19:35:37 +01:00
Mick Vleeshouwer c7ce53cc49 Bump pyoverkiz to 1.13.9 (#114442) 2024-03-29 19:35:33 +01:00
Steven B db7d0a0ee9 Bump python-ring-doorbell to 0.8.8 (#114431)
* Bump ring_doorbell to 0.8.8

* Fix intercom history test for new library version

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-03-29 19:35:30 +01:00
J. Nick Koston 906febadef Cleanup some plex tasks that delayed startup (#114418) 2024-03-29 19:35:27 +01:00
J. Nick Koston bc740f95c9 Avoid concurrent radio operations with powerview hubs (#114399)
Co-authored-by: kingy444 <toddlesking4@hotmail.com>
2024-03-29 19:35:23 +01:00
Alexey ALERT Rubashёff bf4e527f44 Add overkiz bottom tank water temperature and core control water temperature for Atlantic Water Heater (#114186)
* Adds bottom tank water temperature and core conrol water temperature sensors for Atlantic water heater

* Update homeassistant/components/overkiz/sensor.py

Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>

* Update homeassistant/components/overkiz/sensor.py

Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>

---------

Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2024-03-29 19:35:20 +01:00
Alexey ALERT Rubashёff 35e582a240 Add overkiz water targets temperature numbers for Atlantic water heater (#114185)
* Adds water targets temperature numbers for Atlantic water heater

* Update homeassistant/components/overkiz/number.py

Co-authored-by: Mick Vleeshouwer <mick@imick.nl>

* Update homeassistant/components/overkiz/number.py

Co-authored-by: Mick Vleeshouwer <mick@imick.nl>

* ruff formatting reverted

* Update homeassistant/components/overkiz/number.py

Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>

* Update homeassistant/components/overkiz/number.py

Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>

* changed command hardcode to a constant

---------

Co-authored-by: Mick Vleeshouwer <mick@imick.nl>
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2024-03-29 19:35:17 +01:00
Alexey ALERT Rubashёff 65d25bd780 Add overkiz heating status, absence mode, and boost mode binary sensors for Atlantic Water Heater (#114184)
* Adds heating status, absense mode, and boost mode binary sensors for Atlantic water heater

* Renamed absence mode and boost mode binary sensors

* Update homeassistant/components/overkiz/binary_sensor.py

Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>

* Update homeassistant/components/overkiz/binary_sensor.py

Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>

* Update homeassistant/components/overkiz/binary_sensor.py

Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>

---------

Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2024-03-29 19:35:13 +01:00
Jeremy TRUFIER b8a2c14813 Follow real AtlanticPassAPCZoneControlZone physical mode on Overkiz (HEAT, COOL or HEAT_COOL) (#111830)
* Support HEAT_COOL when mode is Auto on overkiz AtlanticPassAPCZoneControlZone

* Refactor ZoneControlZone to simplify usic by only using a single hvac mode

* Fix linting issues

* Makes more sense to use halves there

* Fix PR feedback
2024-03-29 19:35:09 +01:00
198 changed files with 2771 additions and 1006 deletions
+5
View File
@@ -93,6 +93,11 @@ from .util.async_ import create_eager_task
from .util.logging import async_activate_log_queue_handler
from .util.package import async_get_user_site, is_virtual_env
with contextlib.suppress(ImportError):
# Ensure anyio backend is imported to avoid it being imported in the event loop
from anyio._backends import _asyncio # noqa: F401
if TYPE_CHECKING:
from .runner import RuntimeConfig
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_push",
"loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.4.6"]
"requirements": ["aioairzone-cloud==0.4.7"]
}
+20 -1
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
from datetime import datetime
from functools import partial
import logging
from time import monotonic
from aiohttp import ClientError
from yalexs.activity import Activity, ActivityType
@@ -26,9 +27,11 @@ _LOGGER = logging.getLogger(__name__)
ACTIVITY_STREAM_FETCH_LIMIT = 10
ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500
INITIAL_LOCK_RESYNC_TIME = 60
# If there is a storm of activity (ie lock, unlock, door open, door close, etc)
# we want to debounce the updates so we don't hammer the activity api too much.
ACTIVITY_DEBOUNCE_COOLDOWN = 3
ACTIVITY_DEBOUNCE_COOLDOWN = 4
@callback
@@ -62,6 +65,7 @@ class ActivityStream(AugustSubscriberMixin):
self.pubnub = pubnub
self._update_debounce: dict[str, Debouncer] = {}
self._update_debounce_jobs: dict[str, HassJob] = {}
self._start_time: float | None = None
@callback
def _async_update_house_id_later(self, debouncer: Debouncer, _: datetime) -> None:
@@ -70,6 +74,7 @@ class ActivityStream(AugustSubscriberMixin):
async def async_setup(self) -> None:
"""Token refresh check and catch up the activity stream."""
self._start_time = monotonic()
update_debounce = self._update_debounce
update_debounce_jobs = self._update_debounce_jobs
for house_id in self._house_ids:
@@ -140,11 +145,25 @@ class ActivityStream(AugustSubscriberMixin):
debouncer = self._update_debounce[house_id]
debouncer.async_schedule_call()
# Schedule two updates past the debounce time
# to ensure we catch the case where the activity
# api does not update right away and we need to poll
# it again. Sometimes the lock operator or a doorbell
# will not show up in the activity stream right away.
# Only do additional polls if we are past
# the initial lock resync time to avoid a storm
# of activity at setup.
if (
not self._start_time
or monotonic() - self._start_time < INITIAL_LOCK_RESYNC_TIME
):
_LOGGER.debug(
"Skipping additional updates due to ongoing initial lock resync time"
)
return
_LOGGER.debug("Scheduling additional updates for house id %s", house_id)
job = self._update_debounce_jobs[house_id]
for step in (1, 2):
future_updates.append(
+1 -1
View File
@@ -40,7 +40,7 @@ ATTR_OPERATION_TAG = "tag"
# Limit battery, online, and hardware updates to hourly
# in order to reduce the number of api requests and
# avoid hitting rate limits
MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=1)
MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=24)
# Activity needs to be checked more frequently as the
# doorbell motion and rings are included here
+15 -18
View File
@@ -49,9 +49,17 @@ class AugustSubscriberMixin:
"""Call the refresh method."""
self._hass.async_create_task(self._async_refresh(now), eager_start=True)
@callback
def _async_cancel_update_interval(self, _: Event | None = None) -> None:
"""Cancel the scheduled update."""
if self._unsub_interval:
self._unsub_interval()
self._unsub_interval = None
@callback
def _async_setup_listeners(self) -> None:
"""Create interval and stop listeners."""
self._async_cancel_update_interval()
self._unsub_interval = async_track_time_interval(
self._hass,
self._async_scheduled_refresh,
@@ -59,17 +67,12 @@ class AugustSubscriberMixin:
name="august refresh",
)
@callback
def _async_cancel_update_interval(_: Event) -> None:
self._stop_interval = None
if self._unsub_interval:
self._unsub_interval()
self._stop_interval = self._hass.bus.async_listen(
EVENT_HOMEASSISTANT_STOP,
_async_cancel_update_interval,
run_immediately=True,
)
if not self._stop_interval:
self._stop_interval = self._hass.bus.async_listen(
EVENT_HOMEASSISTANT_STOP,
self._async_cancel_update_interval,
run_immediately=True,
)
@callback
def async_unsubscribe_device_id(
@@ -82,13 +85,7 @@ class AugustSubscriberMixin:
if self._subscriptions:
return
if self._unsub_interval:
self._unsub_interval()
self._unsub_interval = None
if self._stop_interval:
self._stop_interval()
self._stop_interval = None
self._async_cancel_update_interval()
@callback
def async_signal_device_id_update(self, device_id: str) -> None:
+8 -8
View File
@@ -56,6 +56,7 @@ class AxisCamera(AxisEntity, MjpegCamera):
mjpeg_url=self.mjpeg_source,
still_image_url=self.image_source,
authentication=HTTP_DIGEST_AUTHENTICATION,
verify_ssl=False,
unique_id=f"{hub.unique_id}-camera",
)
@@ -74,16 +75,18 @@ class AxisCamera(AxisEntity, MjpegCamera):
Additionally used when device change IP address.
"""
proto = self.hub.config.protocol
host = self.hub.config.host
port = self.hub.config.port
image_options = self.generate_options(skip_stream_profile=True)
self._still_image_url = (
f"http://{self.hub.config.host}:{self.hub.config.port}/axis-cgi"
f"/jpg/image.cgi{image_options}"
f"{proto}://{host}:{port}/axis-cgi/jpg/image.cgi{image_options}"
)
mjpeg_options = self.generate_options()
self._mjpeg_url = (
f"http://{self.hub.config.host}:{self.hub.config.port}/axis-cgi"
f"/mjpg/video.cgi{mjpeg_options}"
f"{proto}://{host}:{port}/axis-cgi/mjpg/video.cgi{mjpeg_options}"
)
stream_options = self.generate_options(add_video_codec_h264=True)
@@ -95,10 +98,7 @@ class AxisCamera(AxisEntity, MjpegCamera):
self.hub.additional_diagnostics["camera_sources"] = {
"Image": self._still_image_url,
"MJPEG": self._mjpeg_url,
"Stream": (
f"rtsp://user:pass@{self.hub.config.host}/axis-media"
f"/media.amp{stream_options}"
),
"Stream": (f"rtsp://user:pass@{host}/axis-media/media.amp{stream_options}"),
}
@property
+4 -7
View File
@@ -168,16 +168,13 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
self, entry_data: Mapping[str, Any], keep_password: bool
) -> ConfigFlowResult:
"""Re-run configuration step."""
protocol = entry_data.get(CONF_PROTOCOL, "http")
password = entry_data[CONF_PASSWORD] if keep_password else ""
self.discovery_schema = {
vol.Required(
CONF_PROTOCOL, default=entry_data.get(CONF_PROTOCOL, "http")
): str,
vol.Required(CONF_PROTOCOL, default=protocol): vol.In(PROTOCOL_CHOICES),
vol.Required(CONF_HOST, default=entry_data[CONF_HOST]): str,
vol.Required(CONF_USERNAME, default=entry_data[CONF_USERNAME]): str,
vol.Required(
CONF_PASSWORD,
default=entry_data[CONF_PASSWORD] if keep_password else "",
): str,
vol.Required(CONF_PASSWORD, default=password): str,
vol.Required(CONF_PORT, default=entry_data[CONF_PORT]): int,
}
@@ -12,6 +12,7 @@ from homeassistant.const import (
CONF_NAME,
CONF_PASSWORD,
CONF_PORT,
CONF_PROTOCOL,
CONF_TRIGGER_TIME,
CONF_USERNAME,
)
@@ -31,6 +32,7 @@ class AxisConfig:
entry: ConfigEntry
protocol: str
host: str
port: int
username: str
@@ -54,6 +56,7 @@ class AxisConfig:
options = config_entry.options
return cls(
entry=config_entry,
protocol=config.get(CONF_PROTOCOL, "http"),
host=config[CONF_HOST],
username=config[CONF_USERNAME],
password=config[CONF_PASSWORD],
+3 -2
View File
@@ -116,7 +116,7 @@ class AxisHub:
if status.status.state == ClientState.ACTIVE:
self.config.entry.async_on_unload(
await mqtt.async_subscribe(
hass, f"{self.api.vapix.serial_number}/#", self.mqtt_message
hass, f"{status.config.device_topic_prefix}/#", self.mqtt_message
)
)
@@ -124,7 +124,8 @@ class AxisHub:
def mqtt_message(self, message: ReceiveMessage) -> None:
"""Receive Axis MQTT message."""
self.disconnect_from_stream()
if message.topic.endswith("event/connection"):
return
event = mqtt_json_to_event(message.payload)
self.api.event.handler(event)
+1 -1
View File
@@ -26,7 +26,7 @@
"iot_class": "local_push",
"loggers": ["axis"],
"quality_scale": "platinum",
"requirements": ["axis==59"],
"requirements": ["axis==60"],
"ssdp": [
{
"manufacturer": "AXIS"
@@ -58,6 +58,7 @@ class GetTemperatureIntent(intent.IntentHandler):
raise intent.NoStatesMatchedError(
name=entity_text or entity_name,
area=area_name or area_id,
floor=None,
domains={DOMAIN},
device_classes=None,
)
@@ -75,6 +76,7 @@ class GetTemperatureIntent(intent.IntentHandler):
raise intent.NoStatesMatchedError(
name=entity_name,
area=None,
floor=None,
domains={DOMAIN},
device_classes=None,
)
@@ -34,6 +34,7 @@ from homeassistant.helpers import (
area_registry as ar,
device_registry as dr,
entity_registry as er,
floor_registry as fr,
intent,
start,
template,
@@ -163,7 +164,12 @@ class DefaultAgent(AbstractConversationAgent):
self.hass.bus.async_listen(
ar.EVENT_AREA_REGISTRY_UPDATED,
self._async_handle_area_registry_changed,
self._async_handle_area_floor_registry_changed,
run_immediately=True,
)
self.hass.bus.async_listen(
fr.EVENT_FLOOR_REGISTRY_UPDATED,
self._async_handle_area_floor_registry_changed,
run_immediately=True,
)
self.hass.bus.async_listen(
@@ -696,10 +702,13 @@ class DefaultAgent(AbstractConversationAgent):
return lang_intents
@core.callback
def _async_handle_area_registry_changed(
self, event: core.Event[ar.EventAreaRegistryUpdatedData]
def _async_handle_area_floor_registry_changed(
self,
event: core.Event[
ar.EventAreaRegistryUpdatedData | fr.EventFloorRegistryUpdatedData
],
) -> None:
"""Clear area area cache when the area registry has changed."""
"""Clear area/floor list cache when the area registry has changed."""
self._slot_lists = None
@core.callback
@@ -773,6 +782,8 @@ class DefaultAgent(AbstractConversationAgent):
# Default name
entity_names.append((state.name, state.name, context))
_LOGGER.debug("Exposed entities: %s", entity_names)
# Expose all areas.
#
# We pass in area id here with the expectation that no two areas will
@@ -788,11 +799,25 @@ class DefaultAgent(AbstractConversationAgent):
area_names.append((alias, area.id))
_LOGGER.debug("Exposed entities: %s", entity_names)
# Expose all floors.
#
# We pass in floor id here with the expectation that no two floors will
# share the same name or alias.
floors = fr.async_get(self.hass)
floor_names = []
for floor in floors.async_list_floors():
floor_names.append((floor.name, floor.floor_id))
if floor.aliases:
for alias in floor.aliases:
if not alias.strip():
continue
floor_names.append((alias, floor.floor_id))
self._slot_lists = {
"area": TextSlotList.from_tuples(area_names, allow_template=False),
"name": TextSlotList.from_tuples(entity_names, allow_template=False),
"floor": TextSlotList.from_tuples(floor_names, allow_template=False),
}
return self._slot_lists
@@ -953,6 +978,10 @@ def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str
# area only
return ErrorKey.NO_AREA, {"area": unmatched_area}
if unmatched_floor := unmatched_text.get("floor"):
# floor only
return ErrorKey.NO_FLOOR, {"floor": unmatched_floor}
# Area may still have matched
matched_area: str | None = None
if matched_area_entity := result.entities.get("area"):
@@ -1000,6 +1029,13 @@ def _get_no_states_matched_response(
"area": no_states_error.area,
}
if no_states_error.floor:
# domain in floor
return ErrorKey.NO_DOMAIN_IN_FLOOR, {
"domain": domain,
"floor": no_states_error.floor,
}
# domain only
return ErrorKey.NO_DOMAIN, {"domain": domain}
@@ -7,5 +7,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["hassil==1.6.1", "home-assistant-intents==2024.3.27"]
"requirements": ["hassil==1.6.1", "home-assistant-intents==2024.4.3"]
}
@@ -8,7 +8,7 @@
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
"iot_class": "local_push",
"loggers": ["async_upnp_client"],
"requirements": ["async-upnp-client==0.38.2", "getmac==0.9.4"],
"requirements": ["async-upnp-client==0.38.3", "getmac==0.9.4"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
@@ -8,7 +8,7 @@
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
"iot_class": "local_polling",
"quality_scale": "platinum",
"requirements": ["async-upnp-client==0.38.2"],
"requirements": ["async-upnp-client==0.38.3"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",
+44 -21
View File
@@ -11,7 +11,11 @@ import requests
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN,
HomeAssistant,
ServiceCall,
)
from homeassistant.data_entry_flow import FlowResultType
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
@@ -43,6 +47,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if DOMAIN not in config:
return True
hass.async_create_task(_async_import_config(hass, config))
return True
async def _async_import_config(hass: HomeAssistant, config: ConfigType) -> None:
"""Import the Downloader component from the YAML file."""
import_result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
@@ -51,28 +62,40 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
},
)
translation_key = "deprecated_yaml"
if (
import_result["type"] == FlowResultType.ABORT
and import_result["reason"] == "import_failed"
and import_result["reason"] != "single_instance_allowed"
):
translation_key = "import_failed"
async_create_issue(
hass,
DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2024.9.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key=translation_key,
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Downloader",
},
)
return True
async_create_issue(
hass,
DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2024.10.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="directory_does_not_exist",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Downloader",
"url": "/config/integrations/dashboard/add?domain=downloader",
},
)
else:
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2024.10.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Downloader",
},
)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -83,7 +106,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if not os.path.isabs(download_path):
download_path = hass.config.path(download_path)
if not os.path.isdir(download_path):
if not await hass.async_add_executor_job(os.path.isdir, download_path):
_LOGGER.error(
"Download path %s does not exist. File Downloader not active", download_path
)
@@ -46,19 +46,24 @@ class DownloaderConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_import(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult:
"""Handle a flow initiated by configuration file."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
return await self.async_step_user(user_input)
try:
await self._validate_input(user_input)
except DirectoryDoesNotExist:
return self.async_abort(reason="directory_does_not_exist")
return self.async_create_entry(title=DEFAULT_NAME, data=user_input)
async def _validate_input(self, user_input: dict[str, Any]) -> None:
"""Validate the user input if the directory exists."""
if not os.path.isabs(user_input[CONF_DOWNLOAD_DIR]):
download_path = self.hass.config.path(user_input[CONF_DOWNLOAD_DIR])
download_path = user_input[CONF_DOWNLOAD_DIR]
if not os.path.isabs(download_path):
download_path = self.hass.config.path(download_path)
if not os.path.isdir(download_path):
if not await self.hass.async_add_executor_job(os.path.isdir, download_path):
_LOGGER.error(
"Download path %s does not exist. File Downloader not active",
download_path,
@@ -37,13 +37,9 @@
}
},
"issues": {
"deprecated_yaml": {
"title": "The {integration_title} YAML configuration is being removed",
"description": "Configuring {integration_title} using YAML is being removed.\n\nYour configuration is already imported.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
},
"import_failed": {
"directory_does_not_exist": {
"title": "The {integration_title} failed to import",
"description": "The {integration_title} integration failed to import.\n\nPlease check the logs for more details."
"description": "The {integration_title} integration failed to import because the configured directory does not exist.\n\nEnsure the directory exists and restart Home Assistant to try again or remove the {integration_title} configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"services": {
"restart": "mdi:restart",
"start": "mdi:start",
"start": "mdi:play",
"stop": "mdi:stop"
}
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/flexit_bacnet",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["flexit_bacnet==2.1.0"]
"requirements": ["flexit_bacnet==2.2.1"]
}
+14 -9
View File
@@ -311,6 +311,17 @@ class FritzBoxTools(
)
return unregister_entity_updates
def _entity_states_update(self) -> dict:
"""Run registered entity update calls."""
entity_states = {}
for key in list(self._entity_update_functions):
if (update_fn := self._entity_update_functions.get(key)) is not None:
_LOGGER.debug("update entity %s", key)
entity_states[key] = update_fn(
self.fritz_status, self.data["entity_states"].get(key)
)
return entity_states
async def _async_update_data(self) -> UpdateCoordinatorDataType:
"""Update FritzboxTools data."""
entity_data: UpdateCoordinatorDataType = {
@@ -319,15 +330,9 @@ class FritzBoxTools(
}
try:
await self.async_scan_devices()
for key in list(self._entity_update_functions):
_LOGGER.debug("update entity %s", key)
entity_data["entity_states"][
key
] = await self.hass.async_add_executor_job(
self._entity_update_functions[key],
self.fritz_status,
self.data["entity_states"].get(key),
)
entity_data["entity_states"] = await self.hass.async_add_executor_job(
self._entity_states_update
)
if self.has_call_deflections:
entity_data[
"call_deflections"
@@ -141,7 +141,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="already_in_progress")
# update old and user-configured config entries
for entry in self._async_current_entries():
for entry in self._async_current_entries(include_ignore=False):
if entry.data[CONF_HOST] == host:
if uuid and not entry.unique_id:
self.hass.config_entries.async_update_entry(entry, unique_id=uuid)
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20240328.0"]
"requirements": ["home-assistant-frontend==20240404.1"]
}
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.45", "babel==2.13.1"]
"requirements": ["holidays==0.46", "babel==2.13.1"]
}
@@ -119,4 +119,5 @@ class PowerviewShadeButton(ShadeEntity, ButtonEntity):
async def async_press(self) -> None:
"""Handle the button press."""
await self.entity_description.press_action(self._shade)
async with self.coordinator.radio_operation_lock:
await self.entity_description.press_action(self._shade)
@@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
from datetime import timedelta
import logging
@@ -25,6 +26,10 @@ class PowerviewShadeUpdateCoordinator(DataUpdateCoordinator[PowerviewShadeData])
"""Initialize DataUpdateCoordinator to gather data for specific Powerview Hub."""
self.shades = shades
self.hub = hub
# The hub tends to crash if there are multiple radio operations at the same time
# but it seems to handle all other requests that do not use RF without issue
# so we have a lock to prevent multiple radio operations at the same time
self.radio_operation_lock = asyncio.Lock()
super().__init__(
hass,
_LOGGER,
@@ -67,7 +67,8 @@ async def async_setup_entry(
for shade in pv_entry.shade_data.values():
_LOGGER.debug("Initial refresh of shade: %s", shade.name)
await shade.refresh(suppress_timeout=True) # default 15 second timeout
async with coordinator.radio_operation_lock:
await shade.refresh(suppress_timeout=True) # default 15 second timeout
entities: list[ShadeEntity] = []
for shade in pv_entry.shade_data.values():
@@ -207,7 +208,8 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
async def _async_execute_move(self, move: ShadePosition) -> None:
"""Execute a move that can affect multiple positions."""
_LOGGER.debug("Move request %s: %s", self.name, move)
response = await self._shade.move(move)
async with self.coordinator.radio_operation_lock:
response = await self._shade.move(move)
_LOGGER.debug("Move response %s: %s", self.name, response)
# Process the response from the hub (including new positions)
@@ -318,7 +320,10 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
# error if are already have one in flight
return
# suppress timeouts caused by hub nightly reboot
await self._shade.refresh(suppress_timeout=True) # default 15 second timeout
async with self.coordinator.radio_operation_lock:
await self._shade.refresh(
suppress_timeout=True
) # default 15 second timeout
_LOGGER.debug("Process update %s: %s", self.name, self._shade.current_position)
self._async_update_shade_data(self._shade.current_position)
@@ -114,5 +114,6 @@ class PowerViewSelect(ShadeEntity, SelectEntity):
"""Change the selected option."""
await self.entity_description.select_fn(self._shade, option)
# force update data to ensure new info is in coordinator
await self._shade.refresh()
async with self.coordinator.radio_operation_lock:
await self._shade.refresh(suppress_timeout=True)
self.async_write_ha_state()
@@ -153,5 +153,6 @@ class PowerViewSensor(ShadeEntity, SensorEntity):
async def async_update(self) -> None:
"""Refresh sensor entity."""
await self.entity_description.update_fn(self._shade)
async with self.coordinator.radio_operation_lock:
await self.entity_description.update_fn(self._shade)
self.async_write_ha_state()
@@ -1,6 +1,6 @@
{
"services": {
"select_next": "mdi:skip",
"select_next": "mdi:skip-next",
"select_option": "mdi:check",
"select_previous": "mdi:skip-previous",
"select_first": "mdi:skip-backward",
+1 -1
View File
@@ -179,7 +179,7 @@ async def _get_dashboard_info(hass, url_path):
"views": views,
}
if config is None:
if config is None or "views" not in config:
return data
for idx, view in enumerate(config["views"]):
+1 -1
View File
@@ -141,7 +141,7 @@ class LutronLight(LutronDevice, LightEntity):
else:
brightness = self._prev_brightness
self._prev_brightness = brightness
args = {"new_level": brightness}
args = {"new_level": to_lutron_level(brightness)}
if ATTR_TRANSITION in kwargs:
args["fade_time_seconds"] = kwargs[ATTR_TRANSITION]
self._lutron_device.set_level(**args)
@@ -52,7 +52,7 @@
"unjoin": "mdi:ungroup",
"volume_down": "mdi:volume-minus",
"volume_mute": "mdi:volume-mute",
"volume_set": "mdi:volume",
"volume_set": "mdi:volume-medium",
"volume_up": "mdi:volume-plus"
}
}
@@ -1,4 +1,4 @@
"""Motionblinds BLE integration."""
"""Motionblinds Bluetooth integration."""
from __future__ import annotations
@@ -34,9 +34,9 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Motionblinds BLE integration."""
"""Set up Motionblinds Bluetooth integration."""
_LOGGER.debug("Setting up Motionblinds BLE integration")
_LOGGER.debug("Setting up Motionblinds Bluetooth integration")
# The correct time is needed for encryption
_LOGGER.debug("Setting timezone for encryption: %s", hass.config.time_zone)
@@ -46,7 +46,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Motionblinds BLE device from a config entry."""
"""Set up Motionblinds Bluetooth device from a config entry."""
_LOGGER.debug("(%s) Setting up device", entry.data[CONF_MAC_CODE])
@@ -94,7 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Motionblinds BLE device from a config entry."""
"""Unload Motionblinds Bluetooth device from a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
@@ -1,4 +1,4 @@
"""Button entities for the Motionblinds BLE integration."""
"""Button entities for the Motionblinds Bluetooth integration."""
from __future__ import annotations
@@ -1,4 +1,4 @@
"""Config flow for Motionblinds BLE integration."""
"""Config flow for Motionblinds Bluetooth integration."""
from __future__ import annotations
@@ -38,7 +38,7 @@ CONFIG_SCHEMA = vol.Schema({vol.Required(CONF_MAC_CODE): str})
class FlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Motionblinds BLE."""
"""Handle a config flow for Motionblinds Bluetooth."""
def __init__(self) -> None:
"""Initialize a ConfigFlow."""
@@ -1,4 +1,4 @@
"""Constants for the Motionblinds BLE integration."""
"""Constants for the Motionblinds Bluetooth integration."""
ATTR_CONNECT = "connect"
ATTR_DISCONNECT = "disconnect"
@@ -1,4 +1,4 @@
"""Cover entities for the Motionblinds BLE integration."""
"""Cover entities for the Motionblinds Bluetooth integration."""
from __future__ import annotations
@@ -1,4 +1,4 @@
"""Base entities for the Motionblinds BLE integration."""
"""Base entities for the Motionblinds Bluetooth integration."""
import logging
@@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__)
class MotionblindsBLEEntity(Entity):
"""Base class for Motionblinds BLE entities."""
"""Base class for Motionblinds Bluetooth entities."""
_attr_has_entity_name = True
_attr_should_poll = False
@@ -1,6 +1,6 @@
{
"domain": "motionblinds_ble",
"name": "Motionblinds BLE",
"name": "Motionblinds Bluetooth",
"bluetooth": [
{
"local_name": "MOTION_*",
@@ -1,4 +1,4 @@
"""Select entities for the Motionblinds BLE integration."""
"""Select entities for the Motionblinds Bluetooth integration."""
from __future__ import annotations
@@ -5,7 +5,7 @@ from __future__ import annotations
from http import HTTPStatus
from aiohttp import ClientError, ClientResponseError
from myuplink import MyUplinkAPI, get_manufacturer, get_system_name
from myuplink import MyUplinkAPI, get_manufacturer, get_model, get_system_name
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
@@ -92,7 +92,7 @@ def create_devices(
identifiers={(DOMAIN, device_id)},
name=get_system_name(system),
manufacturer=get_manufacturer(device),
model=device.productName,
model=get_model(device),
sw_version=device.firmwareCurrent,
serial_number=device.product_serial_number,
)
@@ -6,5 +6,5 @@
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/myuplink",
"iot_class": "cloud_polling",
"requirements": ["myuplink==0.5.0"]
"requirements": ["myuplink==0.6.0"]
}
+1 -1
View File
@@ -108,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
(CONF_REFRESH_TOKEN, client.refresh_token),
(CONF_USER_UUID, client.user_uuid),
):
if entry.data[key] == value:
if entry.data.get(key) == value:
continue
entry_updates["data"][key] = value
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/opower",
"iot_class": "cloud_polling",
"loggers": ["opower"],
"requirements": ["opower==0.4.1"]
"requirements": ["opower==0.4.3"]
}
@@ -105,6 +105,22 @@ BINARY_SENSOR_DESCRIPTIONS: list[OverkizBinarySensorDescription] = [
)
== 1,
),
OverkizBinarySensorDescription(
key=OverkizState.CORE_HEATING_STATUS,
name="Heating status",
device_class=BinarySensorDeviceClass.HEAT,
value_fn=lambda state: state == OverkizCommandParam.ON,
),
OverkizBinarySensorDescription(
key=OverkizState.MODBUSLINK_DHW_ABSENCE_MODE,
name="Absence mode",
value_fn=lambda state: state == OverkizCommandParam.ON,
),
OverkizBinarySensorDescription(
key=OverkizState.MODBUSLINK_DHW_BOOST_MODE,
name="Boost mode",
value_fn=lambda state: state == OverkizCommandParam.ON,
),
]
SUPPORTED_STATES = {
+17 -8
View File
@@ -7,6 +7,7 @@ from typing import cast
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import HomeAssistantOverkizData
@@ -27,15 +28,16 @@ async def async_setup_entry(
"""Set up the Overkiz climate from a config entry."""
data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
# Match devices based on the widget.
entities_based_on_widget: list[Entity] = [
WIDGET_TO_CLIMATE_ENTITY[device.widget](device.device_url, data.coordinator)
for device in data.platforms[Platform.CLIMATE]
if device.widget in WIDGET_TO_CLIMATE_ENTITY
)
]
# Match devices based on the widget and controllableName
# This is for example used for Atlantic APC, where devices with different functionality share the same uiClass and widget.
async_add_entities(
# Match devices based on the widget and controllableName.
# ie Atlantic APC
entities_based_on_widget_and_controllable: list[Entity] = [
WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY[device.widget][
cast(Controllable, device.controllable_name)
](device.device_url, data.coordinator)
@@ -43,14 +45,21 @@ async def async_setup_entry(
if device.widget in WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY
and device.controllable_name
in WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY[device.widget]
)
]
# Hitachi Air To Air Heat Pumps
async_add_entities(
# Match devices based on the widget and protocol.
# #ie Hitachi Air To Air Heat Pumps
entities_based_on_widget_and_protocol: list[Entity] = [
WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY[device.widget][device.protocol](
device.device_url, data.coordinator
)
for device in data.platforms[Platform.CLIMATE]
if device.widget in WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY
and device.protocol in WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY[device.widget]
]
async_add_entities(
entities_based_on_widget
+ entities_based_on_widget_and_controllable
+ entities_based_on_widget_and_protocol
)
@@ -159,7 +159,7 @@ class AtlanticPassAPCHeatingZone(OverkizEntity, ClimateEntity):
await self.async_set_heating_mode(PRESET_MODES_TO_OVERKIZ[preset_mode])
@property
def preset_mode(self) -> str:
def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp."""
heating_mode = cast(
str, self.executor.select_state(OverkizState.IO_PASS_APC_HEATING_MODE)
@@ -179,7 +179,7 @@ class AtlanticPassAPCHeatingZone(OverkizEntity, ClimateEntity):
return OVERKIZ_TO_PRESET_MODES[heating_mode]
@property
def target_temperature(self) -> float:
def target_temperature(self) -> float | None:
"""Return hvac target temperature."""
current_heating_profile = self.current_heating_profile
if current_heating_profile in OVERKIZ_TEMPERATURE_STATE_BY_PROFILE:
@@ -3,16 +3,24 @@
from __future__ import annotations
from asyncio import sleep
from functools import cached_property
from typing import Any, cast
from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState
from homeassistant.components.climate import PRESET_NONE, HVACMode
from homeassistant.const import ATTR_TEMPERATURE
from homeassistant.components.climate import (
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
PRESET_NONE,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES
from ..coordinator import OverkizDataUpdateCoordinator
from ..executor import OverkizExecutor
from .atlantic_pass_apc_heating_zone import AtlanticPassAPCHeatingZone
from .atlantic_pass_apc_zone_control import OVERKIZ_TO_HVAC_MODE
PRESET_SCHEDULE = "schedule"
PRESET_MANUAL = "manual"
@@ -24,32 +32,127 @@ OVERKIZ_MODE_TO_PRESET_MODES: dict[str, str] = {
PRESET_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_MODE_TO_PRESET_MODES.items()}
TEMPERATURE_ZONECONTROL_DEVICE_INDEX = 20
# Maps the HVAC current ZoneControl system operating mode.
OVERKIZ_TO_HVAC_ACTION: dict[str, HVACAction] = {
OverkizCommandParam.COOLING: HVACAction.COOLING,
OverkizCommandParam.DRYING: HVACAction.DRYING,
OverkizCommandParam.HEATING: HVACAction.HEATING,
# There is no known way to differentiate OFF from Idle.
OverkizCommandParam.STOP: HVACAction.OFF,
}
HVAC_ACTION_TO_OVERKIZ_PROFILE_STATE: dict[HVACAction, OverkizState] = {
HVACAction.COOLING: OverkizState.IO_PASS_APC_COOLING_PROFILE,
HVACAction.HEATING: OverkizState.IO_PASS_APC_HEATING_PROFILE,
}
HVAC_ACTION_TO_OVERKIZ_MODE_STATE: dict[HVACAction, OverkizState] = {
HVACAction.COOLING: OverkizState.IO_PASS_APC_COOLING_MODE,
HVACAction.HEATING: OverkizState.IO_PASS_APC_HEATING_MODE,
}
TEMPERATURE_ZONECONTROL_DEVICE_INDEX = 1
SUPPORTED_FEATURES: ClimateEntityFeature = (
ClimateEntityFeature.PRESET_MODE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
OVERKIZ_THERMAL_CONFIGURATION_TO_HVAC_MODE: dict[
OverkizCommandParam, tuple[HVACMode, ClimateEntityFeature]
] = {
OverkizCommandParam.COOLING: (
HVACMode.COOL,
SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE,
),
OverkizCommandParam.HEATING: (
HVACMode.HEAT,
SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE,
),
OverkizCommandParam.HEATING_AND_COOLING: (
HVACMode.HEAT_COOL,
SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
),
}
# Those device depends on a main probe that choose the operating mode (heating, cooling, ...)
# Those device depends on a main probe that choose the operating mode (heating, cooling, ...).
class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
"""Representation of Atlantic Pass APC Heating And Cooling Zone Control."""
_attr_target_temperature_step = PRECISION_HALVES
def __init__(
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
) -> None:
"""Init method."""
super().__init__(device_url, coordinator)
# There is less supported functions, because they depend on the ZoneControl.
if not self.is_using_derogated_temperature_fallback:
# Modes are not configurable, they will follow current HVAC Mode of Zone Control.
self._attr_hvac_modes = []
# When using derogated temperature, we fallback to legacy behavior.
if self.is_using_derogated_temperature_fallback:
return
# Those are available and tested presets on Shogun.
self._attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ]
self._attr_hvac_modes = []
self._attr_supported_features = ClimateEntityFeature(0)
# Modes depends on device capabilities.
if (thermal_configuration := self.thermal_configuration) is not None:
(
device_hvac_mode,
climate_entity_feature,
) = thermal_configuration
self._attr_hvac_modes = [device_hvac_mode, HVACMode.OFF]
self._attr_supported_features = climate_entity_feature
# Those are available and tested presets on Shogun.
self._attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ]
# Those APC Heating and Cooling probes depends on the zone control device (main probe).
# Only the base device (#1) can be used to get/set some states.
# Like to retrieve and set the current operating mode (heating, cooling, drying, off).
self.zone_control_device = self.executor.linked_device(
TEMPERATURE_ZONECONTROL_DEVICE_INDEX
self.zone_control_executor: OverkizExecutor | None = None
if (
zone_control_device := self.executor.linked_device(
TEMPERATURE_ZONECONTROL_DEVICE_INDEX
)
) is not None:
self.zone_control_executor = OverkizExecutor(
zone_control_device.device_url,
coordinator,
)
@cached_property
def thermal_configuration(self) -> tuple[HVACMode, ClimateEntityFeature] | None:
"""Retrieve thermal configuration for this devices."""
if (
(
state_thermal_configuration := cast(
OverkizCommandParam | None,
self.executor.select_state(OverkizState.CORE_THERMAL_CONFIGURATION),
)
)
is not None
and state_thermal_configuration
in OVERKIZ_THERMAL_CONFIGURATION_TO_HVAC_MODE
):
return OVERKIZ_THERMAL_CONFIGURATION_TO_HVAC_MODE[
state_thermal_configuration
]
return None
@cached_property
def device_hvac_mode(self) -> HVACMode | None:
"""ZoneControlZone device has a single possible mode."""
return (
None
if self.thermal_configuration is None
else self.thermal_configuration[0]
)
@property
@@ -61,21 +164,37 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
)
@property
def zone_control_hvac_mode(self) -> HVACMode:
def zone_control_hvac_action(self) -> HVACAction:
"""Return hvac operation ie. heat, cool, dry, off mode."""
if (
self.zone_control_device is not None
and (
state := self.zone_control_device.states[
if self.zone_control_executor is not None and (
(
state := self.zone_control_executor.select_state(
OverkizState.IO_PASS_APC_OPERATING_MODE
]
)
)
is not None
and (value := state.value_as_str) is not None
):
return OVERKIZ_TO_HVAC_MODE[value]
return HVACMode.OFF
return OVERKIZ_TO_HVAC_ACTION[cast(str, state)]
return HVACAction.OFF
@property
def hvac_action(self) -> HVACAction | None:
"""Return the current running hvac operation."""
# When ZoneControl action is heating/cooling but Zone is stopped, means the zone is idle.
if (
hvac_action := self.zone_control_hvac_action
) in HVAC_ACTION_TO_OVERKIZ_PROFILE_STATE and cast(
str,
self.executor.select_state(
HVAC_ACTION_TO_OVERKIZ_PROFILE_STATE[hvac_action]
),
) == OverkizCommandParam.STOP:
return HVACAction.IDLE
return hvac_action
@property
def hvac_mode(self) -> HVACMode:
@@ -84,30 +203,32 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
if self.is_using_derogated_temperature_fallback:
return super().hvac_mode
zone_control_hvac_mode = self.zone_control_hvac_mode
if (device_hvac_mode := self.device_hvac_mode) is None:
return HVACMode.OFF
# Should be same, because either thermostat or this integration change both.
on_off_state = cast(
cooling_is_off = cast(
str,
self.executor.select_state(
OverkizState.CORE_COOLING_ON_OFF
if zone_control_hvac_mode == HVACMode.COOL
else OverkizState.CORE_HEATING_ON_OFF
),
)
self.executor.select_state(OverkizState.CORE_COOLING_ON_OFF),
) in (OverkizCommandParam.OFF, None)
heating_is_off = cast(
str,
self.executor.select_state(OverkizState.CORE_HEATING_ON_OFF),
) in (OverkizCommandParam.OFF, None)
# Device is Stopped, it means the air flux is flowing but its venting door is closed.
if on_off_state == OverkizCommandParam.OFF:
hvac_mode = HVACMode.OFF
else:
hvac_mode = zone_control_hvac_mode
if (
(device_hvac_mode == HVACMode.COOL and cooling_is_off)
or (device_hvac_mode == HVACMode.HEAT and heating_is_off)
or (
device_hvac_mode == HVACMode.HEAT_COOL
and cooling_is_off
and heating_is_off
)
):
return HVACMode.OFF
# It helps keep it consistent with the Zone Control, within the interface.
if self._attr_hvac_modes != [zone_control_hvac_mode, HVACMode.OFF]:
self._attr_hvac_modes = [zone_control_hvac_mode, HVACMode.OFF]
self.async_write_ha_state()
return hvac_mode
return device_hvac_mode
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
@@ -118,46 +239,49 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
# They are mainly managed by the Zone Control device
# However, it make sense to map the OFF Mode to the Overkiz STOP Preset
if hvac_mode == HVACMode.OFF:
await self.executor.async_execute_command(
OverkizCommand.SET_COOLING_ON_OFF,
OverkizCommandParam.OFF,
)
await self.executor.async_execute_command(
OverkizCommand.SET_HEATING_ON_OFF,
OverkizCommandParam.OFF,
)
else:
await self.executor.async_execute_command(
OverkizCommand.SET_COOLING_ON_OFF,
OverkizCommandParam.ON,
)
await self.executor.async_execute_command(
OverkizCommand.SET_HEATING_ON_OFF,
OverkizCommandParam.ON,
)
on_off_target_command_param = (
OverkizCommandParam.OFF
if hvac_mode == HVACMode.OFF
else OverkizCommandParam.ON
)
await self.executor.async_execute_command(
OverkizCommand.SET_COOLING_ON_OFF,
on_off_target_command_param,
)
await self.executor.async_execute_command(
OverkizCommand.SET_HEATING_ON_OFF,
on_off_target_command_param,
)
await self.async_refresh_modes()
@property
def preset_mode(self) -> str:
def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., schedule, manual."""
if self.is_using_derogated_temperature_fallback:
return super().preset_mode
mode = OVERKIZ_MODE_TO_PRESET_MODES[
cast(
str,
self.executor.select_state(
OverkizState.IO_PASS_APC_COOLING_MODE
if self.zone_control_hvac_mode == HVACMode.COOL
else OverkizState.IO_PASS_APC_HEATING_MODE
),
if (
self.zone_control_hvac_action in HVAC_ACTION_TO_OVERKIZ_MODE_STATE
and (
mode_state := HVAC_ACTION_TO_OVERKIZ_MODE_STATE[
self.zone_control_hvac_action
]
)
]
and (
(
mode := OVERKIZ_MODE_TO_PRESET_MODES[
cast(str, self.executor.select_state(mode_state))
]
)
is not None
)
):
return mode
return mode if mode is not None else PRESET_NONE
return PRESET_NONE
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
@@ -178,13 +302,18 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
await self.async_refresh_modes()
@property
def target_temperature(self) -> float:
def target_temperature(self) -> float | None:
"""Return hvac target temperature."""
if self.is_using_derogated_temperature_fallback:
return super().target_temperature
if self.zone_control_hvac_mode == HVACMode.COOL:
device_hvac_mode = self.device_hvac_mode
if device_hvac_mode == HVACMode.HEAT_COOL:
return None
if device_hvac_mode == HVACMode.COOL:
return cast(
float,
self.executor.select_state(
@@ -192,7 +321,7 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
),
)
if self.zone_control_hvac_mode == HVACMode.HEAT:
if device_hvac_mode == HVACMode.HEAT:
return cast(
float,
self.executor.select_state(
@@ -204,32 +333,73 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
float, self.executor.select_state(OverkizState.CORE_TARGET_TEMPERATURE)
)
@property
def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach (cooling)."""
if self.device_hvac_mode != HVACMode.HEAT_COOL:
return None
return cast(
float,
self.executor.select_state(OverkizState.CORE_COOLING_TARGET_TEMPERATURE),
)
@property
def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach (heating)."""
if self.device_hvac_mode != HVACMode.HEAT_COOL:
return None
return cast(
float,
self.executor.select_state(OverkizState.CORE_HEATING_TARGET_TEMPERATURE),
)
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new temperature."""
if self.is_using_derogated_temperature_fallback:
return await super().async_set_temperature(**kwargs)
temperature = kwargs[ATTR_TEMPERATURE]
target_temperature = kwargs.get(ATTR_TEMPERATURE)
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
hvac_mode = self.hvac_mode
if hvac_mode == HVACMode.HEAT_COOL:
if target_temp_low is not None:
await self.executor.async_execute_command(
OverkizCommand.SET_HEATING_TARGET_TEMPERATURE,
target_temp_low,
)
if target_temp_high is not None:
await self.executor.async_execute_command(
OverkizCommand.SET_COOLING_TARGET_TEMPERATURE,
target_temp_high,
)
elif target_temperature is not None:
if hvac_mode == HVACMode.HEAT:
await self.executor.async_execute_command(
OverkizCommand.SET_HEATING_TARGET_TEMPERATURE,
target_temperature,
)
elif hvac_mode == HVACMode.COOL:
await self.executor.async_execute_command(
OverkizCommand.SET_COOLING_TARGET_TEMPERATURE,
target_temperature,
)
# Change both (heating/cooling) temperature is a good way to have consistency
await self.executor.async_execute_command(
OverkizCommand.SET_HEATING_TARGET_TEMPERATURE,
temperature,
)
await self.executor.async_execute_command(
OverkizCommand.SET_COOLING_TARGET_TEMPERATURE,
temperature,
)
await self.executor.async_execute_command(
OverkizCommand.SET_DEROGATION_ON_OFF_STATE,
OverkizCommandParam.OFF,
OverkizCommandParam.ON,
)
# Target temperature may take up to 1 minute to get refreshed.
await self.executor.async_execute_command(
OverkizCommand.REFRESH_TARGET_TEMPERATURE
)
await self.async_refresh_modes()
async def async_refresh_modes(self) -> None:
"""Refresh the device modes to have new states."""
@@ -256,3 +426,51 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
await self.executor.async_execute_command(
OverkizCommand.REFRESH_TARGET_TEMPERATURE
)
@property
def min_temp(self) -> float:
"""Return Minimum Temperature for AC of this group."""
device_hvac_mode = self.device_hvac_mode
if device_hvac_mode in (HVACMode.HEAT, HVACMode.HEAT_COOL):
return cast(
float,
self.executor.select_state(
OverkizState.CORE_MINIMUM_HEATING_TARGET_TEMPERATURE
),
)
if device_hvac_mode == HVACMode.COOL:
return cast(
float,
self.executor.select_state(
OverkizState.CORE_MINIMUM_COOLING_TARGET_TEMPERATURE
),
)
return super().min_temp
@property
def max_temp(self) -> float:
"""Return Max Temperature for AC of this group."""
device_hvac_mode = self.device_hvac_mode
if device_hvac_mode == HVACMode.HEAT:
return cast(
float,
self.executor.select_state(
OverkizState.CORE_MAXIMUM_HEATING_TARGET_TEMPERATURE
),
)
if device_hvac_mode in (HVACMode.COOL, HVACMode.HEAT_COOL):
return cast(
float,
self.executor.select_state(
OverkizState.CORE_MAXIMUM_COOLING_TARGET_TEMPERATURE
),
)
return super().max_temp
@@ -298,6 +298,11 @@ class HitachiAirToAirHeatPumpOVP(OverkizEntity, ClimateEntity):
OverkizState.OVP_FAN_SPEED,
OverkizCommandParam.AUTO,
)
# Sanitize fan mode: Overkiz is sometimes providing a state that
# cannot be used as a command. Convert it to HA space and back to Overkiz
if fan_mode not in FAN_MODES_TO_OVERKIZ.values():
fan_mode = FAN_MODES_TO_OVERKIZ[OVERKIZ_TO_FAN_MODES[fan_mode]]
hvac_mode = self._control_backfill(
hvac_mode,
OverkizState.OVP_MODE_CHANGE,
@@ -357,5 +362,5 @@ class HitachiAirToAirHeatPumpOVP(OverkizEntity, ClimateEntity):
]
await self.executor.async_execute_command(
OverkizCommand.GLOBAL_CONTROL, command_data
OverkizCommand.GLOBAL_CONTROL, *command_data
)
@@ -19,7 +19,7 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
"requirements": ["pyoverkiz==1.13.8"],
"requirements": ["pyoverkiz==1.13.9"],
"zeroconf": [
{
"type": "_kizbox._tcp.local.",
@@ -97,6 +97,28 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [
max_value_state_name=OverkizState.CORE_MAXIMAL_SHOWER_MANUAL_MODE,
entity_category=EntityCategory.CONFIG,
),
OverkizNumberDescription(
key=OverkizState.CORE_TARGET_DWH_TEMPERATURE,
name="Target temperature",
device_class=NumberDeviceClass.TEMPERATURE,
command=OverkizCommand.SET_TARGET_DHW_TEMPERATURE,
native_min_value=50,
native_max_value=65,
min_value_state_name=OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE,
max_value_state_name=OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE,
entity_category=EntityCategory.CONFIG,
),
OverkizNumberDescription(
key=OverkizState.CORE_WATER_TARGET_TEMPERATURE,
name="Water target temperature",
device_class=NumberDeviceClass.TEMPERATURE,
command=OverkizCommand.SET_WATER_TARGET_TEMPERATURE,
native_min_value=50,
native_max_value=65,
min_value_state_name=OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE,
max_value_state_name=OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE,
entity_category=EntityCategory.CONFIG,
),
# SomfyHeatingTemperatureInterface
OverkizNumberDescription(
key=OverkizState.CORE_ECO_ROOM_TEMPERATURE,
@@ -399,6 +399,20 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
native_unit_of_measurement=UnitOfTime.SECONDS,
entity_category=EntityCategory.DIAGNOSTIC,
),
OverkizSensorDescription(
key=OverkizState.CORE_BOTTOM_TANK_WATER_TEMPERATURE,
name="Bottom tank water temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
OverkizSensorDescription(
key=OverkizState.CORE_CONTROL_WATER_TARGET_TEMPERATURE,
name="Control water target temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
# Cover
OverkizSensorDescription(
key=OverkizState.CORE_TARGET_CLOSURE,
+4 -15
View File
@@ -42,7 +42,6 @@ from .const import (
DOMAIN,
INVALID_TOKEN_MESSAGE,
PLATFORMS,
PLATFORMS_COMPLETED,
PLEX_SERVER_CONFIG,
PLEX_UPDATE_LIBRARY_SIGNAL,
PLEX_UPDATE_PLATFORMS_SIGNAL,
@@ -94,18 +93,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
gdm.scan(scan_for_clients=True)
debouncer = Debouncer[None](
hass,
_LOGGER,
cooldown=10,
immediate=True,
function=gdm_scan,
hass, _LOGGER, cooldown=10, immediate=True, function=gdm_scan, background=True
).async_call
hass_data = PlexData(
servers={},
dispatchers={},
websockets={},
platforms_completed={},
gdm_scanner=gdm,
gdm_debouncer=debouncer,
)
@@ -180,7 +174,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
server_id = plex_server.machine_identifier
hass_data = get_plex_data(hass)
hass_data[SERVERS][server_id] = plex_server
hass_data[PLATFORMS_COMPLETED][server_id] = set()
entry.add_update_listener(async_options_updated)
@@ -233,11 +226,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
hass_data[WEBSOCKETS][server_id] = websocket
def start_websocket_session(platform):
hass_data[PLATFORMS_COMPLETED][server_id].add(platform)
if hass_data[PLATFORMS_COMPLETED][server_id] == PLATFORMS:
hass.loop.create_task(websocket.listen())
def close_websocket_session(_):
websocket.close()
@@ -248,8 +236,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
for platform in PLATFORMS:
start_websocket_session(platform)
entry.async_create_background_task(
hass, websocket.listen(), f"plex websocket listener {entry.entry_id}"
)
async_cleanup_plex_devices(hass, entry)
-1
View File
@@ -24,7 +24,6 @@ GDM_SCANNER: Final = "gdm_scanner"
PLATFORMS = frozenset(
[Platform.BUTTON, Platform.MEDIA_PLAYER, Platform.SENSOR, Platform.UPDATE]
)
PLATFORMS_COMPLETED: Final = "platforms_completed"
PLAYER_SOURCE = "player_source"
SERVERS: Final = "servers"
WEBSOCKETS: Final = "websockets"
-2
View File
@@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Any, TypedDict
from plexapi.gdm import GDM
from plexwebsocket import PlexWebsocket
from homeassistant.const import Platform
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from .const import DOMAIN, SERVERS
@@ -23,7 +22,6 @@ class PlexData(TypedDict):
servers: dict[str, PlexServer]
dispatchers: dict[str, list[CALLBACK_TYPE]]
websockets: dict[str, PlexWebsocket]
platforms_completed: dict[str, set[Platform]]
gdm_scanner: GDM
gdm_debouncer: Callable[[], Coroutine[Any, Any, None]]
+1 -1
View File
@@ -8,7 +8,7 @@
"iot_class": "local_push",
"loggers": ["plexapi", "plexwebsocket"],
"requirements": [
"PlexAPI==4.15.10",
"PlexAPI==4.15.11",
"plexauth==0.0.6",
"plexwebsocket==0.0.14"
],
+1
View File
@@ -97,6 +97,7 @@ class PlexServer:
cooldown=DEBOUNCE_TIMEOUT,
immediate=True,
function=self._async_update_platforms,
background=True,
).async_call
self.thumbnail_cache = {}
+11 -1
View File
@@ -715,6 +715,7 @@ class Statistics(Base, StatisticsBase):
"start_ts",
unique=True,
),
_DEFAULT_TABLE_ARGS,
)
__tablename__ = TABLE_STATISTICS
@@ -732,6 +733,7 @@ class StatisticsShortTerm(Base, StatisticsBase):
"start_ts",
unique=True,
),
_DEFAULT_TABLE_ARGS,
)
__tablename__ = TABLE_STATISTICS_SHORT_TERM
@@ -760,7 +762,10 @@ class StatisticsMeta(Base):
class RecorderRuns(Base):
"""Representation of recorder run."""
__table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),)
__table_args__ = (
Index("ix_recorder_runs_start_end", "start", "end"),
_DEFAULT_TABLE_ARGS,
)
__tablename__ = TABLE_RECORDER_RUNS
run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True)
start: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow)
@@ -789,6 +794,7 @@ class MigrationChanges(Base):
"""Representation of migration changes."""
__tablename__ = TABLE_MIGRATION_CHANGES
__table_args__ = (_DEFAULT_TABLE_ARGS,)
migration_id: Mapped[str] = mapped_column(String(255), primary_key=True)
version: Mapped[int] = mapped_column(SmallInteger)
@@ -798,6 +804,8 @@ class SchemaChanges(Base):
"""Representation of schema version changes."""
__tablename__ = TABLE_SCHEMA_CHANGES
__table_args__ = (_DEFAULT_TABLE_ARGS,)
change_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True)
schema_version: Mapped[int | None] = mapped_column(Integer)
changed: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow)
@@ -816,6 +824,8 @@ class StatisticsRuns(Base):
"""Representation of statistics run."""
__tablename__ = TABLE_STATISTICS_RUNS
__table_args__ = (_DEFAULT_TABLE_ARGS,)
run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True)
start: Mapped[datetime] = mapped_column(DATETIME_TYPE, index=True)
@@ -125,16 +125,16 @@ class RenaultVehicleProxy:
coordinator = self.coordinators[key]
if coordinator.not_supported:
# Remove endpoint as it is not supported for this vehicle.
LOGGER.info(
"Ignoring endpoint %s as it is not supported for this vehicle: %s",
LOGGER.warning(
"Ignoring endpoint %s as it is not supported: %s",
coordinator.name,
coordinator.last_exception,
)
del self.coordinators[key]
elif coordinator.access_denied:
# Remove endpoint as it is denied for this vehicle.
LOGGER.info(
"Ignoring endpoint %s as it is denied for this vehicle: %s",
LOGGER.warning(
"Ignoring endpoint %s as it is denied: %s",
coordinator.name,
coordinator.last_exception,
)
@@ -46,7 +46,6 @@ class ReolinkVODMediaSource(MediaSource):
"""Initialize ReolinkVODMediaSource."""
super().__init__(DOMAIN)
self.hass = hass
self.data: dict[str, ReolinkData] = hass.data[DOMAIN]
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
"""Resolve media to a url."""
@@ -57,7 +56,8 @@ class ReolinkVODMediaSource(MediaSource):
_, config_entry_id, channel_str, stream_res, filename = identifier
channel = int(channel_str)
host = self.data[config_entry_id].host
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
host = data[config_entry_id].host
vod_type = VodRequestType.RTMP
if host.api.is_nvr:
@@ -130,7 +130,8 @@ class ReolinkVODMediaSource(MediaSource):
if config_entry.state != ConfigEntryState.LOADED:
continue
channels: list[str] = []
host = self.data[config_entry.entry_id].host
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
host = data[config_entry.entry_id].host
entities = er.async_entries_for_config_entry(
entity_reg, config_entry.entry_id
)
@@ -187,7 +188,8 @@ class ReolinkVODMediaSource(MediaSource):
self, config_entry_id: str, channel: int
) -> BrowseMediaSource:
"""Allow the user to select the high or low playback resolution, (low loads faster)."""
host = self.data[config_entry_id].host
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
host = data[config_entry_id].host
main_enc = await host.api.get_encoding(channel, "main")
if main_enc == "h265":
@@ -236,7 +238,8 @@ class ReolinkVODMediaSource(MediaSource):
self, config_entry_id: str, channel: int, stream: str
) -> BrowseMediaSource:
"""Return all days on which recordings are available for a reolink camera."""
host = self.data[config_entry_id].host
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
host = data[config_entry_id].host
# We want today of the camera, not necessarily today of the server
now = host.api.time() or await host.api.async_get_time()
@@ -288,7 +291,8 @@ class ReolinkVODMediaSource(MediaSource):
day: int,
) -> BrowseMediaSource:
"""Return all recording files on a specific day of a Reolink camera."""
host = self.data[config_entry_id].host
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
host = data[config_entry_id].host
start = dt.datetime(year, month, day, hour=0, minute=0, second=0)
end = dt.datetime(year, month, day, hour=23, minute=59, second=59)
+1 -1
View File
@@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/ring",
"iot_class": "cloud_polling",
"loggers": ["ring_doorbell"],
"requirements": ["ring-doorbell[listen]==0.8.7"]
"requirements": ["ring-doorbell[listen]==0.8.9"]
}
+1 -1
View File
@@ -5,6 +5,6 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/romy",
"iot_class": "local_polling",
"requirements": ["romy==0.0.7"],
"requirements": ["romy==0.0.10"],
"zeroconf": ["_aicu-http._tcp.local."]
}
@@ -24,7 +24,7 @@
"documentation": "https://www.home-assistant.io/integrations/roomba",
"iot_class": "local_push",
"loggers": ["paho_mqtt", "roombapy"],
"requirements": ["roombapy==1.6.13"],
"requirements": ["roombapy==1.8.1"],
"zeroconf": [
{
"type": "_amzn-alexa._tcp.local.",
+1 -1
View File
@@ -54,7 +54,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Optional(CONF_HOUSE_NUMBER_SUFFIX, default=""): cv.string,
vol.Optional(CONF_NAME, default="Rova"): cv.string,
vol.Optional(CONF_MONITORED_CONDITIONS, default=["bio"]): vol.All(
cv.ensure_list, [vol.In(SENSOR_TYPES)]
cv.ensure_list, [vol.In(["bio", "paper", "plastic", "residual"])]
),
}
)
@@ -39,7 +39,7 @@
"samsungctl[websocket]==0.7.1",
"samsungtvws[async,encrypted]==2.6.0",
"wakeonlan==2.1.0",
"async-upnp-client==0.38.2"
"async-upnp-client==0.38.3"
],
"ssdp": [
{
+2
View File
@@ -234,3 +234,5 @@ DEVICES_WITHOUT_FIRMWARE_CHANGELOG = (
)
CONF_GEN = "gen"
SHELLY_PLUS_RGBW_CHANNELS = 4
+17
View File
@@ -14,6 +14,7 @@ from homeassistant.components.light import (
ATTR_RGB_COLOR,
ATTR_RGBW_COLOR,
ATTR_TRANSITION,
DOMAIN as LIGHT_DOMAIN,
ColorMode,
LightEntity,
LightEntityFeature,
@@ -34,12 +35,14 @@ from .const import (
RGBW_MODELS,
RPC_MIN_TRANSITION_TIME_SEC,
SHBLB_1_RGB_EFFECTS,
SHELLY_PLUS_RGBW_CHANNELS,
STANDARD_RGB_EFFECTS,
)
from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data
from .entity import ShellyBlockEntity, ShellyRpcEntity
from .utils import (
async_remove_shelly_entity,
async_remove_shelly_rpc_entities,
brightness_to_percentage,
get_device_entry_gen,
get_rpc_key_ids,
@@ -118,14 +121,28 @@ def async_setup_rpc_entry(
return
if light_key_ids := get_rpc_key_ids(coordinator.device.status, "light"):
# Light mode remove RGB & RGBW entities, add light entities
async_remove_shelly_rpc_entities(
hass, LIGHT_DOMAIN, coordinator.mac, ["rgb:0", "rgbw:0"]
)
async_add_entities(RpcShellyLight(coordinator, id_) for id_ in light_key_ids)
return
light_keys = [f"light:{i}" for i in range(SHELLY_PLUS_RGBW_CHANNELS)]
if rgb_key_ids := get_rpc_key_ids(coordinator.device.status, "rgb"):
# RGB mode remove light & RGBW entities, add RGB entity
async_remove_shelly_rpc_entities(
hass, LIGHT_DOMAIN, coordinator.mac, [*light_keys, "rgbw:0"]
)
async_add_entities(RpcShellyRgbLight(coordinator, id_) for id_ in rgb_key_ids)
return
if rgbw_key_ids := get_rpc_key_ids(coordinator.device.status, "rgbw"):
# RGBW mode remove light & RGB entities, add RGBW entity
async_remove_shelly_rpc_entities(
hass, LIGHT_DOMAIN, coordinator.mac, [*light_keys, "rgb:0"]
)
async_add_entities(RpcShellyRgbwLight(coordinator, id_) for id_ in rgbw_key_ids)
+10 -6
View File
@@ -222,7 +222,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
) -> None:
"""Initialize update entity."""
super().__init__(coordinator, key, attribute, description)
self._ota_in_progress: bool = False
self._ota_in_progress: bool | int = False
self._attr_release_url = get_release_url(
coordinator.device.gen, coordinator.model, description.beta
)
@@ -237,14 +237,13 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
@callback
def _ota_progress_callback(self, event: dict[str, Any]) -> None:
"""Handle device OTA progress."""
if self._ota_in_progress:
if self.in_progress is not False:
event_type = event["event"]
if event_type == OTA_BEGIN:
self._attr_in_progress = 0
self._ota_in_progress = 0
elif event_type == OTA_PROGRESS:
self._attr_in_progress = event["progress_percent"]
self._ota_in_progress = event["progress_percent"]
elif event_type in (OTA_ERROR, OTA_SUCCESS):
self._attr_in_progress = False
self._ota_in_progress = False
self.async_write_ha_state()
@@ -262,6 +261,11 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
return self.installed_version
@property
def in_progress(self) -> bool | int:
"""Update installation in progress."""
return self._ota_in_progress
async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
@@ -292,7 +296,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
await self.coordinator.async_shutdown_device_and_start_reauth()
else:
self._ota_in_progress = True
LOGGER.debug("OTA update call successful")
LOGGER.info("OTA update call for %s successful", self.coordinator.name)
class RpcSleepingUpdateEntity(
+12
View File
@@ -488,3 +488,15 @@ async def async_shutdown_device(device: BlockDevice | RpcDevice) -> None:
await device.shutdown()
if isinstance(device, BlockDevice):
device.shutdown()
@callback
def async_remove_shelly_rpc_entities(
hass: HomeAssistant, domain: str, mac: str, keys: list[str]
) -> None:
"""Remove RPC based Shelly entity."""
entity_reg = er_async_get(hass)
for key in keys:
if entity_id := entity_reg.async_get_entity_id(domain, DOMAIN, f"{mac}-{key}"):
LOGGER.debug("Removing entity: %s", entity_id)
entity_reg.async_remove(entity_id)
+1 -1
View File
@@ -270,7 +270,7 @@ class SnmpData:
"SNMP OID %s received type=%s and data %s",
self._baseoid,
type(value),
bytes(value),
value,
)
if isinstance(value, NoSuchObject):
_LOGGER.error(
@@ -7,6 +7,7 @@ from contextlib import suppress
from functools import partial
import logging
from typing import cast
import urllib.parse
from soco.data_structures import DidlObject
from soco.ms_data_structures import MusicServiceItem
@@ -60,12 +61,14 @@ def get_thumbnail_url_full(
media_content_id,
media_content_type,
)
return getattr(item, "album_art_uri", None)
return urllib.parse.unquote(getattr(item, "album_art_uri", ""))
return get_browse_image_url(
media_content_type,
media_content_id,
media_image_id,
return urllib.parse.unquote(
get_browse_image_url(
media_content_type,
media_content_id,
media_image_id,
)
)
@@ -166,6 +169,7 @@ def build_item_response(
payload["idstring"] = "A:ALBUMARTIST/" + "/".join(
payload["idstring"].split("/")[2:]
)
payload["idstring"] = urllib.parse.unquote(payload["idstring"])
try:
search_type = MEDIA_TYPES_TO_SONOS[payload["search_type"]]
@@ -201,7 +205,7 @@ def build_item_response(
if not title:
try:
title = payload["idstring"].split("/")[1]
title = urllib.parse.unquote(payload["idstring"].split("/")[1])
except IndexError:
title = LIBRARY_TITLES_MAPPING[payload["idstring"]]
@@ -493,10 +497,24 @@ def get_media(
"""Fetch media/album."""
search_type = MEDIA_TYPES_TO_SONOS.get(search_type, search_type)
if search_type == "playlists":
# Format is S:TITLE or S:ITEM_ID
splits = item_id.split(":")
title = splits[1] if len(splits) > 1 else None
playlist = next(
(
p
for p in media_library.get_playlists()
if (item_id == p.item_id or title == p.title)
),
None,
)
return playlist
if not item_id.startswith("A:ALBUM") and search_type == SONOS_ALBUM:
item_id = "A:ALBUMARTIST/" + "/".join(item_id.split("/")[2:])
search_term = item_id.split("/")[-1]
search_term = urllib.parse.unquote(item_id.split("/")[-1])
matches = media_library.get_music_library_information(
search_type, search_term=search_term, full_album_art_uri=True
)
@@ -626,13 +626,13 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
soco.play_uri(media_id, force_radio=is_radio)
elif media_type == MediaType.PLAYLIST:
if media_id.startswith("S:"):
item = media_browser.get_media(self.media.library, media_id, media_type)
soco.play_uri(item.get_uri())
return
try:
playlist = media_browser.get_media(
self.media.library, media_id, media_type
)
else:
playlists = soco.get_sonos_playlists(complete_result=True)
playlist = next(p for p in playlists if p.title == media_id)
except StopIteration:
playlist = next((p for p in playlists if p.title == media_id), None)
if not playlist:
_LOGGER.error('Could not find a Sonos playlist named "%s"', media_id)
else:
soco.clear_queue()
@@ -25,10 +25,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
partial(speedtest.Speedtest, secure=True)
)
coordinator = SpeedTestDataCoordinator(hass, config_entry, api)
await hass.async_add_executor_job(coordinator.update_servers)
except speedtest.SpeedtestException as err:
raise ConfigEntryNotReady from err
hass.data[DOMAIN] = coordinator
async def _async_finish_startup(hass: HomeAssistant) -> None:
"""Run this only when HA has finished its startup."""
await coordinator.async_config_entry_first_refresh()
@@ -36,8 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
# Don't start a speedtest during startup
async_at_started(hass, _async_finish_startup)
hass.data[DOMAIN] = coordinator
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
config_entry.async_on_unload(config_entry.add_update_listener(update_listener))
+1 -1
View File
@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["async_upnp_client"],
"quality_scale": "internal",
"requirements": ["async-upnp-client==0.38.2"]
"requirements": ["async-upnp-client==0.38.3"]
}
@@ -58,14 +58,14 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]):
async def _async_update_data(self) -> StarlinkData:
async with asyncio.timeout(4):
try:
status, location, sleep = await asyncio.gather(
self.hass.async_add_executor_job(status_data, self.channel_context),
self.hass.async_add_executor_job(
location_data, self.channel_context
),
self.hass.async_add_executor_job(
get_sleep_config, self.channel_context
),
status = await self.hass.async_add_executor_job(
status_data, self.channel_context
)
location = await self.hass.async_add_executor_job(
location_data, self.channel_context
)
sleep = await self.hass.async_add_executor_job(
get_sleep_config, self.channel_context
)
return StarlinkData(location, sleep, *status)
except GrpcError as exc:
+13 -4
View File
@@ -10,6 +10,7 @@ import math
from homeassistant.components.time import TimeEntity, TimeEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
@@ -62,14 +63,22 @@ class StarlinkTimeEntity(StarlinkEntity, TimeEntity):
def _utc_minutes_to_time(utc_minutes: int, timezone: tzinfo) -> time:
hour = math.floor(utc_minutes / 60)
minute = utc_minutes % 60
utc = datetime.now(UTC).replace(hour=hour, minute=minute, second=0, microsecond=0)
try:
utc = datetime.now(UTC).replace(
hour=hour, minute=minute, second=0, microsecond=0
)
except ValueError as exc:
raise HomeAssistantError from exc
return utc.astimezone(timezone).time()
def _time_to_utc_minutes(t: time, timezone: tzinfo) -> int:
zoned_time = datetime.now(timezone).replace(
hour=t.hour, minute=t.minute, second=0, microsecond=0
)
try:
zoned_time = datetime.now(timezone).replace(
hour=t.hour, minute=t.minute, second=0, microsecond=0
)
except ValueError as exc:
raise HomeAssistantError from exc
utc_time = zoned_time.astimezone(UTC).time()
return (utc_time.hour * 60) + utc_time.minute
@@ -105,6 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if (
SynoSurveillanceStation.INFO_API_KEY in available_apis
and SynoSurveillanceStation.HOME_MODE_API_KEY in available_apis
and api.surveillance_station is not None
):
coordinator_switches = SynologyDSMSwitchUpdateCoordinator(hass, entry, api)
await coordinator_switches.async_config_entry_first_refresh()
@@ -75,7 +75,7 @@
}
},
"services": {
"reboot": "mdi:reboot",
"reboot": "mdi:restart",
"shutdown": "mdi:power"
}
}
@@ -10,6 +10,6 @@
"iot_class": "local_push",
"loggers": ["systembridgeconnector"],
"quality_scale": "silver",
"requirements": ["systembridgeconnector==4.0.3"],
"requirements": ["systembridgeconnector==4.0.3", "systembridgemodels==4.0.4"],
"zeroconf": ["_system-bridge._tcp.local."]
}
@@ -166,7 +166,6 @@ class LogEntry:
"level",
"message",
"exception",
"extracted_tb",
"root_cause",
"source",
"count",
@@ -200,7 +199,6 @@ class LogEntry:
else:
self.source = (record.pathname, record.lineno)
self.count = 1
self.extracted_tb = extracted_tb
self.key = (self.name, self.source, self.root_cause)
def to_dict(self) -> dict[str, Any]:
@@ -34,7 +34,7 @@ DESCRIPTIONS: tuple[TessieBinarySensorEntityDescription, ...] = (
is_on=lambda x: x == TessieState.ONLINE,
),
TessieBinarySensorEntityDescription(
key="charge_state_battery_heater_on",
key="climate_state_battery_heater",
device_class=BinarySensorDeviceClass.HEAT,
entity_category=EntityCategory.DIAGNOSTIC,
),
+1 -1
View File
@@ -252,7 +252,7 @@
"state": {
"name": "Status"
},
"charge_state_battery_heater_on": {
"climate_state_battery_heater": {
"name": "Battery heater"
},
"charge_state_charge_enable_request": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"services": {
"start": "mdi:start",
"start": "mdi:play",
"pause": "mdi:pause",
"cancel": "mdi:cancel",
"finish": "mdi:check",
@@ -107,6 +107,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.ENUM,
options=[
"inaccurate_position",
"not_reporting",
"operational",
"system_shutdown_user",
@@ -70,6 +70,7 @@
"tracker_state": {
"name": "Tracker state",
"state": {
"inaccurate_position": "Inaccurate position",
"not_reporting": "Not reporting",
"operational": "Operational",
"system_shutdown_user": "System shutdown user",
+1 -1
View File
@@ -8,7 +8,7 @@
"iot_class": "local_push",
"loggers": ["aiounifi"],
"quality_scale": "platinum",
"requirements": ["aiounifi==73"],
"requirements": ["aiounifi==74"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",
+1 -1
View File
@@ -8,7 +8,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["async_upnp_client"],
"requirements": ["async-upnp-client==0.38.2", "getmac==0.9.4"],
"requirements": ["async-upnp-client==0.38.3", "getmac==0.9.4"],
"ssdp": [
{
"st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1"
@@ -13,7 +13,7 @@
"velbus-packet",
"velbus-protocol"
],
"requirements": ["velbus-aio==2023.12.0"],
"requirements": ["velbus-aio==2024.4.0"],
"usb": [
{
"vid": "10CF",
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/weatherflow_cloud",
"iot_class": "cloud_polling",
"requirements": ["weatherflow4py==0.2.17"]
"requirements": ["weatherflow4py==0.2.20"]
}
@@ -0,0 +1,49 @@
"""Diagnostics support for Whirlpool."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from . import WhirlpoolData
from .const import DOMAIN
TO_REDACT = {
"SERIAL_NUMBER",
"macaddress",
"username",
"password",
"token",
"unique_id",
"SAID",
}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant,
config_entry: ConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
whirlpool: WhirlpoolData = hass.data[DOMAIN][config_entry.entry_id]
diagnostics_data = {
"Washer_dryers": {
wd["NAME"]: dict(wd.items())
for wd in whirlpool.appliances_manager.washer_dryers
},
"aircons": {
ac["NAME"]: dict(ac.items()) for ac in whirlpool.appliances_manager.aircons
},
"ovens": {
oven["NAME"]: dict(oven.items())
for oven in whirlpool.appliances_manager.ovens
},
}
return {
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),
"appliances": async_redact_data(diagnostics_data, TO_REDACT),
}
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["whirlpool"],
"requirements": ["whirlpool-sixth-sense==0.18.6"]
"requirements": ["whirlpool-sixth-sense==0.18.7"]
}
+11 -5
View File
@@ -2,6 +2,8 @@
from __future__ import annotations
from functools import partial
from holidays import HolidayBase, country_holidays
from homeassistant.config_entries import ConfigEntry
@@ -13,7 +15,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss
from .const import CONF_PROVINCE, DOMAIN, PLATFORMS
def _validate_country_and_province(
async def _async_validate_country_and_province(
hass: HomeAssistant, entry: ConfigEntry, country: str | None, province: str | None
) -> None:
"""Validate country and province."""
@@ -21,7 +23,7 @@ def _validate_country_and_province(
if not country:
return
try:
country_holidays(country)
await hass.async_add_executor_job(country_holidays, country)
except NotImplementedError as ex:
async_create_issue(
hass,
@@ -39,7 +41,9 @@ def _validate_country_and_province(
if not province:
return
try:
country_holidays(country, subdiv=province)
await hass.async_add_executor_job(
partial(country_holidays, country, subdiv=province)
)
except NotImplementedError as ex:
async_create_issue(
hass,
@@ -66,10 +70,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
country: str | None = entry.options.get(CONF_COUNTRY)
province: str | None = entry.options.get(CONF_PROVINCE)
_validate_country_and_province(hass, entry, country, province)
await _async_validate_country_and_province(hass, entry, country, province)
if country and CONF_LANGUAGE not in entry.options:
cls: HolidayBase = country_holidays(country, subdiv=province)
cls: HolidayBase = await hass.async_add_executor_job(
partial(country_holidays, country, subdiv=province)
)
default_language = cls.default_language
new_options = entry.options.copy()
new_options[CONF_LANGUAGE] = default_language
@@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"quality_scale": "internal",
"requirements": ["holidays==0.45"]
"requirements": ["holidays==0.46"]
}
@@ -17,7 +17,7 @@
"switch_set_wifi_led_off": "mdi:wifi-off",
"switch_set_power_price": "mdi:currency-usd",
"switch_set_power_mode": "mdi:power",
"vacuum_remote_control_start": "mdi:start",
"vacuum_remote_control_start": "mdi:play",
"vacuum_remote_control_stop": "mdi:stop",
"vacuum_remote_control_move": "mdi:remote",
"vacuum_remote_control_move_step": "mdi:remote",
@@ -17,7 +17,7 @@
"iot_class": "local_push",
"loggers": ["async_upnp_client", "yeelight"],
"quality_scale": "platinum",
"requirements": ["yeelight==0.7.14", "async-upnp-client==0.38.2"],
"requirements": ["yeelight==0.7.14", "async-upnp-client==0.38.3"],
"zeroconf": [
{
"type": "_miio._udp.local.",
@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["zeroconf"],
"quality_scale": "internal",
"requirements": ["zeroconf==0.131.0"]
"requirements": ["zeroconf==0.132.0"]
}
+2 -2
View File
@@ -124,8 +124,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
zha_data = get_zha_data(hass)
if zha_data.yaml_config.get(CONF_ENABLE_QUIRKS, True):
setup_quirks(
custom_quirks_path=zha_data.yaml_config.get(CONF_CUSTOM_QUIRKS_PATH)
await hass.async_add_import_executor_job(
setup_quirks, zha_data.yaml_config.get(CONF_CUSTOM_QUIRKS_PATH)
)
# Load and cache device trigger information early
@@ -553,6 +553,13 @@ class OtaClientClusterHandler(ClientClusterHandler):
Ota.AttributeDefs.current_file_version.name: True,
}
@callback
def attribute_updated(self, attrid: int, value: Any, timestamp: Any) -> None:
"""Handle an attribute updated on this cluster."""
# We intentionally avoid the `ClientClusterHandler` attribute update handler:
# it emits a logbook event on every update, which pollutes the logbook
ClusterHandler.attribute_updated(self, attrid, value, timestamp)
@property
def current_file_version(self) -> int | None:
"""Return cached value of current_file_version attribute."""
-5
View File
@@ -130,14 +130,9 @@ class ZHAFirmwareUpdateEntity(
def _get_cluster_version(self) -> str | None:
"""Synchronize current file version with the cluster."""
device = self._ota_cluster_handler._endpoint.device # pylint: disable=protected-access
if self._ota_cluster_handler.current_file_version is not None:
return f"0x{self._ota_cluster_handler.current_file_version:08x}"
if device.sw_version is not None:
return device.sw_version
return None
@callback

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