Compare commits

...

125 Commits

Author SHA1 Message Date
Franck Nijhof cf53a9743f 2024.12.1 (#132509) 2024-12-06 20:21:31 +01:00
Franck Nijhof 4884891b2c Bump version to 2024.12.1 2024-12-06 18:54:13 +01:00
Allen Porter 30504fc9bd Fix google tasks due date timezone handling (#132498) 2024-12-06 18:53:42 +01:00
Bram Kragten 8827454dbd Update frontend to 20241127.6 (#132494) 2024-12-06 18:53:39 +01:00
Bram Kragten 3b30bbb85e Update frontend to 20241127.5 (#132475) 2024-12-06 18:53:35 +01:00
epenet df9eb482b5 Bump samsungtvws to 2.7.2 (#132474) 2024-12-06 18:53:32 +01:00
Steven B. 32aee61441 Bump tplink python-kasa dependency to 0.8.1 (#132472) 2024-12-06 18:53:29 +01:00
Robert Resch 35873cbe27 Point to the Ecovacs issue in the library for unspoorted devices (#132470)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2024-12-06 18:53:26 +01:00
Robert Resch 6fe492a51c Bump deebot-client to 9.2.0 (#132467) 2024-12-06 18:53:22 +01:00
G Johansson b1bc35f1c3 Fix nordpool dont have previous or next price (#132457) 2024-12-06 18:53:19 +01:00
Joakim Sørensen 56d10a0a7a Bump hass-nabucasa from 0.85.0 to 0.86.0 (#132456)
Bump hass-nabucasa fro 0.85.0 to 0.86.0
2024-12-06 18:53:16 +01:00
Allen Porter d091936ac6 Update exception handling for python3.13 for getpass.getuser() (#132449)
* Update exception handling for python3.13 for getpass.getuser()

* Add comment

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* Cleanup trailing space

---------

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-12-06 18:53:12 +01:00
J. Nick Koston 1dfd4e80b9 Bump aioesphomeapi to 28.0.0 (#132447) 2024-12-06 18:53:09 +01:00
J. Nick Koston d919de6734 Bump aiohttp to 3.11.10 (#132441) 2024-12-06 18:53:06 +01:00
Blake Bryant 3f9f0f8ac2 Bump pydeako to 0.6.0 (#132432)
feat: update deako integration to use improved version of pydeako

Some things of note:
- simplified errors
- pydeako has introduced some connection improvements

See here: https://github.com/DeakoLights/pydeako/releases/tag/0.6.0
2024-12-06 18:53:03 +01:00
Glenn Waters bf20ffae96 Bump upb-lib to 0.5.9 (#132411) 2024-12-06 18:53:00 +01:00
Diogo Gomes dad81927cb Removes references to croniter from utility_meter (#132364)
remove croniter
2024-12-06 18:52:56 +01:00
robinostlund 92392ab3d4 Add missing UnitOfPower to sensor (#132352)
* Add missing UnitOfPower to sensor

* Update homeassistant/components/sensor/const.py

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* adding to number

---------

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-12-06 18:52:53 +01:00
Brett Adams a47e5398f0 Bump tesla-fleet-api to 0.8.5 (#132339) 2024-12-06 18:52:50 +01:00
J. Nick Koston cf6d33635b Fix deprecated call to mimetypes.guess_type in CachingStaticResource (#132299) 2024-12-06 18:52:47 +01:00
Alberto Geniola 6a4031a383 Bump elmax-api to 0.0.6.3 (#131876) 2024-12-06 18:52:39 +01:00
Franck Nijhof 2b40844171 2024.12.0 (#132195) 2024-12-04 19:58:02 +01:00
Franck Nijhof 9b90df74a6 Bump version to 2024.12.0 2024-12-04 19:18:48 +01:00
Michael Hansen dcdf033fa9 Bump intents to 2024.12.4 (#132274) 2024-12-04 19:03:26 +01:00
Franck Nijhof 4c3ae395a4 Bump version to 2024.12.0b6 2024-12-04 15:33:47 +01:00
Jan Bouwhuis 333ada7670 Ensure MQTT subscriptions can be made when the broker is disconnected (#132270) 2024-12-04 15:33:35 +01:00
Bram Kragten 4fd4ba7813 Update frontend to 20241127.4 (#132268) 2024-12-04 15:33:31 +01:00
Robert Resch 7e96666dc5 Bump deebot-client to 9.1.0 (#132253) 2024-12-04 15:33:28 +01:00
Joost Lekkerkerker e463d5d16f Bump yt-dlp to 2024.12.03 (#132220) 2024-12-04 15:33:24 +01:00
Raphael Hehl f28579357e fix: unifiprotect prevent RTSP repair for third-party cameras (#132212)
Co-authored-by: J. Nick Koston <nick@koston.org>
2024-12-04 15:33:21 +01:00
cnico d40a9bd9ef Fix blocking call in netdata (#132209)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2024-12-04 15:33:17 +01:00
lunmay 28ecee6479 Fix typo in exception message in google_photos integration (#132194) 2024-12-04 15:33:14 +01:00
Michael Hansen 512ac7d572 Ensure entity names are not hassil templates (#132184) 2024-12-04 15:33:11 +01:00
Pete 22b353f7d5 Fix recorder "year" period in leap year (#132167)
* FIX: make "year" period work in leap year

* Add test

* Set second and microsecond to non-zero in test start times

* FIX: better fix for leap year problem

* Revert "FIX: better fix for leap year problem"

This reverts commit 06aba46ec6a0a1e944c88fe99d9bc6181a73cc1c.

---------

Co-authored-by: Erik <erik@montnemery.com>
2024-12-04 15:33:07 +01:00
Jan-Philipp Benecke 49c40cd902 Track if intent was processed locally (#132166) 2024-12-04 15:33:04 +01:00
LG-ThinQ-Integration 629c7a53ce Bump thinqconnect to 1.0.2 (#132131)
Co-authored-by: yunseon.park <yunseon.park@lge.com>
2024-12-04 15:32:58 +01:00
G Johansson 66e3ffffa7 Bump holidays to 0.62 (#132108) 2024-12-04 15:32:55 +01:00
Joost Lekkerkerker 139b424717 Bump knocki to 0.4.2 (#129261) 2024-12-04 15:32:50 +01:00
Franck Nijhof 33633f885d Ran hassfest 2024-12-04 09:59:28 +01:00
Franck Nijhof 759a2b84f5 Bump version to 2024.12.0b5 2024-12-03 18:03:36 +01:00
Bram Kragten ebffcb455f Update frontend to 20241127.3 (#132176) 2024-12-03 18:01:38 +01:00
epenet 08773cefb7 Pin rpds-py to 0.21.0 to fix CI (#132170)
* Pin rpds-py==0.21.0 to fix CI

* Add carriage return
2024-12-03 18:01:35 +01:00
Jon Seager 79352ea0f0 Bump pytouchlinesl to 0.3.0 (#132157) 2024-12-03 18:01:32 +01:00
Raphael Hehl b7038d4eb7 Bump uiprotect to 6.6.5 (#132147) 2024-12-03 18:01:29 +01:00
Tobias Perschon 8a310cbbf8 Improve error logging for unifi-ap (#132141) 2024-12-03 18:01:26 +01:00
Paulus Schoutsen 07196b0fda Fix bad hassil tests on CI (#132132)
* Fix CI

* Fix whitespace

---------

Co-authored-by: Michael Hansen <mike@rhasspy.org>
2024-12-03 18:01:23 +01:00
Tobias Perschon 0a38af7e48 Bump unifi_ap to 0.0.2 (#132125) 2024-12-03 18:01:19 +01:00
Bram Kragten 155fafb735 Update frontend to 20241127.2 (#132109)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2024-12-03 17:59:19 +01:00
J. Nick Koston 54ec41f25d Bump PyJWT to 2.10.1 (#132100) 2024-12-03 17:56:59 +01:00
Abílio Costa f480cc3396 Use translations on NumberEntity unit_of_measurement property (#132095)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2024-12-03 17:55:30 +01:00
Michael Hansen 2aea738032 Bump hassil and intents (#132092) 2024-12-03 17:54:28 +01:00
Jan Bouwhuis ab5165fdfa Fix imap sensor in case of alternative empty search response (#132081) 2024-12-03 17:36:08 +01:00
Jan-Philipp Benecke c6468aca2b Mark trend sensor unavailable when source entity is unknown/unavailable (#132080) 2024-12-03 17:36:04 +01:00
Duco Sebel 895ffbabf7 Round status light brightness number in HomeWizard (#132069) 2024-12-03 17:36:01 +01:00
Josef Zweck 3f1286b338 Set connections on device for acaia (#132064) 2024-12-03 17:35:57 +01:00
Simone Rescio d3a577ad89 Bump pyezviz to 0.2.2.3 (#132060) 2024-12-03 17:35:54 +01:00
Jan Rieger f44103ac7f Add translated native unit of measurement to Jellyfin (#132055) 2024-12-03 17:35:51 +01:00
Josef Zweck f1ebda7c6f Instantiate new httpx client for lamarzocco (#132016) 2024-12-03 17:35:47 +01:00
starkillerOG 905769f0e8 Fix Reolink dispatcher ID for onvif fallback (#131953) 2024-12-03 17:35:44 +01:00
Thomas55555 43899b6f28 Catch InverterReturnedError in APSystems (#131930)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-12-03 17:35:40 +01:00
Andrew Jackson b5e7da4262 Add translated native unit of measurement - QBitTorrent (#131918)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-12-03 17:35:37 +01:00
Andrew Jackson 3dc0ca7e1e Add translated native unit of measurement - PiHole (#131915) 2024-12-03 17:35:34 +01:00
Andrew Jackson 42c46a15b4 Add translated native unit of measurement - Transmission (#131913) 2024-12-03 17:35:31 +01:00
Andrew Jackson 97a725c2c6 Add translated native unit of measurement - squeezebox (#131912) 2024-12-03 17:35:28 +01:00
Abílio Costa c3499e5294 Update buienradar sensors only after being added to HA (#131830)
* Update buienradar sensors only after being added to HA

* Move check to util

* Check for platform in sensor state property

* Move check to unit translation key property

* Add test for sensor check

* Properly handle added_to_hass

* Remove redundant comment
2024-12-03 17:35:25 +01:00
Marcel van der Veldt 110935461e Add support for features changing at runtime in Matter integration (#129426) 2024-12-03 17:35:21 +01:00
Franck Nijhof be40db3dff Bump version to 2024.12.0b4 2024-12-02 13:02:23 +01:00
Josef Zweck c3c500955a Use format_mac correctly for acaia (#132062) 2024-12-02 12:59:41 +01:00
ashionky 1e5a5925e6 Bump refoss to v1.2.5 (#132051) 2024-12-02 12:59:37 +01:00
TimL d956e4b11d Bump psymlight v0.1.4 (#132045) 2024-12-02 12:59:33 +01:00
J. Nick Koston 8ff8cd8b65 Bump aiohttp to 3.11.9 (#132036)
changelog: https://github.com/aio-libs/aiohttp/compare/v3.11.8...v3.11.9
2024-12-02 12:59:29 +01:00
Joost Lekkerkerker fab35f227d Handle not found playlists in Spotify (#132033)
* Handle not found playlists

* Handle not found playlists

* Handle not found playlists

* Handle not found playlists

* Handle not found playlists

* Update homeassistant/components/spotify/coordinator.py

---------

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2024-12-02 12:59:26 +01:00
Joost Lekkerkerker e4d19541f5 Bump spotifyaio to 0.8.11 (#132032) 2024-12-02 12:59:22 +01:00
Joost Lekkerkerker 6b6fc6bbeb Bump yt-dlp to 2024.11.18 (#132026) 2024-12-02 12:59:18 +01:00
J. Nick Koston f2bafee84a Bump yarl to 1.18.3 (#132025)
changelog: https://github.com/aio-libs/yarl/compare/v1.18.0...v1.18.3
2024-12-02 12:59:15 +01:00
J. Nick Koston 4e0cdb0537 Bump propcache to 0.2.1 (#132022) 2024-12-02 12:59:06 +01:00
Richard Kroegel 79c919f62d Bump bimmer_connected to 0.17.2 (#132005) 2024-12-02 12:58:53 +01:00
Erik Montnemery b6dec11487 Freeze integration setup timeout for recorder during non-live data migration (#131998) 2024-12-02 12:58:44 +01:00
Bouwe Westerdijk e2073d7762 Bugfix for Plugwise, small code optimization (#131990) 2024-12-02 12:58:37 +01:00
Paulus Schoutsen d7428786cd Bump version to 2024.12.0b3 2024-12-01 03:14:16 +00:00
J. Nick Koston 673bdcc556 Reduce precision loss when converting HomeKit temperature (#131973) 2024-12-01 03:14:11 +00:00
J. Nick Koston e8ef990e72 Strip trailing spaces from HomeKit names (#131971) 2024-12-01 03:14:10 +00:00
starkillerOG 0d155c416a Bump reolink_aio to 0.11.4 (#131957) 2024-12-01 03:14:10 +00:00
Andrew Jackson e48be5c406 Bump aiomealie to 0.9.4 (#131951) 2024-12-01 03:14:09 +00:00
Matthias Alphart 787a1613ec Fix KNX IP Secure tunnelling endpoint selection with keyfile (#131941) 2024-12-01 03:14:08 +00:00
Raphael Hehl bb847b346d Bump uiprotect to 6.6.4 (#131931) 2024-12-01 03:14:07 +00:00
Jc2k e9b34eaad0 Bump aiohomekit to 3.2.7 (#131924) 2024-12-01 03:14:06 +00:00
Marcel van der Veldt 572347025b Fix media player join action for Music Assistant integration (#131910)
* Fix media player join action for Music Assistant integration

* Add tests for join/unjoin

* add one more test
2024-12-01 03:14:05 +00:00
Josef Zweck 29e80e56c6 Bump aioacaia to 0.1.10 (#131906) 2024-12-01 03:14:04 +00:00
Oliver b60b2fdd7c Bump denonavr to v1.0.1 (#131882) 2024-12-01 03:14:04 +00:00
Josef Zweck aaf3f61675 Guard against hostname change in lamarzocco discovery (#131873)
* Guard against hostname change in lamarzocco discovery

* switch to abort_entries_match
2024-12-01 03:13:50 +00:00
karwosts 5bf972ff16 Fix history stats count update immediately after change (#131856)
* Fix history stats count update immediately after change

* rerun CI
2024-12-01 03:13:45 +00:00
Glenn Vandeuren (aka Iondependent) 8eb52edabf Fix modbus state not dumped on restart (#131319)
* Fix modbus state not dumped on restart

* Update test_init.py

* Set event back  to stop

* Update test_init.py

---------

Co-authored-by: VandeurenGlenn <8685280+VandeurenGlenn@users.noreply.github.com>
2024-12-01 03:13:44 +00:00
J. Nick Koston 4326689f52 Bump SQLAlchemy to 2.0.36 (#126683)
* Bump SQLAlchemy to 2.0.35

changelog: https://docs.sqlalchemy.org/en/20/changelog/changelog_20.html#change-2.0.35

* fix mocking

* adjust to .36

* remove ignored as these are now typed

* fix SQLAlchemy
2024-12-01 03:13:44 +00:00
Franck Nijhof 06838c0280 Bump version to 2024.12.0b2 2024-11-28 21:02:37 +01:00
Richard Kroegel f97d96e3ae Add captcha to BMW ConfigFlow (#131351)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2024-11-28 21:02:23 +01:00
karwosts ee960933db Fix flaky test in history stats (#131869) 2024-11-28 20:55:34 +01:00
Joost Lekkerkerker 2ea0c54788 Only download translation strings we have defined (#131864) 2024-11-28 20:55:31 +01:00
Madhan dd18672341 Bump PyMetEireann to 2024.11.0 (#131860)
Co-authored-by: Joostlek <joostlek@outlook.com>
2024-11-28 20:55:26 +01:00
Bram Kragten ac4ae0430e Update frontend to 20241127.1 (#131855) 2024-11-28 20:55:23 +01:00
Joost Lekkerkerker eeb63d42a0 Bump pyatv to 0.16.0 (#131852) 2024-11-28 20:55:20 +01:00
Michael 9d48f36754 Allow empty trigger sentence responses in conversations (#131849)
allow empty trigger sentence responses
2024-11-28 20:55:16 +01:00
Joost Lekkerkerker 157198bf41 Make wake word selection part of configuration (#131832) 2024-11-28 20:55:13 +01:00
Joost Lekkerkerker be25b9d4d0 Bump spotifyaio to 0.8.10 (#131827) 2024-11-28 20:55:10 +01:00
epenet e08b71086f Fix more flaky translation checks (#131824) 2024-11-28 20:55:07 +01:00
Norbert Rittel 9677c6e24c Remove wrong plural "s" in 'todo.remove_item' action (#131814) 2024-11-28 20:55:03 +01:00
Franck Nijhof e2cda54473 Ensure custom integrations are assigned the custom IQS scale (#131795) 2024-11-28 20:55:00 +01:00
epenet 3ca49dc8a6 Bump samsungtvws to 2.7.1 (#131784) 2024-11-28 20:54:57 +01:00
Joost Lekkerkerker 80bc70771e Remove Spotify featured playlists and categories from media browser (#131758) 2024-11-28 20:54:54 +01:00
Erik Montnemery 7ab1bfcf1f Improve recorder history queries (#131702)
* Improve recorder history queries

* Remove some comments

* Update StatesManager._oldest_ts when adding pending state

* Update after review

* Improve tests

* Improve post-purge logic

* Avoid calling dt_util.utc_to_timestamp in new code

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2024-11-28 20:54:50 +01:00
Richard Kroegel 99f8dbd278 Bump bimmer_connected to 0.17.0 (#131352) 2024-11-28 20:54:46 +01:00
Franck Nijhof 3af0bc2c33 Bump version to 2024.12.0b1 2024-11-28 08:44:28 +01:00
TheJulianJES b8c4ce932c Fix Home Connect microwave programs (#131782) 2024-11-28 08:44:14 +01:00
puddly 0a3a3edf77 Bump ZHA to 0.0.41 (#131776) 2024-11-28 08:44:11 +01:00
J. Nick Koston 71376229f6 Bump aioesphomeapi to 27.0.3 (#131773) 2024-11-28 08:44:07 +01:00
Manu c9dde419a2 Fix rounding of attributes in Habitica integration (#131772) 2024-11-28 08:44:04 +01:00
Josef Zweck 2fc01a02db Bump pylamarzocco to 1.2.12 (#131765) 2024-11-28 08:44:01 +01:00
J. Nick Koston f02d2344fc Bump uiprotect to 6.6.3 (#131764) 2024-11-28 08:43:58 +01:00
Joost Lekkerkerker 509311ac19 Remove Spotify audio feature sensors (#131754) 2024-11-28 08:43:54 +01:00
J. Nick Koston 47e7c4f1c1 Bump orjson to 3.10.12 (#131752)
changelog: https://github.com/ijl/orjson/compare/3.10.11...3.10.12
2024-11-28 08:43:51 +01:00
J. Nick Koston c9d3ba900e Bump aiohttp to 3.11.8 (#131744) 2024-11-28 08:43:48 +01:00
Allen Porter 74a3d11aea Add a missing rainbird data description (#131740) 2024-11-28 08:43:45 +01:00
Marcel van der Veldt 897abc114e Bump music assistant client 1.0.8 (#131739) 2024-11-28 08:43:41 +01:00
Josef Zweck 3fff3003f2 Add missing data_description for lamarzocco OptionsFlow (#131708) 2024-11-28 08:43:37 +01:00
Franck Nijhof db5c93f96d Bump version to 2024.12.0b0 2024-11-27 18:36:24 +01:00
207 changed files with 2303 additions and 2183 deletions
+1 -1
View File
@@ -143,7 +143,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-dev"
skip-binary: aiohttp;multidict;yarl
skip-binary: aiohttp;multidict;propcache;yarl;SQLAlchemy
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements.txt"
@@ -42,7 +42,7 @@ class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
if user_input is not None:
mac = format_mac(user_input[CONF_ADDRESS])
mac = user_input[CONF_ADDRESS]
try:
is_new_style_scale = await is_new_scale(mac)
except AcaiaDeviceNotFound:
@@ -53,12 +53,12 @@ class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
except AcaiaUnknownDevice:
return self.async_abort(reason="unsupported_device")
else:
await self.async_set_unique_id(mac)
await self.async_set_unique_id(format_mac(mac))
self._abort_if_unique_id_configured()
if not errors:
return self.async_create_entry(
title=self._discovered_devices[user_input[CONF_ADDRESS]],
title=self._discovered_devices[mac],
data={
CONF_ADDRESS: mac,
CONF_IS_NEW_STYLE_SCALE: is_new_style_scale,
@@ -99,10 +99,10 @@ class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle a discovered Bluetooth device."""
self._discovered[CONF_ADDRESS] = mac = format_mac(discovery_info.address)
self._discovered[CONF_ADDRESS] = discovery_info.address
self._discovered[CONF_NAME] = discovery_info.name
await self.async_set_unique_id(mac)
await self.async_set_unique_id(format_mac(discovery_info.address))
self._abort_if_unique_id_configured()
try:
+9 -3
View File
@@ -2,7 +2,11 @@
from dataclasses import dataclass
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import (
CONNECTION_BLUETOOTH,
DeviceInfo,
format_mac,
)
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -25,13 +29,15 @@ class AcaiaEntity(CoordinatorEntity[AcaiaCoordinator]):
super().__init__(coordinator)
self.entity_description = entity_description
self._scale = coordinator.scale
self._attr_unique_id = f"{self._scale.mac}_{entity_description.key}"
formatted_mac = format_mac(self._scale.mac)
self._attr_unique_id = f"{formatted_mac}_{entity_description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._scale.mac)},
identifiers={(DOMAIN, formatted_mac)},
manufacturer="Acaia",
model=self._scale.model,
suggested_area="Kitchen",
connections={(CONNECTION_BLUETOOTH, self._scale.mac)},
)
@property
+1 -1
View File
@@ -25,5 +25,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["aioacaia"],
"requirements": ["aioacaia==0.1.9"]
"requirements": ["aioacaia==0.1.10"]
}
@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
"iot_class": "local_push",
"loggers": ["pyatv", "srptools"],
"requirements": ["pyatv==0.15.1"],
"requirements": ["pyatv==0.16.0"],
"zeroconf": [
"_mediaremotetv._tcp.local.",
"_companion-link._tcp.local.",
@@ -5,12 +5,17 @@ from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from APsystemsEZ1 import APsystemsEZ1M, ReturnAlarmInfo, ReturnOutputData
from APsystemsEZ1 import (
APsystemsEZ1M,
InverterReturnedError,
ReturnAlarmInfo,
ReturnOutputData,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import LOGGER
from .const import DOMAIN, LOGGER
@dataclass
@@ -43,6 +48,11 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
self.api.min_power = device_info.minPower
async def _async_update_data(self) -> ApSystemsSensorData:
output_data = await self.api.get_output_data()
alarm_info = await self.api.get_alarm_info()
try:
output_data = await self.api.get_output_data()
alarm_info = await self.api.get_alarm_info()
except InverterReturnedError:
raise UpdateFailed(
translation_domain=DOMAIN, translation_key="inverter_error"
) from None
return ApSystemsSensorData(output_data=output_data, alarm_info=alarm_info)
@@ -72,5 +72,10 @@
"name": "Inverter status"
}
}
},
"exceptions": {
"inverter_error": {
"message": "Inverter returned an error"
}
}
}
@@ -1018,6 +1018,7 @@ class PipelineRun:
"intent_input": intent_input,
"conversation_id": conversation_id,
"device_id": device_id,
"prefer_local_intents": self.pipeline.prefer_local_intents,
},
)
)
@@ -1031,6 +1032,7 @@ class PipelineRun:
language=self.pipeline.language,
agent_id=self.intent_agent,
)
processed_locally = self.intent_agent == conversation.HOME_ASSISTANT_AGENT
conversation_result: conversation.ConversationResult | None = None
if user_input.agent_id != conversation.HOME_ASSISTANT_AGENT:
@@ -1040,7 +1042,7 @@ class PipelineRun:
:= await conversation.async_handle_sentence_triggers(
self.hass, user_input
)
):
) is not None:
# Sentence trigger matched
trigger_response = intent.IntentResponse(
self.pipeline.conversation_language
@@ -1061,6 +1063,7 @@ class PipelineRun:
response=intent_response,
conversation_id=user_input.conversation_id,
)
processed_locally = True
if conversation_result is None:
# Fall back to pipeline conversation agent
@@ -1085,7 +1088,10 @@ class PipelineRun:
self.process_event(
PipelineEvent(
PipelineEventType.INTENT_END,
{"intent_output": conversation_result.as_dict()},
{
"processed_locally": processed_locally,
"intent_output": conversation_result.as_dict(),
},
)
)
@@ -27,9 +27,18 @@ from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_SOURCE, CONF_US
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
from homeassistant.util.ssl import get_default_context
from . import DOMAIN
from .const import CONF_ALLOWED_REGIONS, CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN
from .const import (
CONF_ALLOWED_REGIONS,
CONF_CAPTCHA_REGIONS,
CONF_CAPTCHA_TOKEN,
CONF_CAPTCHA_URL,
CONF_GCID,
CONF_READ_ONLY,
CONF_REFRESH_TOKEN,
)
DATA_SCHEMA = vol.Schema(
{
@@ -41,7 +50,14 @@ DATA_SCHEMA = vol.Schema(
translation_key="regions",
)
),
}
},
extra=vol.REMOVE_EXTRA,
)
CAPTCHA_SCHEMA = vol.Schema(
{
vol.Required(CONF_CAPTCHA_TOKEN): str,
},
extra=vol.REMOVE_EXTRA,
)
@@ -54,6 +70,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
data[CONF_USERNAME],
data[CONF_PASSWORD],
get_region_from_name(data[CONF_REGION]),
hcaptcha_token=data.get(CONF_CAPTCHA_TOKEN),
verify=get_default_context(),
)
try:
@@ -79,15 +97,17 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
data: dict[str, Any] = {}
_existing_entry_data: Mapping[str, Any] | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
errors: dict[str, str] = self.data.pop("errors", {})
if user_input is not None:
if user_input is not None and not errors:
unique_id = f"{user_input[CONF_REGION]}-{user_input[CONF_USERNAME]}"
await self.async_set_unique_id(unique_id)
@@ -96,22 +116,35 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
else:
self._abort_if_unique_id_configured()
# Store user input for later use
self.data.update(user_input)
# North America and Rest of World require captcha token
if (
self.data.get(CONF_REGION) in CONF_CAPTCHA_REGIONS
and CONF_CAPTCHA_TOKEN not in self.data
):
return await self.async_step_captcha()
info = None
try:
info = await validate_input(self.hass, user_input)
entry_data = {
**user_input,
CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN),
CONF_GCID: info.get(CONF_GCID),
}
info = await validate_input(self.hass, self.data)
except MissingCaptcha:
errors["base"] = "missing_captcha"
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
finally:
self.data.pop(CONF_CAPTCHA_TOKEN, None)
if info:
entry_data = {
**self.data,
CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN),
CONF_GCID: info.get(CONF_GCID),
}
if self.source == SOURCE_REAUTH:
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data=entry_data
@@ -128,7 +161,7 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
schema = self.add_suggested_values_to_schema(
DATA_SCHEMA,
self._existing_entry_data,
self._existing_entry_data or self.data,
)
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
@@ -147,6 +180,22 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
self._existing_entry_data = self._get_reconfigure_entry().data
return await self.async_step_user()
async def async_step_captcha(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Show captcha form."""
if user_input and user_input.get(CONF_CAPTCHA_TOKEN):
self.data[CONF_CAPTCHA_TOKEN] = user_input[CONF_CAPTCHA_TOKEN].strip()
return await self.async_step_user(self.data)
return self.async_show_form(
step_id="captcha",
data_schema=CAPTCHA_SCHEMA,
description_placeholders={
"captcha_url": CONF_CAPTCHA_URL.format(region=self.data[CONF_REGION])
},
)
@staticmethod
@callback
def async_get_options_flow(
@@ -8,10 +8,15 @@ ATTR_DIRECTION = "direction"
ATTR_VIN = "vin"
CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"]
CONF_CAPTCHA_REGIONS = ["north_america", "rest_of_world"]
CONF_READ_ONLY = "read_only"
CONF_ACCOUNT = "account"
CONF_REFRESH_TOKEN = "refresh_token"
CONF_GCID = "gcid"
CONF_CAPTCHA_TOKEN = "captcha_token"
CONF_CAPTCHA_URL = (
"https://bimmer-connected.readthedocs.io/en/stable/captcha/{region}.html"
)
DATA_HASS_CONFIG = "hass_config"
@@ -84,11 +84,6 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
if self.account.refresh_token != old_refresh_token:
self._update_config_entry_refresh_token(self.account.refresh_token)
_LOGGER.debug(
"bimmer_connected: refresh token %s > %s",
old_refresh_token,
self.account.refresh_token,
)
def _update_config_entry_refresh_token(self, refresh_token: str | None) -> None:
"""Update or delete the refresh_token in the Config Entry."""
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"iot_class": "cloud_polling",
"loggers": ["bimmer_connected"],
"requirements": ["bimmer-connected[china]==0.16.4"]
"requirements": ["bimmer-connected[china]==0.17.2"]
}
@@ -7,6 +7,16 @@
"password": "[%key:common::config_flow::data::password%]",
"region": "ConnectedDrive Region"
}
},
"captcha": {
"title": "Are you a robot?",
"description": "A captcha is required for BMW login. Visit the external website to complete the challenge and submit the form. Copy the resulting token into the field below.\n\n{captcha_url}\n\nNo data will be exposed outside of your Home Assistant instance.",
"data": {
"captcha_token": "Captcha token"
},
"data_description": {
"captcha_token": "One-time token retrieved from the captcha challenge."
}
}
},
"error": {
+17 -4
View File
@@ -742,6 +742,7 @@ class BrSensor(SensorEntity):
) -> None:
"""Initialize the sensor."""
self.entity_description = description
self._data: BrData | None = None
self._measured = None
self._attr_unique_id = (
f"{coordinates[CONF_LATITUDE]:2.6f}{coordinates[CONF_LONGITUDE]:2.6f}"
@@ -756,17 +757,29 @@ class BrSensor(SensorEntity):
if description.key.startswith(PRECIPITATION_FORECAST):
self._timeframe = None
async def async_added_to_hass(self) -> None:
"""Handle entity being added to hass."""
if self._data is None:
return
self._update()
@callback
def data_updated(self, data: BrData):
"""Update data."""
if self._load_data(data.data) and self.hass:
"""Handle data update."""
self._data = data
if not self.hass:
return
self._update()
def _update(self):
"""Update sensor data."""
_LOGGER.debug("Updating sensor %s", self.entity_id)
if self._load_data(self._data.data):
self.async_write_ha_state()
@callback
def _load_data(self, data): # noqa: C901
"""Load the sensor with relevant data."""
# Find sensor
# Check if we have a new measurement,
# otherwise we do not have to update the sensor
if self._measured == data.get(MEASURED):
+1 -1
View File
@@ -8,6 +8,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["hass_nabucasa"],
"requirements": ["hass-nabucasa==0.85.0"],
"requirements": ["hass-nabucasa==0.86.0"],
"single_config_entry": true
}
@@ -711,7 +711,7 @@ class DefaultAgent(ConversationEntity):
for name_tuple in self._get_entity_name_tuples(exposed=False):
self._unexposed_names_trie.insert(
name_tuple[0].lower(),
TextSlotValue.from_tuple(name_tuple),
TextSlotValue.from_tuple(name_tuple, allow_template=False),
)
# Build filtered slot list
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["hassil==2.0.4", "home-assistant-intents==2024.11.27"]
"requirements": ["hassil==2.0.5", "home-assistant-intents==2024.12.4"]
}
+3 -8
View File
@@ -4,8 +4,7 @@ from __future__ import annotations
import logging
from pydeako.deako import Deako, DeviceListTimeout, FindDevicesTimeout
from pydeako.discover import DeakoDiscoverer
from pydeako import Deako, DeakoDiscoverer, FindDevicesError
from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigEntry
@@ -30,12 +29,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: DeakoConfigEntry) -> boo
await connection.connect()
try:
await connection.find_devices()
except DeviceListTimeout as exc: # device list never received
_LOGGER.warning("Device not responding to device list")
await connection.disconnect()
raise ConfigEntryNotReady(exc) from exc
except FindDevicesTimeout as exc: # total devices expected not received
_LOGGER.warning("Device not responding to device requests")
except FindDevicesError as exc:
_LOGGER.warning("Error finding devices: %s", exc)
await connection.disconnect()
raise ConfigEntryNotReady(exc) from exc
@@ -1,6 +1,6 @@
"""Config flow for deako."""
from pydeako.discover import DeakoDiscoverer, DevicesNotFoundException
from pydeako import DeakoDiscoverer, DevicesNotFoundException
from homeassistant.components import zeroconf
from homeassistant.core import HomeAssistant
+1 -1
View File
@@ -2,7 +2,7 @@
from typing import Any
from pydeako.deako import Deako
from pydeako import Deako
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.core import HomeAssistant
+1 -1
View File
@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/deako",
"iot_class": "local_polling",
"loggers": ["pydeako"],
"requirements": ["pydeako==0.5.4"],
"requirements": ["pydeako==0.6.0"],
"single_config_entry": true,
"zeroconf": ["_deako._tcp.local."]
}
@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/denonavr",
"iot_class": "local_push",
"loggers": ["denonavr"],
"requirements": ["denonavr==1.0.0"],
"requirements": ["denonavr==1.0.1"],
"ssdp": [
{
"manufacturer": "Denon",
@@ -99,8 +99,8 @@ class EcovacsController:
for device_config in devices.not_supported:
_LOGGER.warning(
(
'Device "%s" not supported. Please add support for it to '
"https://github.com/DeebotUniverse/client.py: %s"
'Device "%s" not supported. More information at '
"https://github.com/DeebotUniverse/client.py/issues/612: %s"
),
device_config["deviceName"],
device_config,
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.10", "deebot-client==9.0.0"]
"requirements": ["py-sucks==0.9.10", "deebot-client==9.2.0"]
}
+1 -1
View File
@@ -35,7 +35,7 @@ def check_local_version_supported(api_version: str | None) -> bool:
class DirectPanel(PanelEntry):
"""Helper class for wrapping a directly accessed Elmax Panel."""
def __init__(self, panel_uri):
def __init__(self, panel_uri) -> None:
"""Construct the object."""
super().__init__(panel_uri, True, {})
@@ -203,7 +203,7 @@ class ElmaxConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_direct(self, user_input: dict[str, Any]) -> ConfigFlowResult:
"""Handle the direct setup step."""
self._selected_mode = CONF_ELMAX_MODE_CLOUD
self._selected_mode = CONF_ELMAX_MODE_DIRECT
if user_input is None:
return self.async_show_form(
step_id=CONF_ELMAX_MODE_DIRECT,
+2 -2
View File
@@ -121,13 +121,13 @@ class ElmaxCover(ElmaxEntity, CoverEntity):
else:
_LOGGER.debug("Ignoring stop request as the cover is IDLE")
async def async_open_cover(self, **kwargs):
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
await self.coordinator.http_client.execute_command(
endpoint_id=self._device.endpoint_id, command=CoverCommand.UP
)
async def async_close_cover(self, **kwargs):
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
await self.coordinator.http_client.execute_command(
endpoint_id=self._device.endpoint_id, command=CoverCommand.DOWN
+1 -1
View File
@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/elmax",
"iot_class": "cloud_polling",
"loggers": ["elmax_api"],
"requirements": ["elmax-api==0.0.6.1"],
"requirements": ["elmax-api==0.0.6.3"],
"zeroconf": [
{
"type": "_elmax-ssl._tcp.local."
@@ -16,7 +16,7 @@
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
"mqtt": ["esphome/discover/#"],
"requirements": [
"aioesphomeapi==27.0.2",
"aioesphomeapi==28.0.0",
"esphome-dashboard-api==1.2.3",
"bleak-esphome==1.1.0"
],
+4 -1
View File
@@ -10,6 +10,7 @@ from homeassistant.components.assist_pipeline.select import (
)
from homeassistant.components.assist_satellite import AssistSatelliteConfiguration
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import restore_state
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -100,7 +101,9 @@ class EsphomeAssistSatelliteWakeWordSelect(
"""Wake word selector for esphome devices."""
entity_description = SelectEntityDescription(
key="wake_word", translation_key="wake_word"
key="wake_word",
translation_key="wake_word",
entity_category=EntityCategory.CONFIG,
)
_attr_should_poll = False
_attr_current_option: str | None = None
+1 -1
View File
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/ezviz",
"iot_class": "cloud_polling",
"loggers": ["paho_mqtt", "pyezviz"],
"requirements": ["pyezviz==0.2.1.2"]
"requirements": ["pyezviz==0.2.2.3"]
}
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20241127.0"]
"requirements": ["home-assistant-frontend==20241127.6"]
}
@@ -48,7 +48,7 @@
"message": "`{filename}` is not an image"
},
"missing_upload_permission": {
"message": "Home Assistnt was not granted permission to upload to Google Photos"
"message": "Home Assistant was not granted permission to upload to Google Photos"
},
"upload_error": {
"message": "Failed to upload content: {message}"
@@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import date, datetime, timedelta
from datetime import UTC, date, datetime, timedelta
from typing import Any, cast
from homeassistant.components.todo import (
@@ -39,8 +39,10 @@ def _convert_todo_item(item: TodoItem) -> dict[str, str | None]:
else:
result["status"] = TodoItemStatus.NEEDS_ACTION
if (due := item.due) is not None:
# due API field is a timestamp string, but with only date resolution
result["due"] = dt_util.start_of_local_day(due).isoformat()
# due API field is a timestamp string, but with only date resolution.
# The time portion of the date is always discarded by the API, so we
# always set to UTC.
result["due"] = dt_util.start_of_local_day(due).replace(tzinfo=UTC).isoformat()
else:
result["due"] = None
result["notes"] = item.description
@@ -51,6 +53,8 @@ def _convert_api_item(item: dict[str, str]) -> TodoItem:
"""Convert tasks API items into a TodoItem."""
due: date | None = None
if (due_str := item.get("due")) is not None:
# Due dates are returned always in UTC so we only need to
# parse the date portion which will be interpreted as a a local date.
due = datetime.fromisoformat(due_str).date()
return TodoItem(
summary=item["title"],
+1 -1
View File
@@ -174,7 +174,7 @@ def get_attribute_points(
)
return {
"level": min(round(user["stats"]["lvl"] / 2), 50),
"level": min(floor(user["stats"]["lvl"] / 2), 50),
"equipment": equipment,
"class": class_bonus,
"allocated": user["stats"][attribute],
+5 -2
View File
@@ -22,7 +22,7 @@ import homeassistant.util.dt as dt_util
from . import websocket_api
from .const import DOMAIN
from .helpers import entities_may_have_state_changes_after, has_recorder_run_after
from .helpers import entities_may_have_state_changes_after, has_states_before
CONF_ORDER = "use_include_order"
@@ -107,7 +107,10 @@ class HistoryPeriodView(HomeAssistantView):
no_attributes = "no_attributes" in request.query
if (
(end_time and not has_recorder_run_after(hass, end_time))
# has_states_before will return True if there are states older than
# end_time. If it's false, we know there are no states in the
# database up until end_time.
(end_time and not has_states_before(hass, end_time))
or not include_start_time_state
and entity_ids
and not entities_may_have_state_changes_after(
+7 -6
View File
@@ -6,7 +6,6 @@ from collections.abc import Iterable
from datetime import datetime as dt
from homeassistant.components.recorder import get_instance
from homeassistant.components.recorder.models import process_timestamp
from homeassistant.core import HomeAssistant
@@ -26,8 +25,10 @@ def entities_may_have_state_changes_after(
return False
def has_recorder_run_after(hass: HomeAssistant, run_time: dt) -> bool:
"""Check if the recorder has any runs after a specific time."""
return run_time >= process_timestamp(
get_instance(hass).recorder_runs_manager.first.start
)
def has_states_before(hass: HomeAssistant, run_time: dt) -> bool:
"""Check if the recorder has states as old or older than run_time.
Returns True if there may be such states.
"""
oldest_ts = get_instance(hass).states_manager.oldest_ts
return oldest_ts is not None and run_time.timestamp() >= oldest_ts
@@ -39,7 +39,7 @@ from homeassistant.util.async_ import create_eager_task
import homeassistant.util.dt as dt_util
from .const import EVENT_COALESCE_TIME, MAX_PENDING_HISTORY_STATES
from .helpers import entities_may_have_state_changes_after, has_recorder_run_after
from .helpers import entities_may_have_state_changes_after, has_states_before
_LOGGER = logging.getLogger(__name__)
@@ -142,7 +142,10 @@ async def ws_get_history_during_period(
no_attributes = msg["no_attributes"]
if (
(end_time and not has_recorder_run_after(hass, end_time))
# has_states_before will return True if there are states older than
# end_time. If it's false, we know there are no states in the
# database up until end_time.
(end_time and not has_states_before(hass, end_time))
or not include_start_time_state
and entity_ids
and not entities_may_have_state_changes_after(
+10 -1
View File
@@ -4,6 +4,8 @@ from __future__ import annotations
from dataclasses import dataclass
import datetime
import logging
import math
from homeassistant.components.recorder import get_instance, history
from homeassistant.core import Event, EventStateChangedData, HomeAssistant, State
@@ -14,6 +16,8 @@ from .helpers import async_calculate_period, floored_timestamp
MIN_TIME_UTC = datetime.datetime.min.replace(tzinfo=dt_util.UTC)
_LOGGER = logging.getLogger(__name__)
@dataclass
class HistoryStatsState:
@@ -186,8 +190,13 @@ class HistoryStats:
current_state_matches = history_state.state in self._entity_states
state_change_timestamp = history_state.last_changed
if state_change_timestamp > now_timestamp:
if math.floor(state_change_timestamp) > now_timestamp:
# Shouldn't count states that are in the future
_LOGGER.debug(
"Skipping future timestamp %s (now %s)",
state_change_timestamp,
now_timestamp,
)
continue
if previous_state_matches:
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.61", "babel==2.15.0"]
"requirements": ["holidays==0.62", "babel==2.15.0"]
}
@@ -140,12 +140,12 @@ TRANSLATION_KEYS_PROGRAMS_MAP = {
"Cooking.Oven.Program.HeatingMode.HotAir80Steam",
"Cooking.Oven.Program.HeatingMode.HotAir100Steam",
"Cooking.Oven.Program.HeatingMode.SabbathProgramme",
"Cooking.Oven.Program.Microwave90Watt",
"Cooking.Oven.Program.Microwave180Watt",
"Cooking.Oven.Program.Microwave360Watt",
"Cooking.Oven.Program.Microwave600Watt",
"Cooking.Oven.Program.Microwave900Watt",
"Cooking.Oven.Program.Microwave1000Watt",
"Cooking.Oven.Program.Microwave.90Watt",
"Cooking.Oven.Program.Microwave.180Watt",
"Cooking.Oven.Program.Microwave.360Watt",
"Cooking.Oven.Program.Microwave.600Watt",
"Cooking.Oven.Program.Microwave.900Watt",
"Cooking.Oven.Program.Microwave.1000Watt",
"Cooking.Oven.Program.Microwave.Max",
"Cooking.Oven.Program.HeatingMode.WarmingDrawer",
"LaundryCare.Washer.Program.Cotton",
+3 -11
View File
@@ -114,7 +114,7 @@ _LOGGER = logging.getLogger(__name__)
NUMBERS_ONLY_RE = re.compile(r"[^\d.]+")
VERSION_RE = re.compile(r"([0-9]+)(\.[0-9]+)?(\.[0-9]+)?")
INVALID_END_CHARS = "-_"
INVALID_END_CHARS = "-_ "
MAX_VERSION_PART = 2**32 - 1
@@ -424,20 +424,12 @@ def cleanup_name_for_homekit(name: str | None) -> str:
def temperature_to_homekit(temperature: float, unit: str) -> float:
"""Convert temperature to Celsius for HomeKit."""
return round(
TemperatureConverter.convert(temperature, unit, UnitOfTemperature.CELSIUS), 1
)
return TemperatureConverter.convert(temperature, unit, UnitOfTemperature.CELSIUS)
def temperature_to_states(temperature: float, unit: str) -> float:
"""Convert temperature back from Celsius to Home Assistant unit."""
return (
round(
TemperatureConverter.convert(temperature, UnitOfTemperature.CELSIUS, unit)
* 2
)
/ 2
)
return TemperatureConverter.convert(temperature, UnitOfTemperature.CELSIUS, unit)
def density_to_air_quality(density: float) -> int:
@@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"],
"requirements": ["aiohomekit==3.2.6"],
"requirements": ["aiohomekit==3.2.7"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
}
@@ -64,4 +64,4 @@ class HWEnergyNumberEntity(HomeWizardEntity, NumberEntity):
or (brightness := self.coordinator.data.state.brightness) is None
):
return None
return brightness_to_value((0, 100), brightness)
return round(brightness_to_value((0, 100), brightness))
+2 -1
View File
@@ -326,7 +326,8 @@ class HomeAssistantApplication(web.Application):
protocol,
writer,
task,
loop=self._loop,
# loop will never be None when called from aiohttp
loop=self._loop, # type: ignore[arg-type]
client_max_size=self._client_max_size,
)
+11 -3
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Mapping
from pathlib import Path
import sys
from typing import Final
from aiohttp.hdrs import CACHE_CONTROL, CONTENT_TYPE
@@ -17,6 +18,15 @@ CACHE_HEADER = f"public, max-age={CACHE_TIME}"
CACHE_HEADERS: Mapping[str, str] = {CACHE_CONTROL: CACHE_HEADER}
RESPONSE_CACHE: LRU[tuple[str, Path], tuple[Path, str]] = LRU(512)
if sys.version_info >= (3, 13):
# guess_type is soft-deprecated in 3.13
# for paths and should only be used for
# URLs. guess_file_type should be used
# for paths instead.
_GUESSER = CONTENT_TYPES.guess_file_type
else:
_GUESSER = CONTENT_TYPES.guess_type
class CachingStaticResource(StaticResource):
"""Static Resource handler that will add cache headers."""
@@ -37,9 +47,7 @@ class CachingStaticResource(StaticResource):
# Must be directory index; ignore caching
return response
file_path = response._path # noqa: SLF001
response.content_type = (
CONTENT_TYPES.guess_type(file_path)[0] or FALLBACK_CONTENT_TYPE
)
response.content_type = _GUESSER(file_path)[0] or FALLBACK_CONTENT_TYPE
# Cache actual header after setter construction.
content_type = response.headers[CONTENT_TYPE]
RESPONSE_CACHE[key] = (file_path, content_type)
+11 -1
View File
@@ -332,7 +332,17 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]):
raise UpdateFailed(
f"Invalid response for search '{self.config_entry.data[CONF_SEARCH]}': {result} / {lines[0]}"
)
if not (count := len(message_ids := lines[0].split())):
# Check we do have returned items.
#
# In rare cases, when no UID's are returned,
# only the status line is returned, and not an empty line.
# See: https://github.com/home-assistant/core/issues/132042
#
# Strictly the RfC notes that 0 or more numbers should be returned
# delimited by a space.
#
# See: https://datatracker.ietf.org/doc/html/rfc3501#section-7.2.5
if len(lines) == 1 or not (count := len(message_ids := lines[0].split())):
self._last_message_uid = None
return 0
last_message_uid = (
@@ -36,7 +36,6 @@ SENSOR_TYPES: tuple[JellyfinSensorEntityDescription, ...] = (
key="watching",
translation_key="watching",
value_fn=_count_now_playing,
native_unit_of_measurement="clients",
),
)
@@ -29,7 +29,8 @@
"entity": {
"sensor": {
"watching": {
"name": "Active clients"
"name": "Active clients",
"unit_of_measurement": "clients"
}
}
},
+2 -3
View File
@@ -41,13 +41,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: KnockiConfigEntry) -> bo
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_create_background_task(
hass, client.start_websocket(), "knocki-websocket"
)
await client.start_websocket()
return True
async def async_unload_entry(hass: HomeAssistant, entry: KnockiConfigEntry) -> bool:
"""Unload a config entry."""
await entry.runtime_data.client.close()
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["knocki"],
"requirements": ["knocki==0.3.5"]
"requirements": ["knocki==0.4.2"]
}
+3
View File
@@ -54,6 +54,7 @@ from .const import (
CONF_KNX_SECURE_USER_PASSWORD,
CONF_KNX_STATE_UPDATER,
CONF_KNX_TELEGRAM_LOG_SIZE,
CONF_KNX_TUNNEL_ENDPOINT_IA,
CONF_KNX_TUNNELING,
CONF_KNX_TUNNELING_TCP,
CONF_KNX_TUNNELING_TCP_SECURE,
@@ -352,6 +353,7 @@ class KNXModule:
if _conn_type == CONF_KNX_TUNNELING_TCP:
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING_TCP,
individual_address=self.entry.data.get(CONF_KNX_TUNNEL_ENDPOINT_IA),
gateway_ip=self.entry.data[CONF_HOST],
gateway_port=self.entry.data[CONF_PORT],
auto_reconnect=True,
@@ -364,6 +366,7 @@ class KNXModule:
if _conn_type == CONF_KNX_TUNNELING_TCP_SECURE:
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING_TCP_SECURE,
individual_address=self.entry.data.get(CONF_KNX_TUNNEL_ENDPOINT_IA),
gateway_ip=self.entry.data[CONF_HOST],
gateway_port=self.entry.data[CONF_PORT],
secure_config=SecureConfig(
+1 -1
View File
@@ -104,7 +104,7 @@ class KNXConfigEntryData(TypedDict, total=False):
route_back: bool # not required
host: str # only required for tunnelling
port: int # only required for tunnelling
tunnel_endpoint_ia: str | None
tunnel_endpoint_ia: str | None # tunnelling only - not required (use get())
# KNX secure
user_id: int | None # not required
user_password: str | None # not required
@@ -23,7 +23,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.httpx_client import create_async_httpx_client
from .const import CONF_USE_BLUETOOTH, DOMAIN
from .coordinator import LaMarzoccoConfigEntry, LaMarzoccoUpdateCoordinator
@@ -47,11 +47,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
assert entry.unique_id
serial = entry.unique_id
client = create_async_httpx_client(hass)
cloud_client = LaMarzoccoCloudClient(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
client=get_async_client(hass),
client=client,
)
# initialize local API
@@ -61,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
local_client = LaMarzoccoLocalClient(
host=host,
local_bearer=entry.data[CONF_TOKEN],
client=get_async_client(hass),
client=client,
)
# initialize Bluetooth
@@ -6,6 +6,7 @@ from collections.abc import Mapping
import logging
from typing import Any
from httpx import AsyncClient
from pylamarzocco.client_cloud import LaMarzoccoCloudClient
from pylamarzocco.client_local import LaMarzoccoLocalClient
from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
@@ -37,7 +38,7 @@ from homeassistant.const import (
)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.httpx_client import create_async_httpx_client
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
@@ -57,6 +58,8 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 2
_client: AsyncClient
def __init__(self) -> None:
"""Initialize the config flow."""
self._config: dict[str, Any] = {}
@@ -79,10 +82,12 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
**user_input,
**self._discovered,
}
self._client = create_async_httpx_client(self.hass)
cloud_client = LaMarzoccoCloudClient(
username=data[CONF_USERNAME],
password=data[CONF_PASSWORD],
client=self._client,
)
try:
self._fleet = await cloud_client.get_customer_fleet()
@@ -163,7 +168,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
# validate local connection if host is provided
if user_input.get(CONF_HOST):
if not await LaMarzoccoLocalClient.validate_connection(
client=get_async_client(self.hass),
client=self._client,
host=user_input[CONF_HOST],
token=selected_device.communication_key,
):
@@ -291,6 +296,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_ADDRESS: discovery_info.macaddress,
}
)
self._async_abort_entries_match({CONF_ADDRESS: discovery_info.macaddress})
_LOGGER.debug(
"Discovered La Marzocco machine %s through DHCP at address %s",
@@ -36,5 +36,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"loggers": ["pylamarzocco"],
"requirements": ["pylamarzocco==1.2.11"]
"requirements": ["pylamarzocco==1.2.12"]
}
@@ -67,8 +67,10 @@
"step": {
"init": {
"data": {
"title": "Update Configuration",
"use_bluetooth": "Use Bluetooth"
},
"data_description": {
"use_bluetooth": "Should the integration try to use Bluetooth to control the machine?"
}
}
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/lg_thinq",
"iot_class": "cloud_push",
"loggers": ["thinqconnect"],
"requirements": ["thinqconnect==1.0.1"]
"requirements": ["thinqconnect==1.0.2"]
}
+12 -6
View File
@@ -45,6 +45,7 @@ class MatterAdapter:
self.hass = hass
self.config_entry = config_entry
self.platform_handlers: dict[Platform, AddEntitiesCallback] = {}
self.discovered_entities: set[str] = set()
def register_platform_handler(
self, platform: Platform, add_entities: AddEntitiesCallback
@@ -54,23 +55,19 @@ class MatterAdapter:
async def setup_nodes(self) -> None:
"""Set up all existing nodes and subscribe to new nodes."""
initialized_nodes: set[int] = set()
for node in self.matter_client.get_nodes():
initialized_nodes.add(node.node_id)
self._setup_node(node)
def node_added_callback(event: EventType, node: MatterNode) -> None:
"""Handle node added event."""
initialized_nodes.add(node.node_id)
self._setup_node(node)
def node_updated_callback(event: EventType, node: MatterNode) -> None:
"""Handle node updated event."""
if node.node_id in initialized_nodes:
return
if not node.available:
return
initialized_nodes.add(node.node_id)
# We always run the discovery logic again,
# because the firmware version could have been changed or features added.
self._setup_node(node)
def endpoint_added_callback(event: EventType, data: dict[str, int]) -> None:
@@ -237,11 +234,20 @@ class MatterAdapter:
self._create_device_registry(endpoint)
# run platform discovery from device type instances
for entity_info in async_discover_entities(endpoint):
discovery_key = (
f"{entity_info.platform}_{endpoint.node.node_id}_{endpoint.endpoint_id}_"
f"{entity_info.primary_attribute.cluster_id}_"
f"{entity_info.primary_attribute.attribute_id}_"
f"{entity_info.entity_description.key}"
)
if discovery_key in self.discovered_entities:
continue
LOGGER.debug(
"Creating %s entity for %s",
entity_info.platform,
entity_info.primary_attribute,
)
self.discovered_entities.add(discovery_key)
new_entity = entity_info.entity_class(
self.matter_client, endpoint, entity_info
)
@@ -159,6 +159,7 @@ DISCOVERY_SCHEMAS = [
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.DoorLock.Attributes.DoorState,),
featuremap_contains=clusters.DoorLock.Bitmaps.Feature.kDoorPositionSensor,
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
@@ -69,6 +69,7 @@ DISCOVERY_SCHEMAS = [
entity_class=MatterCommandButton,
required_attributes=(clusters.Identify.Attributes.AcceptedCommandList,),
value_contains=clusters.Identify.Commands.Identify.command_id,
allow_multi=True,
),
MatterDiscoverySchema(
platform=Platform.BUTTON,
+2
View File
@@ -13,3 +13,5 @@ LOGGER = logging.getLogger(__package__)
# prefixes to identify device identifier id types
ID_TYPE_DEVICE_ID = "deviceid"
ID_TYPE_SERIAL = "serial"
FEATUREMAP_ATTRIBUTE_ID = 65532
+19 -5
View File
@@ -13,6 +13,7 @@ from homeassistant.core import callback
from .binary_sensor import DISCOVERY_SCHEMAS as BINARY_SENSOR_SCHEMAS
from .button import DISCOVERY_SCHEMAS as BUTTON_SCHEMAS
from .climate import DISCOVERY_SCHEMAS as CLIMATE_SENSOR_SCHEMAS
from .const import FEATUREMAP_ATTRIBUTE_ID
from .cover import DISCOVERY_SCHEMAS as COVER_SCHEMAS
from .event import DISCOVERY_SCHEMAS as EVENT_SCHEMAS
from .fan import DISCOVERY_SCHEMAS as FAN_SCHEMAS
@@ -121,12 +122,24 @@ def async_discover_entities(
continue
# check for required value in (primary) attribute
primary_attribute = schema.required_attributes[0]
primary_value = endpoint.get_attribute_value(None, primary_attribute)
if schema.value_contains is not None and (
(primary_attribute := next((x for x in schema.required_attributes), None))
is None
or (value := endpoint.get_attribute_value(None, primary_attribute)) is None
or not isinstance(value, list)
or schema.value_contains not in value
isinstance(primary_value, list)
and schema.value_contains not in primary_value
):
continue
# check for required value in cluster featuremap
if schema.featuremap_contains is not None and (
not bool(
int(
endpoint.get_attribute_value(
primary_attribute.cluster_id, FEATUREMAP_ATTRIBUTE_ID
)
)
& schema.featuremap_contains
)
):
continue
@@ -147,6 +160,7 @@ def async_discover_entities(
attributes_to_watch=attributes_to_watch,
entity_description=schema.entity_description,
entity_class=schema.entity_class,
discovery_schema=schema,
)
# prevent re-discovery of the primary attribute if not allowed
+38 -1
View File
@@ -16,9 +16,10 @@ from propcache import cached_property
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity, EntityDescription
import homeassistant.helpers.entity_registry as er
from homeassistant.helpers.typing import UndefinedType
from .const import DOMAIN, ID_TYPE_DEVICE_ID
from .const import DOMAIN, FEATUREMAP_ATTRIBUTE_ID, ID_TYPE_DEVICE_ID
from .helpers import get_device_id
if TYPE_CHECKING:
@@ -140,6 +141,19 @@ class MatterEntity(Entity):
node_filter=self._endpoint.node.node_id,
)
)
# subscribe to FeatureMap attribute (as that can dynamically change)
self._unsubscribes.append(
self.matter_client.subscribe_events(
callback=self._on_featuremap_update,
event_filter=EventType.ATTRIBUTE_UPDATED,
node_filter=self._endpoint.node.node_id,
attr_path_filter=create_attribute_path(
endpoint=self._endpoint.endpoint_id,
cluster_id=self._entity_info.primary_attribute.cluster_id,
attribute_id=FEATUREMAP_ATTRIBUTE_ID,
),
)
)
@cached_property
def name(self) -> str | UndefinedType | None:
@@ -159,6 +173,29 @@ class MatterEntity(Entity):
self._update_from_device()
self.async_write_ha_state()
@callback
def _on_featuremap_update(
self, event: EventType, data: tuple[int, str, int] | None
) -> None:
"""Handle FeatureMap attribute updates."""
if data is None:
return
new_value = data[2]
# handle edge case where a Feature is removed from a cluster
if (
self._entity_info.discovery_schema.featuremap_contains is not None
and not bool(
new_value & self._entity_info.discovery_schema.featuremap_contains
)
):
# this entity is no longer supported by the device
ent_reg = er.async_get(self.hass)
ent_reg.async_remove(self.entity_id)
return
# all other cases, just update the entity
self._on_matter_event(event, data)
@callback
def _update_from_device(self) -> None:
"""Update data from Matter device."""
-1
View File
@@ -206,6 +206,5 @@ DISCOVERY_SCHEMAS = [
),
entity_class=MatterLock,
required_attributes=(clusters.DoorLock.Attributes.LockState,),
optional_attributes=(clusters.DoorLock.Attributes.DoorState,),
),
]
@@ -51,6 +51,9 @@ class MatterEntityInfo:
# entity class to use to instantiate the entity
entity_class: type
# the original discovery schema used to create this entity
discovery_schema: MatterDiscoverySchema
@property
def primary_attribute(self) -> type[ClusterAttributeDescriptor]:
"""Return Primary Attribute belonging to the entity."""
@@ -113,6 +116,10 @@ class MatterDiscoverySchema:
# NOTE: only works for list values
value_contains: Any | None = None
# [optional] the primary attribute's cluster featuremap must contain this value
# for example for the DoorSensor on a DoorLock Cluster
featuremap_contains: int | None = None
# [optional] bool to specify if this primary value may be discovered
# by multiple platforms
allow_multi: bool = False
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/mealie",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["aiomealie==0.9.3"]
"requirements": ["aiomealie==0.9.4"]
}
@@ -8,6 +8,6 @@
"iot_class": "calculated",
"loggers": ["yt_dlp"],
"quality_scale": "internal",
"requirements": ["yt-dlp[default]==2024.11.04"],
"requirements": ["yt-dlp[default]==2024.12.03"],
"single_config_entry": true
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/met_eireann",
"iot_class": "cloud_polling",
"loggers": ["meteireann"],
"requirements": ["PyMetEireann==2021.8.0"]
"requirements": ["PyMetEireann==2024.11.0"]
}
@@ -158,8 +158,6 @@ async def async_modbus_setup(
async def async_stop_modbus(event: Event) -> None:
"""Stop Modbus service."""
async_dispatcher_send(hass, SIGNAL_STOP_ENTITY)
for client in hub_collect.values():
await client.async_close()
+1 -1
View File
@@ -227,7 +227,7 @@ def async_subscribe_internal(
translation_placeholders={"topic": topic},
) from exc
client = mqtt_data.client
if not client.connected and not mqtt_config_entry_enabled(hass):
if not mqtt_config_entry_enabled(hass):
raise HomeAssistantError(
f"Cannot subscribe to topic '{topic}', MQTT is not enabled",
translation_key="mqtt_not_setup_cannot_subscribe",
@@ -7,6 +7,6 @@
"documentation": "https://www.home-assistant.io/integrations/music_assistant",
"iot_class": "local_push",
"loggers": ["music_assistant"],
"requirements": ["music-assistant-client==1.0.5"],
"requirements": ["music-assistant-client==1.0.8"],
"zeroconf": ["_mass._tcp.local."]
}
@@ -193,7 +193,7 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
super().__init__(mass, player_id)
self._attr_icon = self.player.icon.replace("mdi-", "mdi:")
self._attr_supported_features = SUPPORTED_FEATURES
if PlayerFeature.SYNC in self.player.supported_features:
if PlayerFeature.SET_MEMBERS in self.player.supported_features:
self._attr_supported_features |= MediaPlayerEntityFeature.GROUPING
if PlayerFeature.VOLUME_MUTE in self.player.supported_features:
self._attr_supported_features |= MediaPlayerEntityFeature.VOLUME_MUTE
@@ -400,19 +400,19 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
async def async_join_players(self, group_members: list[str]) -> None:
"""Join `group_members` as a player group with the current player."""
player_ids: list[str] = []
entity_registry = er.async_get(self.hass)
for child_entity_id in group_members:
# resolve HA entity_id to MA player_id
if (hass_state := self.hass.states.get(child_entity_id)) is None:
continue
if (mass_player_id := hass_state.attributes.get("mass_player_id")) is None:
continue
player_ids.append(mass_player_id)
await self.mass.players.player_command_sync_many(self.player_id, player_ids)
if not (entity_reg_entry := entity_registry.async_get(child_entity_id)):
raise HomeAssistantError(f"Entity {child_entity_id} not found")
# unique id is the MA player_id
player_ids.append(entity_reg_entry.unique_id)
await self.mass.players.player_command_group_many(self.player_id, player_ids)
@catch_musicassistant_error
async def async_unjoin_player(self) -> None:
"""Remove this player from any group."""
await self.mass.players.player_command_unsync(self.player_id)
await self.mass.players.player_command_ungroup(self.player_id)
@catch_musicassistant_error
async def _async_handle_play_media(
@@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["netdata"],
"quality_scale": "legacy",
"requirements": ["netdata==1.1.0"]
"requirements": ["netdata==1.3.0"]
}
+4 -1
View File
@@ -24,6 +24,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
_LOGGER = logging.getLogger(__name__)
@@ -70,7 +71,9 @@ async def async_setup_platform(
port = config[CONF_PORT]
resources = config[CONF_RESOURCES]
netdata = NetdataData(Netdata(host, port=port, timeout=20.0))
netdata = NetdataData(
Netdata(host, port=port, timeout=20.0, httpx_client=get_async_client(hass))
)
await netdata.async_update()
if netdata.api.metrics is None:
+18 -5
View File
@@ -27,7 +27,9 @@ from .entity import NordpoolBaseEntity
PARALLEL_UPDATES = 0
def get_prices(data: DeliveryPeriodData) -> dict[str, tuple[float, float, float]]:
def get_prices(
data: DeliveryPeriodData,
) -> dict[str, tuple[float | None, float, float | None]]:
"""Return previous, current and next prices.
Output: {"SE3": (10.0, 10.5, 12.1)}
@@ -39,6 +41,7 @@ def get_prices(data: DeliveryPeriodData) -> dict[str, tuple[float, float, float]
previous_time = current_time - timedelta(hours=1)
next_time = current_time + timedelta(hours=1)
price_data = data.entries
LOGGER.debug("Price data: %s", price_data)
for entry in price_data:
if entry.start <= current_time <= entry.end:
current_price_entries = entry.entry
@@ -46,10 +49,20 @@ def get_prices(data: DeliveryPeriodData) -> dict[str, tuple[float, float, float]
last_price_entries = entry.entry
if entry.start <= next_time <= entry.end:
next_price_entries = entry.entry
LOGGER.debug(
"Last price %s, current price %s, next price %s",
last_price_entries,
current_price_entries,
next_price_entries,
)
result = {}
for area, price in current_price_entries.items():
result[area] = (last_price_entries[area], price, next_price_entries[area])
result[area] = (
last_price_entries.get(area),
price,
next_price_entries.get(area),
)
LOGGER.debug("Prices: %s", result)
return result
@@ -90,7 +103,7 @@ class NordpoolDefaultSensorEntityDescription(SensorEntityDescription):
class NordpoolPricesSensorEntityDescription(SensorEntityDescription):
"""Describes Nord Pool prices sensor entity."""
value_fn: Callable[[tuple[float, float, float]], float | None]
value_fn: Callable[[tuple[float | None, float, float | None]], float | None]
@dataclass(frozen=True, kw_only=True)
@@ -136,13 +149,13 @@ PRICES_SENSOR_TYPES: tuple[NordpoolPricesSensorEntityDescription, ...] = (
NordpoolPricesSensorEntityDescription(
key="last_price",
translation_key="last_price",
value_fn=lambda data: data[0] / 1000,
value_fn=lambda data: data[0] / 1000 if data[0] else None,
suggested_display_precision=2,
),
NordpoolPricesSensorEntityDescription(
key="next_price",
translation_key="next_price",
value_fn=lambda data: data[2] / 1000,
value_fn=lambda data: data[2] / 1000 if data[2] else None,
suggested_display_precision=2,
),
)
@@ -384,6 +384,18 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
):
return self.hass.config.units.temperature_unit
if (translation_key := self._unit_of_measurement_translation_key) and (
unit_of_measurement
:= self.platform.default_language_platform_translations.get(translation_key)
):
if native_unit_of_measurement is not None:
raise ValueError(
f"Number entity {type(self)} from integration '{self.platform.platform_name}' "
f"has a translation key for unit_of_measurement '{unit_of_measurement}', "
f"but also has a native_unit_of_measurement '{native_unit_of_measurement}'"
)
return unit_of_measurement
return native_unit_of_measurement
@cached_property
+7 -1
View File
@@ -480,7 +480,13 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
NumberDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.POWER_FACTOR: {PERCENTAGE, None},
NumberDeviceClass.POWER: {UnitOfPower.WATT, UnitOfPower.KILO_WATT},
NumberDeviceClass.POWER: {
UnitOfPower.WATT,
UnitOfPower.KILO_WATT,
UnitOfPower.MEGA_WATT,
UnitOfPower.GIGA_WATT,
UnitOfPower.TERA_WATT,
},
NumberDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth),
NumberDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux),
NumberDeviceClass.PRESSURE: set(UnitOfPressure),
+5 -24
View File
@@ -18,7 +18,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="ads_blocked_today",
translation_key="ads_blocked_today",
native_unit_of_measurement="ads",
),
SensorEntityDescription(
key="ads_percentage_today",
@@ -28,38 +27,20 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="clients_ever_seen",
translation_key="clients_ever_seen",
native_unit_of_measurement="clients",
),
SensorEntityDescription(
key="dns_queries_today",
translation_key="dns_queries_today",
native_unit_of_measurement="queries",
key="dns_queries_today", translation_key="dns_queries_today"
),
SensorEntityDescription(
key="domains_being_blocked",
translation_key="domains_being_blocked",
native_unit_of_measurement="domains",
),
SensorEntityDescription(key="queries_cached", translation_key="queries_cached"),
SensorEntityDescription(
key="queries_cached",
translation_key="queries_cached",
native_unit_of_measurement="queries",
),
SensorEntityDescription(
key="queries_forwarded",
translation_key="queries_forwarded",
native_unit_of_measurement="queries",
),
SensorEntityDescription(
key="unique_clients",
translation_key="unique_clients",
native_unit_of_measurement="clients",
),
SensorEntityDescription(
key="unique_domains",
translation_key="unique_domains",
native_unit_of_measurement="domains",
key="queries_forwarded", translation_key="queries_forwarded"
),
SensorEntityDescription(key="unique_clients", translation_key="unique_clients"),
SensorEntityDescription(key="unique_domains", translation_key="unique_domains"),
)
+16 -8
View File
@@ -41,31 +41,39 @@
},
"sensor": {
"ads_blocked_today": {
"name": "Ads blocked today"
"name": "Ads blocked today",
"unit_of_measurement": "ads"
},
"ads_percentage_today": {
"name": "Ads percentage blocked today"
},
"clients_ever_seen": {
"name": "Seen clients"
"name": "Seen clients",
"unit_of_measurement": "clients"
},
"dns_queries_today": {
"name": "DNS queries today"
"name": "DNS queries today",
"unit_of_measurement": "queries"
},
"domains_being_blocked": {
"name": "Domains blocked"
"name": "Domains blocked",
"unit_of_measurement": "domains"
},
"queries_cached": {
"name": "DNS queries cached"
"name": "DNS queries cached",
"unit_of_measurement": "[%key:component::pi_hole::entity::sensor::dns_queries_today::unit_of_measurement%]"
},
"queries_forwarded": {
"name": "DNS queries forwarded"
"name": "DNS queries forwarded",
"unit_of_measurement": "[%key:component::pi_hole::entity::sensor::dns_queries_today::unit_of_measurement%]"
},
"unique_clients": {
"name": "DNS unique clients"
"name": "DNS unique clients",
"unit_of_measurement": "[%key:component::pi_hole::entity::sensor::clients_ever_seen::unit_of_measurement%]"
},
"unique_domains": {
"name": "DNS unique domains"
"name": "DNS unique domains",
"unit_of_measurement": "[%key:component::pi_hole::entity::sensor::domains_being_blocked::unit_of_measurement%]"
}
},
"update": {
+27 -24
View File
@@ -78,19 +78,18 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
self._attr_extra_state_attributes = {}
self._attr_unique_id = f"{device_id}-climate"
self._devices = coordinator.data.devices
self._gateway = coordinator.data.gateway
gateway_id: str = self._gateway["gateway_id"]
self._gateway_data = self._devices[gateway_id]
self._location = device_id
if (location := self.device.get("location")) is not None:
self._location = location
self.cdr_gateway = coordinator.data.gateway
gateway_id: str = coordinator.data.gateway["gateway_id"]
self.gateway_data = coordinator.data.devices[gateway_id]
# Determine supported features
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
if (
self.cdr_gateway["cooling_present"]
and self.cdr_gateway["smile_name"] != "Adam"
):
if self._gateway["cooling_present"] and self._gateway["smile_name"] != "Adam":
self._attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
)
@@ -116,10 +115,10 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
"""
# When no cooling available, _previous_mode is always heating
if (
"regulation_modes" in self.gateway_data
and "cooling" in self.gateway_data["regulation_modes"]
"regulation_modes" in self._gateway_data
and "cooling" in self._gateway_data["regulation_modes"]
):
mode = self.gateway_data["select_regulation_mode"]
mode = self._gateway_data["select_regulation_mode"]
if mode in ("cooling", "heating"):
self._previous_mode = mode
@@ -166,17 +165,17 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
def hvac_modes(self) -> list[HVACMode]:
"""Return a list of available HVACModes."""
hvac_modes: list[HVACMode] = []
if "regulation_modes" in self.gateway_data:
if "regulation_modes" in self._gateway_data:
hvac_modes.append(HVACMode.OFF)
if "available_schedules" in self.device:
hvac_modes.append(HVACMode.AUTO)
if self.cdr_gateway["cooling_present"]:
if "regulation_modes" in self.gateway_data:
if self.gateway_data["select_regulation_mode"] == "cooling":
if self._gateway["cooling_present"]:
if "regulation_modes" in self._gateway_data:
if self._gateway_data["select_regulation_mode"] == "cooling":
hvac_modes.append(HVACMode.COOL)
if self.gateway_data["select_regulation_mode"] == "heating":
if self._gateway_data["select_regulation_mode"] == "heating":
hvac_modes.append(HVACMode.HEAT)
else:
hvac_modes.append(HVACMode.HEAT_COOL)
@@ -192,17 +191,21 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
self._previous_action_mode(self.coordinator)
# Adam provides the hvac_action for each thermostat
if (control_state := self.device.get("control_state")) == "cooling":
return HVACAction.COOLING
if control_state == "heating":
return HVACAction.HEATING
if control_state == "preheating":
return HVACAction.PREHEATING
if control_state == "off":
if self._gateway["smile_name"] == "Adam":
if (control_state := self.device.get("control_state")) == "cooling":
return HVACAction.COOLING
if control_state == "heating":
return HVACAction.HEATING
if control_state == "preheating":
return HVACAction.PREHEATING
if control_state == "off":
return HVACAction.IDLE
return HVACAction.IDLE
heater: str = self.coordinator.data.gateway["heater_id"]
heater_data = self.coordinator.data.devices[heater]
# Anna
heater: str = self._gateway["heater_id"]
heater_data = self._devices[heater]
if heater_data["binary_sensors"]["heating_state"]:
return HVACAction.HEATING
if heater_data["binary_sensors"].get("cooling_state", False):
@@ -100,13 +100,11 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = (
QBittorrentSensorEntityDescription(
key=SENSOR_TYPE_ALL_TORRENTS,
translation_key="all_torrents",
native_unit_of_measurement="torrents",
value_fn=lambda coordinator: count_torrents_in_states(coordinator, []),
),
QBittorrentSensorEntityDescription(
key=SENSOR_TYPE_ACTIVE_TORRENTS,
translation_key="active_torrents",
native_unit_of_measurement="torrents",
value_fn=lambda coordinator: count_torrents_in_states(
coordinator, ["downloading", "uploading"]
),
@@ -114,7 +112,6 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = (
QBittorrentSensorEntityDescription(
key=SENSOR_TYPE_INACTIVE_TORRENTS,
translation_key="inactive_torrents",
native_unit_of_measurement="torrents",
value_fn=lambda coordinator: count_torrents_in_states(
coordinator, ["stalledDL", "stalledUP"]
),
@@ -122,7 +119,6 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = (
QBittorrentSensorEntityDescription(
key=SENSOR_TYPE_PAUSED_TORRENTS,
translation_key="paused_torrents",
native_unit_of_measurement="torrents",
value_fn=lambda coordinator: count_torrents_in_states(
coordinator, ["pausedDL", "pausedUP"]
),
@@ -36,16 +36,20 @@
}
},
"active_torrents": {
"name": "Active torrents"
"name": "Active torrents",
"unit_of_measurement": "torrents"
},
"inactive_torrents": {
"name": "Inactive torrents"
"name": "Inactive torrents",
"unit_of_measurement": "[%key:component::qbittorrent::entity::sensor::active_torrents::unit_of_measurement%]"
},
"paused_torrents": {
"name": "Paused torrents"
"name": "Paused torrents",
"unit_of_measurement": "[%key:component::qbittorrent::entity::sensor::active_torrents::unit_of_measurement%]"
},
"all_torrents": {
"name": "All torrents"
"name": "All torrents",
"unit_of_measurement": "[%key:component::qbittorrent::entity::sensor::active_torrents::unit_of_measurement%]"
}
},
"switch": {
@@ -40,6 +40,9 @@
"title": "[%key:component::rainbird::config::step::user::title%]",
"data": {
"duration": "Default irrigation time in minutes"
},
"data_description": {
"duration": "The default duration the sprinkler will run when turned on."
}
}
}
+9 -1
View File
@@ -740,7 +740,7 @@ class Recorder(threading.Thread):
self.schema_version = schema_status.current_version
# Do non-live data migration
migration.migrate_data_non_live(self, self.get_session, schema_status)
self._migrate_data_offline(schema_status)
# Non-live migration is now completed, remaining steps are live
self.migration_is_live = True
@@ -916,6 +916,13 @@ class Recorder(threading.Thread):
return False
def _migrate_data_offline(
self, schema_status: migration.SchemaValidationStatus
) -> None:
"""Migrate data."""
with self.hass.timeout.freeze(DOMAIN):
migration.migrate_data_non_live(self, self.get_session, schema_status)
def _migrate_schema_offline(
self, schema_status: migration.SchemaValidationStatus
) -> tuple[bool, migration.SchemaValidationStatus]:
@@ -1424,6 +1431,7 @@ class Recorder(threading.Thread):
with session_scope(session=self.get_session()) as session:
end_incomplete_runs(session, self.recorder_runs_manager.recording_start)
self.recorder_runs_manager.start(session)
self.states_manager.load_from_db(session)
self._open_event_session()
@@ -162,14 +162,14 @@ class Unused(CHAR):
"""An unused column type that behaves like a string."""
@compiles(UnusedDateTime, "mysql", "mariadb", "sqlite") # type: ignore[misc,no-untyped-call]
@compiles(Unused, "mysql", "mariadb", "sqlite") # type: ignore[misc,no-untyped-call]
@compiles(UnusedDateTime, "mysql", "mariadb", "sqlite")
@compiles(Unused, "mysql", "mariadb", "sqlite")
def compile_char_zero(type_: TypeDecorator, compiler: Any, **kw: Any) -> str:
"""Compile UnusedDateTime and Unused as CHAR(0) on mysql, mariadb, and sqlite."""
return "CHAR(0)" # Uses 1 byte on MySQL (no change on sqlite)
@compiles(Unused, "postgresql") # type: ignore[misc,no-untyped-call]
@compiles(Unused, "postgresql")
def compile_char_one(type_: TypeDecorator, compiler: Any, **kw: Any) -> str:
"""Compile Unused as CHAR(1) on postgresql."""
return "CHAR(1)" # Uses 1 byte
@@ -22,9 +22,9 @@ from homeassistant.core import HomeAssistant, State, split_entity_id
from homeassistant.helpers.recorder import get_instance
import homeassistant.util.dt as dt_util
from ..db_schema import RecorderRuns, StateAttributes, States
from ..db_schema import StateAttributes, States
from ..filters import Filters
from ..models import process_timestamp, process_timestamp_to_utc_isoformat
from ..models import process_timestamp_to_utc_isoformat
from ..models.legacy import LegacyLazyState, legacy_row_to_compressed_state
from ..util import execute_stmt_lambda_element, session_scope
from .const import (
@@ -436,7 +436,7 @@ def get_last_state_changes(
def _get_states_for_entities_stmt(
run_start: datetime,
run_start_ts: float,
utc_point_in_time: datetime,
entity_ids: list[str],
no_attributes: bool,
@@ -447,7 +447,6 @@ def _get_states_for_entities_stmt(
)
# We got an include-list of entities, accelerate the query by filtering already
# in the inner query.
run_start_ts = process_timestamp(run_start).timestamp()
utc_point_in_time_ts = dt_util.utc_to_timestamp(utc_point_in_time)
stmt += lambda q: q.join(
(
@@ -483,7 +482,7 @@ def _get_rows_with_session(
session: Session,
utc_point_in_time: datetime,
entity_ids: list[str],
run: RecorderRuns | None = None,
*,
no_attributes: bool = False,
) -> Iterable[Row]:
"""Return the states at a specific point in time."""
@@ -495,17 +494,16 @@ def _get_rows_with_session(
),
)
if run is None:
run = get_instance(hass).recorder_runs_manager.get(utc_point_in_time)
oldest_ts = get_instance(hass).states_manager.oldest_ts
if run is None or process_timestamp(run.start) > utc_point_in_time:
# History did not run before utc_point_in_time
if oldest_ts is None or oldest_ts > utc_point_in_time.timestamp():
# We don't have any states for the requested time
return []
# We have more than one entity to look at so we need to do a query on states
# since the last recorder run started.
stmt = _get_states_for_entities_stmt(
run.start, utc_point_in_time, entity_ids, no_attributes
oldest_ts, utc_point_in_time, entity_ids, no_attributes
)
return execute_stmt_lambda_element(session, stmt)
@@ -34,7 +34,6 @@ from ..models import (
LazyState,
datetime_to_timestamp_or_none,
extract_metadata_ids,
process_timestamp,
row_to_compressed_state,
)
from ..util import execute_stmt_lambda_element, session_scope
@@ -246,9 +245,9 @@ def get_significant_states_with_session(
if metadata_id is not None
and split_entity_id(entity_id)[0] in SIGNIFICANT_DOMAINS
]
run_start_ts: float | None = None
oldest_ts: float | None = None
if include_start_time_state and not (
run_start_ts := _get_run_start_ts_for_utc_point_in_time(hass, start_time)
oldest_ts := _get_oldest_possible_ts(hass, start_time)
):
include_start_time_state = False
start_time_ts = dt_util.utc_to_timestamp(start_time)
@@ -264,7 +263,7 @@ def get_significant_states_with_session(
significant_changes_only,
no_attributes,
include_start_time_state,
run_start_ts,
oldest_ts,
),
track_on=[
bool(single_metadata_id),
@@ -411,9 +410,9 @@ def state_changes_during_period(
entity_id_to_metadata_id: dict[str, int | None] = {
entity_id: single_metadata_id
}
run_start_ts: float | None = None
oldest_ts: float | None = None
if include_start_time_state and not (
run_start_ts := _get_run_start_ts_for_utc_point_in_time(hass, start_time)
oldest_ts := _get_oldest_possible_ts(hass, start_time)
):
include_start_time_state = False
start_time_ts = dt_util.utc_to_timestamp(start_time)
@@ -426,7 +425,7 @@ def state_changes_during_period(
no_attributes,
limit,
include_start_time_state,
run_start_ts,
oldest_ts,
has_last_reported,
),
track_on=[
@@ -600,17 +599,17 @@ def _get_start_time_state_for_entities_stmt(
)
def _get_run_start_ts_for_utc_point_in_time(
def _get_oldest_possible_ts(
hass: HomeAssistant, utc_point_in_time: datetime
) -> float | None:
"""Return the start time of a run."""
run = get_instance(hass).recorder_runs_manager.get(utc_point_in_time)
if (
run is not None
and (run_start := process_timestamp(run.start)) < utc_point_in_time
):
return run_start.timestamp()
# History did not run before utc_point_in_time but we still
"""Return the oldest possible timestamp.
Returns None if there are no states as old as utc_point_in_time.
"""
oldest_ts = get_instance(hass).states_manager.oldest_ts
if oldest_ts is not None and oldest_ts < utc_point_in_time.timestamp():
return oldest_ts
return None
@@ -7,7 +7,7 @@
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": [
"SQLAlchemy==2.0.31",
"SQLAlchemy==2.0.36",
"fnv-hash-fast==1.0.2",
"psutil-home-assistant==0.0.1"
]
@@ -123,6 +123,9 @@ def purge_old_data(
_purge_old_entity_ids(instance, session)
_purge_old_recorder_runs(instance, session, purge_before)
with session_scope(session=instance.get_session(), read_only=True) as session:
instance.recorder_runs_manager.load_from_db(session)
instance.states_manager.load_from_db(session)
if repack:
repack_database(instance)
return True
@@ -637,6 +637,15 @@ def find_states_to_purge(
)
def find_oldest_state() -> StatementLambdaElement:
"""Find the last_updated_ts of the oldest state."""
return lambda_stmt(
lambda: select(States.last_updated_ts).where(
States.state_id.in_(select(func.min(States.state_id)))
)
)
def find_short_term_statistics_to_purge(
purge_before: datetime, max_bind_vars: int
) -> StatementLambdaElement:
@@ -2,7 +2,15 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any, cast
from sqlalchemy.engine.row import Row
from sqlalchemy.orm.session import Session
from ..db_schema import States
from ..queries import find_oldest_state
from ..util import execute_stmt_lambda_element
class StatesManager:
@@ -13,6 +21,12 @@ class StatesManager:
self._pending: dict[str, States] = {}
self._last_committed_id: dict[str, int] = {}
self._last_reported: dict[int, float] = {}
self._oldest_ts: float | None = None
@property
def oldest_ts(self) -> float | None:
"""Return the oldest timestamp."""
return self._oldest_ts
def pop_pending(self, entity_id: str) -> States | None:
"""Pop a pending state.
@@ -44,6 +58,8 @@ class StatesManager:
recorder thread.
"""
self._pending[entity_id] = state
if self._oldest_ts is None:
self._oldest_ts = state.last_updated_ts
def update_pending_last_reported(
self, state_id: int, last_reported_timestamp: float
@@ -74,6 +90,22 @@ class StatesManager:
"""
self._last_committed_id.clear()
self._pending.clear()
self._oldest_ts = None
def load_from_db(self, session: Session) -> None:
"""Update the cache.
Must run in the recorder thread.
"""
result = cast(
Sequence[Row[Any]],
execute_stmt_lambda_element(session, find_oldest_state()),
)
if not result:
ts = None
else:
ts = result[0].last_updated_ts
self._oldest_ts = ts
def evict_purged_state_ids(self, purged_state_ids: set[int]) -> None:
"""Evict purged states from the committed states.
@@ -120,8 +120,6 @@ class PurgeTask(RecorderTask):
if purge.purge_old_data(
instance, self.purge_before, self.repack, self.apply_filter
):
with instance.get_session() as session:
instance.recorder_runs_manager.load_from_db(session)
# We always need to do the db cleanups after a purge
# is finished to ensure the WAL checkpoint and other
# tasks happen after a vacuum.
+1 -1
View File
@@ -902,7 +902,7 @@ def resolve_period(
start_time = (start_time + timedelta(days=cal_offset * 366)).replace(
month=1, day=1
)
end_time = (start_time + timedelta(days=365)).replace(day=1)
end_time = (start_time + timedelta(days=366)).replace(day=1)
start_time = dt_util.as_utc(start_time)
end_time = dt_util.as_utc(end_time)
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/refoss",
"iot_class": "local_polling",
"requirements": ["refoss-ha==1.2.4"]
"requirements": ["refoss-ha==1.2.5"]
}
@@ -176,14 +176,14 @@ class ReolinkPushBinarySensorEntity(ReolinkBinarySensorEntity):
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{self._host.webhook_id}_{self._channel}",
f"{self._host.unique_id}_{self._channel}",
self._async_handle_event,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{self._host.webhook_id}_all",
f"{self._host.unique_id}_all",
self._async_handle_event,
)
)
+7 -5
View File
@@ -536,6 +536,8 @@ class ReolinkHost:
async def renew(self) -> None:
"""Renew the subscription of motion events (lease time is 15 minutes)."""
await self._api.baichuan.check_subscribe_events()
if self._api.baichuan.events_active and self._api.subscribed(SubType.push):
# TCP push active, unsubscribe from ONVIF push because not needed
self.unregister_webhook()
@@ -721,7 +723,7 @@ class ReolinkHost:
self._hass, POLL_INTERVAL_NO_PUSH, self._poll_job
)
self._signal_write_ha_state(None)
self._signal_write_ha_state()
async def handle_webhook(
self, hass: HomeAssistant, webhook_id: str, request: Request
@@ -780,7 +782,7 @@ class ReolinkHost:
"Could not poll motion state after losing connection during receiving ONVIF event"
)
return
async_dispatcher_send(hass, f"{webhook_id}_all", {})
self._signal_write_ha_state()
return
message = data.decode("utf-8")
@@ -793,14 +795,14 @@ class ReolinkHost:
self._signal_write_ha_state(channels)
def _signal_write_ha_state(self, channels: list[int] | None) -> None:
def _signal_write_ha_state(self, channels: list[int] | None = None) -> None:
"""Update the binary sensors with async_write_ha_state."""
if channels is None:
async_dispatcher_send(self._hass, f"{self.webhook_id}_all", {})
async_dispatcher_send(self._hass, f"{self.unique_id}_all", {})
return
for channel in channels:
async_dispatcher_send(self._hass, f"{self.webhook_id}_{channel}", {})
async_dispatcher_send(self._hass, f"{self.unique_id}_{channel}", {})
@property
def event_connection(self) -> str:
@@ -18,5 +18,5 @@
"documentation": "https://www.home-assistant.io/integrations/reolink",
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"requirements": ["reolink-aio==0.11.3"]
"requirements": ["reolink-aio==0.11.4"]
}
@@ -37,7 +37,7 @@
"requirements": [
"getmac==0.9.4",
"samsungctl[websocket]==0.7.1",
"samsungtvws[async,encrypted]==2.7.0",
"samsungtvws[async,encrypted]==2.7.2",
"wakeonlan==2.1.0",
"async-upnp-client==0.41.0"
],
@@ -504,17 +504,6 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
return self.entity_description.suggested_unit_of_measurement
return None
@cached_property
def _unit_of_measurement_translation_key(self) -> str | None:
"""Return translation key for unit of measurement."""
if self.translation_key is None:
return None
platform = self.platform
return (
f"component.{platform.platform_name}.entity.{platform.domain}"
f".{self.translation_key}.unit_of_measurement"
)
@final
@property
@override

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