Compare commits

...

155 Commits

Author SHA1 Message Date
Franck Nijhof
25909f2ebd 2024.3.3 (#114005) 2024-03-22 18:47:59 +01:00
Robert Svensson
2d2249386e Bump axis to v58 (#114008) 2024-03-22 17:53:17 +01:00
Franck Nijhof
32b4814f2a Bump version to 2024.3.3 2024-03-22 16:35:21 +01:00
Franck Nijhof
d1644f3713 Update cosign to 2.2.3 (#113996) 2024-03-22 16:35:05 +01:00
Paulus Schoutsen
7b431a91b3 2024.3.2 - fix (#113978) 2024-03-21 22:35:16 -04:00
dependabot[bot]
93289c9f09 Bump home-assistant/builder from 2024.01.0 to 2024.03.5 (#113887)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-21 22:20:49 -04:00
Paulus Schoutsen
f10d924e8b 2024.3.2 (#113973)
* Streamline Notion config entry updates (refresh token and user ID) (#112832)

* Bump aioautomower to 2024.3.2 (#113162)

* Bump aioautomower to 2024.3.3 (#113430)

* Check for EA release channel for UniFi Protect (#113432)

Co-authored-by: J. Nick Koston <nick@koston.org>

* Bump `pysnmp-lextudio` to version `6.0.11` (#113463)

* Tado fix water heater (#113464)

Co-authored-by: Joostlek <joostlek@outlook.com>

* Bump aiodhcpwatcher to 0.8.2 (#113466)

* Bump axis to v55 (#113479)

* Bump croniter to 2.0.2 (#113494)

* Revert setting communication delay in Risco init (#113497)

* Bump pyrisco to 0.5.10 (#113505)

* Fix missing context when running script from template entity (#113523)

Co-authored-by: J. Nick Koston <nick@koston.org>

* Bump ical to 7.0.3 to fix local-todo persisted with invalid DTSTART values (#113526)

* Fix Airthings BLE illuminance sensor name (#113560)

* Ignore Shelly block update with cfgChanged None (#113587)

* Catch `TimeoutError` in `Brother` config flow (#113593)

* Catch TimeoutError in Brother config flow

* Update tests

* Remove unnecessary parentheses

---------

Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>

* Bump axis to v56 (#113608)

* Bump pyunifiprotect to 5.0.1 (#113630)

* Bump pyunifiprotect to 5.0.2 (#113651)

* Add removal condition to Shelly battery sensor (#113703)

Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>

* Bump aioraven to 0.5.2 (#113714)

* Fix unknown values in onewire (#113731)

* Fix unknown values in onewire

* Update tests

* Bump pymodbus v3.6.6 (#113796)

* Catch API errors in cast media_player service handlers (#113839)

* Catch API errors in cast media_player service handlers

* Remove left over debug code

* Fix wrapping of coroutine function with api_error

* Bump pychromecast to 14.0.1 (#113841)

* Fix startup race in cast (#113843)

* Redact the area of traccar server geofences (#113861)

* Bump pytedee_async to 0.2.17 (#113933)

* Bump axis to v57 (#113952)

* Bump version to 2024.3.2

---------

Co-authored-by: Aaron Bach <bachya1208@gmail.com>
Co-authored-by: Thomas55555 <59625598+Thomas55555@users.noreply.github.com>
Co-authored-by: Christopher Bailey <cbailey@mort.is>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Lex Li <425130+lextm@users.noreply.github.com>
Co-authored-by: Erwin Douna <e.douna@gmail.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com>
Co-authored-by: Diogo Gomes <diogogomes@gmail.com>
Co-authored-by: On Freund <onfreund@gmail.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Allen Porter <allen@thebends.org>
Co-authored-by: Shay Levy <levyshay1@gmail.com>
Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com>
Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
Co-authored-by: Scott K Logan <logans@cottsay.net>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: jan iversen <jancasacondor@gmail.com>
Co-authored-by: Joakim Sørensen <ludeeus@ludeeus.dev>
Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com>
2024-03-21 21:57:36 -04:00
Paulus Schoutsen
91bb321d8f Bump version to 2024.3.2 2024-03-21 20:25:19 -04:00
Robert Svensson
19ef92735c Bump axis to v57 (#113952) 2024-03-21 20:25:11 -04:00
Josef Zweck
6c274abc50 Bump pytedee_async to 0.2.17 (#113933) 2024-03-21 20:25:10 -04:00
Joakim Sørensen
1e57f52ba2 Redact the area of traccar server geofences (#113861) 2024-03-21 20:25:09 -04:00
Erik Montnemery
8056886c66 Fix startup race in cast (#113843) 2024-03-21 20:25:08 -04:00
Erik Montnemery
14c4cdc089 Bump pychromecast to 14.0.1 (#113841) 2024-03-21 20:25:07 -04:00
Erik Montnemery
4132a3d2ea Catch API errors in cast media_player service handlers (#113839)
* Catch API errors in cast media_player service handlers

* Remove left over debug code

* Fix wrapping of coroutine function with api_error
2024-03-21 20:25:07 -04:00
jan iversen
d67cd2af0c Bump pymodbus v3.6.6 (#113796) 2024-03-21 20:25:06 -04:00
epenet
33678ff5a4 Fix unknown values in onewire (#113731)
* Fix unknown values in onewire

* Update tests
2024-03-21 20:25:05 -04:00
Scott K Logan
6859bae0b1 Bump aioraven to 0.5.2 (#113714) 2024-03-21 20:25:04 -04:00
Maciej Bieniek
368586c9d1 Add removal condition to Shelly battery sensor (#113703)
Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
2024-03-21 20:25:03 -04:00
Christopher Bailey
eb8a8424a5 Bump pyunifiprotect to 5.0.2 (#113651) 2024-03-21 20:25:03 -04:00
Christopher Bailey
686487e59c Bump pyunifiprotect to 5.0.1 (#113630) 2024-03-21 20:25:02 -04:00
Robert Svensson
fa9f5bd647 Bump axis to v56 (#113608) 2024-03-21 20:25:00 -04:00
Maciej Bieniek
2e2d303291 Catch TimeoutError in Brother config flow (#113593)
* Catch TimeoutError in Brother config flow

* Update tests

* Remove unnecessary parentheses

---------

Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
2024-03-21 20:24:59 -04:00
Shay Levy
0a64ae2f7a Ignore Shelly block update with cfgChanged None (#113587) 2024-03-21 20:24:58 -04:00
Joost Lekkerkerker
a7908d8250 Fix Airthings BLE illuminance sensor name (#113560) 2024-03-21 20:24:58 -04:00
Allen Porter
4a620e015f Bump ical to 7.0.3 to fix local-todo persisted with invalid DTSTART values (#113526) 2024-03-21 20:24:57 -04:00
Erik Montnemery
d5864a40a8 Fix missing context when running script from template entity (#113523)
Co-authored-by: J. Nick Koston <nick@koston.org>
2024-03-21 20:24:56 -04:00
On Freund
05b900321b Bump pyrisco to 0.5.10 (#113505) 2024-03-21 20:24:55 -04:00
On Freund
5163b5f888 Revert setting communication delay in Risco init (#113497) 2024-03-21 20:24:27 -04:00
Diogo Gomes
8bae8fdd75 Bump croniter to 2.0.2 (#113494) 2024-03-21 20:22:08 -04:00
Robert Svensson
de966b0eb1 Bump axis to v55 (#113479) 2024-03-21 20:22:07 -04:00
J. Nick Koston
099c228169 Bump aiodhcpwatcher to 0.8.2 (#113466) 2024-03-21 20:21:32 -04:00
Erwin Douna
a5994d1d5f Tado fix water heater (#113464)
Co-authored-by: Joostlek <joostlek@outlook.com>
2024-03-21 20:16:26 -04:00
Lex Li
26b26a3b1f Bump pysnmp-lextudio to version 6.0.11 (#113463) 2024-03-21 20:16:25 -04:00
Christopher Bailey
273d01c0f4 Check for EA release channel for UniFi Protect (#113432)
Co-authored-by: J. Nick Koston <nick@koston.org>
2024-03-21 20:16:24 -04:00
Thomas55555
a167b0acaf Bump aioautomower to 2024.3.3 (#113430) 2024-03-21 20:16:23 -04:00
Thomas55555
8b00229868 Bump aioautomower to 2024.3.2 (#113162) 2024-03-21 20:15:49 -04:00
Aaron Bach
10fc40e415 Streamline Notion config entry updates (refresh token and user ID) (#112832) 2024-03-21 20:10:42 -04:00
Franck Nijhof
f7972ce9b2 2024.3.1 (#113249) 2024-03-14 16:53:07 +01:00
Chris Talkington
05c0416644 Bump pyipp to 0.15.0 (#113204)
update pyipp to 0.15.0
2024-03-14 11:05:33 +01:00
Thomas55555
63e3da1aca Add loggers to Husqvarna Automower (#113381) 2024-03-14 10:58:24 +01:00
Mike Degatano
6ca837b4e1 Supervisor issues update retries on failure (#113373) 2024-03-14 10:57:27 +01:00
Mike Degatano
45ef5a3edf Apply suggestion failures fail supervisor repair (#113372) 2024-03-14 10:57:24 +01:00
Chris Talkington
297c7c11fc Add diagnostics for IPP (#113205) 2024-03-14 10:57:21 +01:00
J. Nick Koston
cda9bf7051 Fix failing google diagnostics test (#113095) 2024-03-14 10:57:17 +01:00
Jonny Bergdahl
eb04365590 Fix Twitch auth token refresh (#112833)
* Fix for expired token

* Add auth token refresh.

* Eliminate extra auth call

* Fixed mock client

---------

Co-authored-by: Jonny Bergdahl <bergdahl@users.noreply.github.com>
2024-03-14 10:57:13 +01:00
Franck Nijhof
b88cdd78bc Hotfix import error in ZHA for 2024.3.1 patch release (#113250) 2024-03-13 19:35:43 +01:00
Franck Nijhof
525b20ca8e Bump version to 2024.3.1 2024-03-13 19:15:13 +01:00
Maciej Bieniek
5769ba023c Bump brother library to version 4.0.2 (#113235)
Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
2024-03-13 19:14:09 +01:00
Chris Talkington
f7da6b5e81 Bump rokuecp to 0.19.2 (#113198) 2024-03-13 19:14:05 +01:00
Ståle Storø Hauknes
6fdfc554a6 Bump airthings_ble to 0.7.1 (#113172)
Co-authored-by: Ståle Storø Hauknes <LaStrada@users.noreply.github.com>
2024-03-13 19:14:01 +01:00
Michael Hansen
962e5ec92a Bump intents to 2023.3.12 (#113160)
Bump intents
2024-03-13 19:13:57 +01:00
Stefan Agner
095d0d0779 Add message from Bad Request errors to HassioAPIError (#113144)
Co-authored-by: Mike Degatano <michael.degatano@gmail.com>
2024-03-13 19:13:50 +01:00
Josef Zweck
d010df7116 bump pytedee_async to 0.2.16 (#113135) 2024-03-13 19:13:07 +01:00
J. Nick Koston
a63bf74886 Bump aiodhcpwatcher to 0.8.1 (#113096) 2024-03-13 19:12:19 +01:00
Robert Svensson
cac22154a8 Bump axis to v54 (#113091)
Co-authored-by: J. Nick Koston <nick@koston.org>
2024-03-13 19:12:15 +01:00
Marcel van der Veldt
0b2322c466 Fix colormode attribute on grouped Hue light (#113071) 2024-03-13 19:12:12 +01:00
Lukas de Boer
66cd6c0d23 Bump rova to 0.4.1 (#113066) 2024-03-13 19:12:08 +01:00
Marcel van der Veldt
a2e9ecfcde Fix for controlling Hue switch entities (#113064) 2024-03-13 19:12:05 +01:00
Alistair Francis
8ac5da95f8 components/gardena_bluetooth: Improve avaliability reliability (#113056)
* components/gardena_bluetooth: Improve avaliability reliability

The available() function incorrectly returns false even though the device
is accessible.

The last_update_success property should correctly indicate if the device
isn't contactable, so we don't need to call async_address_present().
This is confirmed by seeing that no other users are calling
async_address_present() in the available() function.

This commit removes the call to async_address_present() to help fix the
sensor being unavailable when using a ESPHome BLE proxy.

Signed-off-by: Alistair Francis <alistair@alistair23.me>

---------

Signed-off-by: Alistair Francis <alistair@alistair23.me>
Co-authored-by: Joakim Plate <elupus@ecce.se>
2024-03-13 19:12:01 +01:00
Folke Lemaitre
911b39666d Fix hvac_mode for viessmann devices with heatingCooling mode (#113054) 2024-03-13 19:11:56 +01:00
Scott K Logan
2dbc63809d Fix some handle leaks in rainforest_raven (#113035)
There were leaks when
* The component was shutdown
* There was a timeout during the initial device opening

Additionally, the device was not closed/reopened when there was a
timeout reading regular data.
2024-03-13 19:10:59 +01:00
Allen Porter
a448c904d3 Bump ical to 7.0.1 and always use home assistant timezone for local todo dtstart (#113034) 2024-03-13 19:09:15 +01:00
Maciej Bieniek
7b5f879305 Fix availability for GIOS index sensors (#113021)
* Fix availability for index sensors

* Improve test_availability()

---------

Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
2024-03-13 19:08:24 +01:00
Robert Svensson
76cf25228f Bump axis to v53 (#113019) 2024-03-13 19:07:18 +01:00
Ernst Klamer
bbe88c2a5e Bump bthome-ble to 3.8.0 (#113008)
Bump bthome-ble
2024-03-13 19:07:14 +01:00
David Knowles
def4f3cb09 Add missing translation placeholder in Hydrawise (#113007)
Add missing translation placeholder
2024-03-13 19:07:11 +01:00
星野SKY
0d262ea9d4 Bump boschshcpy to 0.2.82 (#112890) 2024-03-13 19:07:08 +01:00
J. Nick Koston
fc2ca1646a Fix MJPEG fallback when still image URL is missing with basic auth (#112861)
* Fix MJPEG fallback when still image URL is missing with basic auth

I picked up an old DCS-930L (circa 2010) camera to test with
to fix #94877

* Fix MJPEG fallback when still image URL is missing with basic auth

I picked up an old DCS-930L (circa 2010) camera to test with
to fix #94877

* Fix MJPEG fallback when still image URL is missing with basic auth

I picked up an old DCS-930L (circa 2010) camera to test with
to fix #94877

* Fix MJPEG fallback when still image URL is missing with basic auth

I picked up an old DCS-930L (circa 2010) camera to test with
to fix #94877
2024-03-13 19:07:04 +01:00
Jan Bouwhuis
2d7de216a7 Fix google_asssistant sensor state reporting (#112838)
* Fix post google_assistant sensor values as float not string

* Fix aqi reporting and improve tests

* Fix _air_quality_description_for_aqi and  test
2024-03-13 19:07:01 +01:00
mrchi
8f2f9b8184 Bump openwrt-luci-rpc version to 1.1.17 (#112796) 2024-03-13 19:06:58 +01:00
Lex Li
93a01938a4 Upgrade pysnmp-lextudio to version 6.0.9 (#112795) 2024-03-13 19:06:55 +01:00
Shay Levy
70389521bf Bump bthome-ble to 3.7.0 (#112783) 2024-03-13 19:06:52 +01:00
Arie Catsman
3f22ad4eac Bump pyenphase to 1.19.2 (#112747) 2024-03-13 19:06:48 +01:00
Allen Porter
d99b9f7a70 Fix local calendar handling of empty recurrence ids (#112745)
* Fix handling of empty recurrence ids

* Revert logging changes
2024-03-13 19:06:45 +01:00
J. Nick Koston
5a125bf379 Guard against db corruption when renaming entities (#112718) 2024-03-13 19:06:41 +01:00
jan iversen
f7b64244b8 Allow duplicate names in different modbus entities (#112701)
Allow duplicate names in different entities.
2024-03-13 19:05:36 +01:00
Maciej Bieniek
c2543289b7 Downgrade pysnmp-lextudio to version 5.0.34 (#112696)
Downgrade pysnmp-lextudio to version 5.0.34

Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
2024-03-13 19:03:51 +01:00
Alin Balutoiu
9e977f2c70 Fix tado climate service (#112686) 2024-03-13 19:03:47 +01:00
Aaron Bach
e95ce2d390 Make sure Notion saves new refresh token upon startup (#112676)
* Make sure Notion saves new refresh token upon startup

* Code review

* Typing

* Smoother syntax

* Fix tests

* Fix tests for real
2024-03-13 19:03:42 +01:00
Aaron Bach
503fbfc038 Bump aionotion to 2024.03.0 (#112675) 2024-03-13 19:03:39 +01:00
Jeef
23fee438a9 Bump weatherflow4py to 0.1.17 (#112661) 2024-03-13 19:03:36 +01:00
Mike Woudenberg
1f9e369b73 Update Loqed helper for more logging (#112646)
Updates Loqed helper for more logging
2024-03-13 19:03:33 +01:00
Robert Svensson
403013b7bd Bump axis to v52 (#112632)
* Bump axis to v51

* Bump to v52
2024-03-13 19:03:30 +01:00
jan iversen
e348c7b043 Bump pymodbus to v3.6.5 (#112629) 2024-03-13 19:03:25 +01:00
Thomas55555
4db36d5ea9 Bump aioautomower to 2024.3.0 (#112627)
Fix error in Husqvarna automower in Zones dataclass
2024-03-13 19:02:32 +01:00
Erik Montnemery
aebbee681c Make hass-nabucasa a core requirement (#112623) 2024-03-13 18:50:48 +01:00
Bram Kragten
2985ab3922 Update frontend to 20240307.0 (#112620) 2024-03-13 18:47:04 +01:00
jan iversen
aa374944a1 modbus scan_interval: 0 is correct configuration (#112619) 2024-03-13 18:46:59 +01:00
jan iversen
84d14cad7f Issue warning modbus configuration when modbus configuration is empty (#112618) 2024-03-13 18:45:53 +01:00
Josef Zweck
b9a14d5eb5 Include pytedee_async logger in tedee integration (#112590)
add pytedee logger
2024-03-13 18:42:02 +01:00
Mr. Bubbles
4514f08a42 Fix incorrect filtering of unsupported locales in bring-api (#112589) 2024-03-13 18:40:54 +01:00
Erik Montnemery
2689f78925 Restore the juicenet integration (#112578) 2024-03-13 18:39:46 +01:00
Jeef
85b63c16e9 Bump weatherflow4py to 0.1.14 (#112554)
adding missing rain states
2024-03-13 18:35:59 +01:00
Jeef
4b387b5d77 Weatherflow_cloud backing lib bump (#112262)
Backing lib bump
2024-03-13 18:35:53 +01:00
Michael Hansen
fba6e5f065 Bump intents to 2024.3.6 (#112515) 2024-03-13 18:33:28 +01:00
puddly
095aab5f9d Disable updating ZHA coordinator path from discovery info (#112415)
* Never update the device path from config flows

* Bring coverage up to 100%

* Update tests/components/zha/test_config_flow.py

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

---------

Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2024-03-13 18:33:25 +01:00
Henning Claßen
c60f203aab Update the numato-gpio dependency of the numato integration to v0.12.0 (#112272)
* Update the numato-gpio dependency of the numato integration to v0.12.0

* Augment numato integration manifest with integration_type

Fulfills a requirement in the PR checklist.
2024-03-13 18:33:22 +01:00
Steven Looman
57c8d47ff3 Improve discovering upnp/igd device by always using the SSDP-discovery for the Unique Device Name (#111487)
* Always use the UDN found in the SSDP discovery, instead of the device description

* Ensure existing DeviceEntries are still matched
2024-03-13 18:33:19 +01:00
On Freund
e087ea5345 Use friendly name for camera media source (#110882) 2024-03-13 18:33:15 +01:00
FieldofClay
3c4bdebcda Ignore AussieBroadband services that don't support usage information (#110253) 2024-03-13 18:33:12 +01:00
mattmccormack
6f6f37ca24 Add auto fan mode icon (#110185) 2024-03-13 18:33:09 +01:00
Jan Stienstra
649dd433d5 Fix optional Jellyfin RunTimeTicks (#108254) 2024-03-13 18:33:00 +01:00
Franck Nijhof
1aa5a07501 2024.3.0 (#112516) 2024-03-06 18:52:11 +01:00
Franck Nijhof
efe9938b33 Bump version to 2024.3.0 2024-03-06 18:37:11 +01:00
Franck Nijhof
1b64989909 Bump version to 2024.3.0b8 2024-03-06 15:03:47 +01:00
Erik Montnemery
b480b68e3e Allow start_time >= 1.1.7 (#112500) 2024-03-06 15:03:23 +01:00
Josef Zweck
5294b492fc Bump pytedee_async to 0.2.15 (#112495) 2024-03-06 15:03:19 +01:00
Bram Kragten
080fe4cf5f Update frontend to 20240306.0 (#112492) 2024-03-06 15:03:16 +01:00
Erik Montnemery
8b2f40390b Add custom integration block list (#112481)
* Add custom integration block list

* Fix typo

* Add version condition

* Add block reason, simplify blocked versions, add tests

* Change logic for OK versions

* Add link to custom integration's issue tracker

* Add missing file

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2024-03-06 15:01:25 +01:00
Thomas55555
3b63719fad Avoid errors when there is no internet connection in Husqvarna Automower (#111101)
* Avoid errors when no internet connection

* Add error

* Create task in HA

* change from matter to automower

* tests

* Update homeassistant/components/husqvarna_automower/coordinator.py

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

* address review

* Make websocket optional

* fix aioautomower version

* Fix tests

* Use stored websocket

* reset reconnect time after sucessful connection

* Typo

* Remove comment

* Add test

* Address review

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2024-03-06 14:58:08 +01:00
Paulus Schoutsen
061ae756ac Bump version to 2024.3.0b7 2024-03-05 23:43:11 -05:00
Matthias Alphart
862bd8ff07 Update xknx to 2.12.2 - Fix thread leak on unsuccessful connections (#112450)
Update xknx to 2.12.2
2024-03-05 23:43:07 -05:00
G Johansson
742710443a Bump holidays to 0.44 (#112442) 2024-03-05 23:43:06 -05:00
Robert Svensson
015aeadf88 Fix handling missing parameter by bumping axis library to v50 (#112437)
Fix handling missing parameter
2024-03-05 23:43:05 -05:00
Robert Svensson
b8b654a160 Do not use list comprehension in async_add_entities in Unifi (#112435)
Do not use list comprehension in async_add_entities
2024-03-05 23:43:04 -05:00
jan iversen
3c5b5ca49b Allow duplicate modbus addresses on different devices (#112434) 2024-03-05 23:43:04 -05:00
Mr. Bubbles
fb789d95ed Bump bring-api to 0.5.5 (#112266)
Fix KeyError listArticleLanguage
2024-03-05 23:43:03 -05:00
Álvaro Fernández Rojas
2e6906c8d4 Update aioairzone to v0.7.6 (#112264)
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2024-03-05 23:43:02 -05:00
Luke Lashley
cc8d44bbd1 Bump python_roborock to 0.40.0 (#112238)
* bump to python_roborock 0.40.0

* manifest went away in merge?
2024-03-05 23:43:01 -05:00
Robert Svensson
0ad56de6fc Fix deCONZ light entity might not report a supported color mode (#112116)
* Handle case where deCONZ light entity might not report a supported color mode

* If in an unknown color mode set ColorMode.UNKNOWN

* Fix comment from external discussion
2024-03-05 23:43:00 -05:00
Paulus Schoutsen
dedd7a5a41 Bump version to 2024.3.0b6 2024-03-04 13:04:03 -05:00
Paul Bottein
44c961720c Update frontend to 20240304.0 (#112263) 2024-03-04 13:03:50 -05:00
Allen Porter
79b1d6df1b Add rainbird request debouncer and immediately update entity switch state (#112152) 2024-03-04 13:03:49 -05:00
Allen Porter
274ab2328e Limit rainbird aiohttp client session to a single connection (#112146)
Limit rainbird to a single open http connection
2024-03-04 13:03:48 -05:00
David F. Mulcahey
93ee900cb3 Fix ZHA groups page (#112140)
* Fix ZHA groups page

* test
2024-03-04 13:02:23 -05:00
Michael
62474967c9 Ignore failing gas stations in Tankerkoening (#112125) 2024-03-04 13:02:22 -05:00
starkillerOG
2cdc8d5f69 Bump reolink-aio to 0.8.9 (#112124)
* Update strings.json

* Bump reolink-aio to 0.8.9
2024-03-04 13:02:21 -05:00
David F. Mulcahey
4863c94824 Bump Zigpy to 0.63.4 (#112117) 2024-03-04 13:02:20 -05:00
Mr. Bubbles
193332da74 Bump bring-api to 0.5.4 (#111654) 2024-03-04 13:02:20 -05:00
Christopher Fenner
9926296d35 Handle exception in ViCare integration (#111128) 2024-03-04 13:02:19 -05:00
Paulus Schoutsen
bb6f8b9d57 Bump version to 2024.3.0b5 2024-03-02 22:09:17 -05:00
J. Nick Koston
780f6e8974 Avoid expensive inspect calls in config validators (#112085)
* Avoid expensive inspect calls in config validators

inspect has a performance problem https://github.com/python/cpython/issues/92041

We now avoid calling inspect unless we are going to log

* remove unused

* reduce

* get_integration_logger
2024-03-02 22:08:59 -05:00
J. Nick Koston
ab30d44184 Fix executor being overloaded in caldav (#112084)
Migrate to using a single executor job instead of creating
one per calendar. If the user had a lot of calendars the
executor would get overloaded
2024-03-02 22:08:58 -05:00
J. Nick Koston
e23f737fa7 Fix bootstrap being fetched three times during unifiprotect startup (#112082)
We always fetch it to check if the device is online.
Avoid fetching it again for migration by passing
it to the migrators
2024-03-02 22:08:57 -05:00
J. Nick Koston
b8e3bb8eb8 Ensure all homekit_controller controllers are imported in advance (#112079)
* Ensure all homekit_controllers are imported in advance

We want to avoid importing them in the event loop later

* Ensure all homekit_controllers are imported in advance

We want to avoid importing them in the event loop later
2024-03-02 22:08:56 -05:00
elmurato
12574bca8b Fix setup failure due to temporary DNS issue in Minecraft Server (#112068)
Change ConfigEntryError to ConfigEntryNotReady on failed init
2024-03-02 22:08:55 -05:00
David Knowles
f16ea54b4f Bump pydrawise to 2024.3.0 (#112066) 2024-03-02 22:08:54 -05:00
Paulus Schoutsen
ad52bf608f Only load camera prefs once (#112064) 2024-03-02 22:08:53 -05:00
Isak Nyberg
46ee52f4ef Add device class for permobil record distance sensor (#112062)
fix record_distance device_class
2024-03-02 22:08:53 -05:00
Shay Levy
88fb44bbba Bump bthome-ble to 3.6.0 (#112060)
* Bump bthome-ble to 3.6.0

* Fix discovery info typing
2024-03-02 22:07:59 -05:00
J. Nick Koston
de5e626430 Bump unifi-discovery to 1.1.8 (#112056) 2024-03-02 22:03:13 -05:00
J. Nick Koston
1bcdba1b4b Import anonymize_data in unifiprotect init to avoid it being imported in the event loop (#112052)
Improve anonymize_data in unifiprotect init to avoid it being imported in the event loop
2024-03-02 22:03:12 -05:00
Paulus Schoutsen
a4353cf39d Bump version to 2024.3.0b4 2024-03-02 13:24:12 -05:00
Jeef
63192f2291 Bump weatherflow4py to v0.1.12 (#112040)
Backing lib bump
2024-03-02 13:24:05 -05:00
Joakim Sørensen
675b7ca7ba Fix config schema for velux (#112037) 2024-03-02 13:24:05 -05:00
Joakim Sørensen
df5eb552a0 Use description key instead of name for Tibber RT unique ID (#112035)
* Use translation key instead of name for Tibber RT unique ID

* migration

* use decription.key instead
2024-03-02 13:24:04 -05:00
Álvaro Fernández Rojas
5017f4a2c7 Update aioairzone-cloud to v0.4.5 (#112034)
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2024-03-02 13:24:03 -05:00
Chris Helming
92d3dccb94 Fix minor language issues in strings.json (#112006)
language fix: allow -> allows
2024-03-02 13:24:01 -05:00
David F. Mulcahey
2c38b5ee7b Bump Zigpy to 0.63.3 (#112002) 2024-03-02 13:24:00 -05:00
Paulus Schoutsen
435bb50d29 Update reporting for media_source.async_resolve_media (#111969)
* Update reporting for media_source.async_resolve_media

* Don't raise on core

* Fix tests
2024-03-02 13:23:59 -05:00
Paul Bottein
005493bb5a Update frontend to 20240301.0 (#111961) 2024-03-02 13:23:59 -05:00
Mick Vleeshouwer
838a4e4f7b Bump pyOverkiz to 1.13.8 (#111930)
Bump pyoverkiz to 1.13.8
2024-03-02 13:23:58 -05:00
Paulus Schoutsen
bc47c80bbf 2024.2.5 (#111648) 2024-02-27 13:23:44 -05:00
Paulus Schoutsen
aabaa30fa7 2024.2.4 (#111441) 2024-02-26 11:17:13 -05:00
Franck Nijhof
1ee39275fc 2024.2.3 (#111133) 2024-02-22 16:08:18 +01:00
210 changed files with 3337 additions and 905 deletions

View File

@@ -639,6 +639,12 @@ omit =
homeassistant/components/izone/climate.py
homeassistant/components/izone/discovery.py
homeassistant/components/joaoapps_join/*
homeassistant/components/juicenet/__init__.py
homeassistant/components/juicenet/device.py
homeassistant/components/juicenet/entity.py
homeassistant/components/juicenet/number.py
homeassistant/components/juicenet/sensor.py
homeassistant/components/juicenet/switch.py
homeassistant/components/justnimbus/coordinator.py
homeassistant/components/justnimbus/entity.py
homeassistant/components/justnimbus/sensor.py

View File

@@ -207,7 +207,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2024.01.0
uses: home-assistant/builder@2024.03.5
with:
args: |
$BUILD_ARGS \
@@ -284,7 +284,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2024.01.0
uses: home-assistant/builder@2024.03.5
with:
args: |
$BUILD_ARGS \
@@ -343,7 +343,7 @@ jobs:
- name: Install Cosign
uses: sigstore/cosign-installer@v3.4.0
with:
cosign-release: "v2.0.2"
cosign-release: "v2.2.3"
- name: Login to DockerHub
uses: docker/login-action@v3.0.0

View File

@@ -669,6 +669,8 @@ build.json @home-assistant/supervisor
/tests/components/jellyfin/ @j-stienstra @ctalkington
/homeassistant/components/jewish_calendar/ @tsvi
/tests/components/jewish_calendar/ @tsvi
/homeassistant/components/juicenet/ @jesserockz
/tests/components/juicenet/ @jesserockz
/homeassistant/components/justnimbus/ @kvanzuijlen
/tests/components/justnimbus/ @kvanzuijlen
/homeassistant/components/jvc_projector/ @SteveEasley @msavazzi

View File

@@ -27,7 +27,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {})
address = entry.unique_id
elevation = hass.config.elevation
is_metric = hass.config.units is METRIC_SYSTEM
assert address is not None
@@ -40,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
f"Could not find Airthings device with address {address}"
)
airthings = AirthingsBluetoothDeviceData(_LOGGER, elevation, is_metric)
airthings = AirthingsBluetoothDeviceData(_LOGGER, is_metric)
async def _async_update_method() -> AirthingsDevice:
"""Get data from Airthings BLE."""

View File

@@ -24,5 +24,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
"iot_class": "local_polling",
"requirements": ["airthings-ble==0.6.1"]
"requirements": ["airthings-ble==0.7.1"]
}

View File

@@ -16,7 +16,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
EntityCategory,
Platform,
@@ -106,8 +105,8 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
),
"illuminance": SensorEntityDescription(
key="illuminance",
device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement=LIGHT_LUX,
translation_key="illuminance",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
}
@@ -222,7 +221,7 @@ class AirthingsSensor(
manufacturer=airthings_device.manufacturer,
hw_version=airthings_device.hw_version,
sw_version=airthings_device.sw_version,
model=airthings_device.model,
model=airthings_device.model.name,
)
@property

View File

@@ -33,6 +33,9 @@
},
"radon_longterm_level": {
"name": "Radon longterm level"
},
"illuminance": {
"name": "[%key:component::sensor::entity_component::illuminance::name%]"
}
}
}

View File

@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==0.7.5"]
"requirements": ["aioairzone==0.7.6"]
}

View File

@@ -24,6 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
options = ConnectionOptions(
entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD],
True,
)
airzone = AirzoneCloudApi(aiohttp_client.async_get_clientsession(hass), options)

View File

@@ -94,6 +94,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
ConnectionOptions(
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
False,
),
)

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_polling",
"loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.3.8"]
"requirements": ["aioairzone-cloud==0.4.5"]
}

View File

@@ -31,9 +31,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async_get_clientsession(hass),
)
# Ignore services that don't support usage data
ignore_types = FETCH_TYPES + ["Hardware"]
try:
await client.login()
services = await client.get_services(drop_types=FETCH_TYPES)
services = await client.get_services(drop_types=ignore_types)
except AuthenticationException as exc:
raise ConfigEntryAuthFailed() from exc
except ClientError as exc:

View File

@@ -26,7 +26,7 @@
"iot_class": "local_push",
"loggers": ["axis"],
"quality_scale": "platinum",
"requirements": ["axis==49"],
"requirements": ["axis==58"],
"ssdp": [
{
"manufacturer": "AXIS"

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/bosch_shc",
"iot_class": "local_push",
"loggers": ["boschshcpy"],
"requirements": ["boschshcpy==0.2.75"],
"requirements": ["boschshcpy==0.2.82"],
"zeroconf": [
{
"type": "_http._tcp.local.",

View File

@@ -6,7 +6,7 @@ import logging
from bring_api.bring import Bring
from bring_api.exceptions import BringParseException, BringRequestException
from bring_api.types import BringItemsResponse, BringList
from bring_api.types import BringList, BringPurchase
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -20,8 +20,8 @@ _LOGGER = logging.getLogger(__name__)
class BringData(BringList):
"""Coordinator data class."""
purchase_items: list[BringItemsResponse]
recently_items: list[BringItemsResponse]
purchase_items: list[BringPurchase]
recently_items: list[BringPurchase]
class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/bring",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["bring-api==0.4.1"]
"requirements": ["bring-api==0.5.6"]
}

View File

@@ -2,8 +2,10 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import uuid
from bring_api.exceptions import BringRequestException
from bring_api.types import BringItem, BringItemOperation
from homeassistant.components.todo import (
TodoItem,
@@ -76,7 +78,7 @@ class BringTodoListEntity(
return [
*(
TodoItem(
uid=item["itemId"],
uid=item["uuid"],
summary=item["itemId"],
description=item["specification"] or "",
status=TodoItemStatus.NEEDS_ACTION,
@@ -85,7 +87,7 @@ class BringTodoListEntity(
),
*(
TodoItem(
uid=item["itemId"],
uid=item["uuid"],
summary=item["itemId"],
description=item["specification"] or "",
status=TodoItemStatus.COMPLETED,
@@ -103,7 +105,10 @@ class BringTodoListEntity(
"""Add an item to the To-do list."""
try:
await self.coordinator.bring.save_item(
self.bring_list["listUuid"], item.summary, item.description or ""
self.bring_list["listUuid"],
item.summary,
item.description or "",
str(uuid.uuid4()),
)
except BringRequestException as e:
raise HomeAssistantError("Unable to save todo item for bring") from e
@@ -121,60 +126,69 @@ class BringTodoListEntity(
- Completed items will move to the "completed" section in home assistant todo
list and get moved to the recently list in bring
- Bring items do not have unique identifiers and are using the
name/summery/title. Therefore the name is not to be changed! Should a name
be changed anyway, a new item will be created instead and no update for
this item is performed and on the next cloud pull update, it will get
cleared and replaced seamlessly
- Bring shows some odd behaviour when renaming items. This is because Bring
did not have unique identifiers for items in the past and this is still
a relic from it. Therefore the name is not to be changed! Should a name
be changed anyway, the item will be deleted and a new item will be created
instead and no update for this item is performed and on the next cloud pull
update, it will get cleared and replaced seamlessly.
"""
bring_list = self.bring_list
bring_purchase_item = next(
(i for i in bring_list["purchase_items"] if i["itemId"] == item.uid),
(i for i in bring_list["purchase_items"] if i["uuid"] == item.uid),
None,
)
bring_recently_item = next(
(i for i in bring_list["recently_items"] if i["itemId"] == item.uid),
(i for i in bring_list["recently_items"] if i["uuid"] == item.uid),
None,
)
current_item = bring_purchase_item or bring_recently_item
if TYPE_CHECKING:
assert item.uid
assert current_item
if item.status == TodoItemStatus.COMPLETED and bring_purchase_item:
await self.coordinator.bring.complete_item(
bring_list["listUuid"],
item.uid,
)
elif item.status == TodoItemStatus.NEEDS_ACTION and bring_recently_item:
await self.coordinator.bring.save_item(
bring_list["listUuid"],
item.uid,
)
elif item.summary == item.uid:
if item.summary == current_item["itemId"]:
try:
await self.coordinator.bring.update_item(
await self.coordinator.bring.batch_update_list(
bring_list["listUuid"],
item.uid,
item.description or "",
BringItem(
itemId=item.summary,
spec=item.description,
uuid=item.uid,
),
BringItemOperation.ADD
if item.status == TodoItemStatus.NEEDS_ACTION
else BringItemOperation.COMPLETE,
)
except BringRequestException as e:
raise HomeAssistantError("Unable to update todo item for bring") from e
else:
try:
await self.coordinator.bring.remove_item(
await self.coordinator.bring.batch_update_list(
bring_list["listUuid"],
item.uid,
)
await self.coordinator.bring.save_tem(
bring_list["listUuid"],
item.summary,
item.description or "",
[
BringItem(
itemId=current_item["itemId"],
spec=item.description,
uuid=item.uid,
operation=BringItemOperation.REMOVE,
),
BringItem(
itemId=item.summary,
spec=item.description,
uuid=str(uuid.uuid4()),
operation=BringItemOperation.ADD
if item.status == TodoItemStatus.NEEDS_ACTION
else BringItemOperation.COMPLETE,
),
],
)
except BringRequestException as e:
raise HomeAssistantError("Unable to replace todo item for bring") from e
@@ -182,12 +196,21 @@ class BringTodoListEntity(
async def async_delete_todo_items(self, uids: list[str]) -> None:
"""Delete an item from the To-do list."""
for uid in uids:
try:
await self.coordinator.bring.remove_item(
self.bring_list["listUuid"], uid
)
except BringRequestException as e:
raise HomeAssistantError("Unable to delete todo item for bring") from e
try:
await self.coordinator.bring.batch_update_list(
self.bring_list["listUuid"],
[
BringItem(
itemId=uid,
spec="",
uuid=uid,
)
for uid in uids
],
BringItemOperation.REMOVE,
)
except BringRequestException as e:
raise HomeAssistantError("Unable to delete todo item for bring") from e
await self.coordinator.async_refresh()

View File

@@ -33,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
brother = await Brother.create(
host, printer_type=printer_type, snmp_engine=snmp_engine
)
except (ConnectionError, SnmpError) as error:
except (ConnectionError, SnmpError, TimeoutError) as error:
raise ConfigEntryNotReady from error
coordinator = BrotherDataUpdateCoordinator(hass, brother)

View File

@@ -58,7 +58,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry(title=title, data=user_input)
except InvalidHost:
errors[CONF_HOST] = "wrong_host"
except ConnectionError:
except (ConnectionError, TimeoutError):
errors["base"] = "cannot_connect"
except SnmpError:
errors["base"] = "snmp_error"
@@ -88,7 +88,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self.brother.async_update()
except UnsupportedModelError:
return self.async_abort(reason="unsupported_model")
except (ConnectionError, SnmpError):
except (ConnectionError, SnmpError, TimeoutError):
return self.async_abort(reason="cannot_connect")
# Check if already configured

View File

@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["brother", "pyasn1", "pysmi", "pysnmp"],
"quality_scale": "platinum",
"requirements": ["brother==4.0.0"],
"requirements": ["brother==4.0.2"],
"zeroconf": [
{
"type": "_printer._tcp.local.",

View File

@@ -1,4 +1,5 @@
"""Config flow for BTHome Bluetooth integration."""
from __future__ import annotations
from collections.abc import Mapping
@@ -11,7 +12,7 @@ import voluptuous as vol
from homeassistant.components import onboarding
from homeassistant.components.bluetooth import (
BluetoothServiceInfo,
BluetoothServiceInfoBleak,
async_discovered_service_info,
)
from homeassistant.config_entries import ConfigFlow
@@ -26,11 +27,11 @@ class Discovery:
"""A discovered bluetooth device."""
title: str
discovery_info: BluetoothServiceInfo
discovery_info: BluetoothServiceInfoBleak
device: DeviceData
def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str:
def _title(discovery_info: BluetoothServiceInfoBleak, device: DeviceData) -> str:
return device.title or device.get_device_name() or discovery_info.name
@@ -41,12 +42,12 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize the config flow."""
self._discovery_info: BluetoothServiceInfo | None = None
self._discovery_info: BluetoothServiceInfoBleak | None = None
self._discovered_device: DeviceData | None = None
self._discovered_devices: dict[str, Discovery] = {}
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfo
self, discovery_info: BluetoothServiceInfoBleak
) -> FlowResult:
"""Handle the bluetooth discovery step."""
await self.async_set_unique_id(discovery_info.address)

View File

@@ -20,5 +20,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bthome",
"iot_class": "local_push",
"requirements": ["bthome-ble==3.5.0"]
"requirements": ["bthome-ble==3.8.0"]
}

View File

@@ -1,6 +1,5 @@
"""Library for working with CalDAV api."""
import asyncio
import caldav
@@ -13,20 +12,13 @@ async def async_get_calendars(
"""Get all calendars that support the specified component."""
def _get_calendars() -> list[caldav.Calendar]:
return client.principal().calendars()
calendars = await hass.async_add_executor_job(_get_calendars)
components_results = await asyncio.gather(
*[
hass.async_add_executor_job(calendar.get_supported_components)
for calendar in calendars
return [
calendar
for calendar in client.principal().calendars()
if component in calendar.get_supported_components()
]
)
return [
calendar
for calendar, supported_components in zip(calendars, components_results)
if component in supported_components
]
return await hass.async_add_executor_job(_get_calendars)
def get_attr_value(obj: caldav.CalendarObjectResource, attribute: str) -> str | None:

View File

@@ -189,6 +189,11 @@ def _validate_rrule(value: Any) -> str:
return str(value)
def _empty_as_none(value: str | None) -> str | None:
"""Convert any empty string values to None."""
return value or None
CREATE_EVENT_SERVICE = "create_event"
CREATE_EVENT_SCHEMA = vol.All(
cv.has_at_least_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN),
@@ -733,7 +738,9 @@ async def handle_calendar_event_create(
vol.Required("type"): "calendar/event/delete",
vol.Required("entity_id"): cv.entity_id,
vol.Required(EVENT_UID): cv.string,
vol.Optional(EVENT_RECURRENCE_ID): cv.string,
vol.Optional(EVENT_RECURRENCE_ID): vol.Any(
vol.All(cv.string, _empty_as_none), None
),
vol.Optional(EVENT_RECURRENCE_RANGE): cv.string,
}
)
@@ -777,7 +784,9 @@ async def handle_calendar_event_delete(
vol.Required("type"): "calendar/event/update",
vol.Required("entity_id"): cv.entity_id,
vol.Required(EVENT_UID): cv.string,
vol.Optional(EVENT_RECURRENCE_ID): cv.string,
vol.Optional(EVENT_RECURRENCE_ID): vol.Any(
vol.All(cv.string, _empty_as_none), None
),
vol.Optional(EVENT_RECURRENCE_RANGE): cv.string,
vol.Required(CONF_EVENT): WEBSOCKET_EVENT_SCHEMA,
}

View File

@@ -391,6 +391,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
prefs = CameraPreferences(hass)
await prefs.async_load()
hass.data[DATA_CAMERA_PREFS] = prefs
hass.http.register_view(CameraImageView(component))

View File

@@ -12,6 +12,7 @@ from homeassistant.components.media_source.models import (
PlayMedia,
)
from homeassistant.components.stream import FORMAT_CONTENT_TYPE, HLS_PROVIDER
from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_component import EntityComponent
@@ -25,13 +26,20 @@ async def async_get_media_source(hass: HomeAssistant) -> CameraMediaSource:
return CameraMediaSource(hass)
def _media_source_for_camera(camera: Camera, content_type: str) -> BrowseMediaSource:
def _media_source_for_camera(
hass: HomeAssistant, camera: Camera, content_type: str
) -> BrowseMediaSource:
camera_state = hass.states.get(camera.entity_id)
title = camera.name
if camera_state:
title = camera_state.attributes.get(ATTR_FRIENDLY_NAME, camera.name)
return BrowseMediaSource(
domain=DOMAIN,
identifier=camera.entity_id,
media_class=MediaClass.VIDEO,
media_content_type=content_type,
title=camera.name,
title=title,
thumbnail=f"/api/camera_proxy/{camera.entity_id}",
can_play=True,
can_expand=False,
@@ -89,7 +97,7 @@ class CameraMediaSource(MediaSource):
async def _filter_browsable_camera(camera: Camera) -> BrowseMediaSource | None:
stream_type = camera.frontend_stream_type
if stream_type is None:
return _media_source_for_camera(camera, camera.content_type)
return _media_source_for_camera(self.hass, camera, camera.content_type)
if not can_stream_hls:
return None
@@ -97,7 +105,7 @@ class CameraMediaSource(MediaSource):
if stream_type != StreamType.HLS and not (await camera.stream_source()):
return None
return _media_source_for_camera(camera, content_type)
return _media_source_for_camera(self.hass, camera, content_type)
component: EntityComponent[Camera] = self.hass.data[DOMAIN]
results = await asyncio.gather(

View File

@@ -29,6 +29,8 @@ class DynamicStreamSettings:
class CameraPreferences:
"""Handle camera preferences."""
_preload_prefs: dict[str, dict[str, bool | Orientation]]
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize camera prefs."""
self._hass = hass
@@ -41,6 +43,10 @@ class CameraPreferences:
str, DynamicStreamSettings
] = {}
async def async_load(self) -> None:
"""Initialize the camera preferences."""
self._preload_prefs = await self._store.async_load() or {}
async def async_update(
self,
entity_id: str,
@@ -63,9 +69,8 @@ class CameraPreferences:
if preload_stream is not UNDEFINED:
if dynamic_stream_settings:
dynamic_stream_settings.preload_stream = preload_stream
preload_prefs = await self._store.async_load() or {}
preload_prefs[entity_id] = {PREF_PRELOAD_STREAM: preload_stream}
await self._store.async_save(preload_prefs)
self._preload_prefs[entity_id] = {PREF_PRELOAD_STREAM: preload_stream}
await self._store.async_save(self._preload_prefs)
if orientation is not UNDEFINED:
if (registry := er.async_get(self._hass)).async_get(entity_id):
@@ -91,10 +96,10 @@ class CameraPreferences:
# Get orientation setting from entity registry
reg_entry = er.async_get(self._hass).async_get(entity_id)
er_prefs: Mapping = reg_entry.options.get(DOMAIN, {}) if reg_entry else {}
preload_prefs = await self._store.async_load() or {}
settings = DynamicStreamSettings(
preload_stream=cast(
bool, preload_prefs.get(entity_id, {}).get(PREF_PRELOAD_STREAM, False)
bool,
self._preload_prefs.get(entity_id, {}).get(PREF_PRELOAD_STREAM, False),
),
orientation=er_prefs.get(PREF_ORIENTATION, Orientation.NO_TRANSFORM),
)

View File

@@ -24,9 +24,9 @@ PLATFORMS = [Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Cast from a config entry."""
hass.data[DOMAIN] = {"cast_platform": {}, "unknown_models": {}}
await home_assistant_cast.async_setup_ha_cast(hass, entry)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
hass.data[DOMAIN] = {"cast_platform": {}, "unknown_models": {}}
await async_process_integration_platforms(hass, DOMAIN, _register_cast_platform)
return True

View File

@@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/cast",
"iot_class": "local_polling",
"loggers": ["casttube", "pychromecast"],
"requirements": ["PyChromecast==14.0.0"],
"requirements": ["PyChromecast==14.0.1"],
"zeroconf": ["_googlecast._tcp.local."]
}

View File

@@ -4,9 +4,10 @@ from __future__ import annotations
from collections.abc import Callable
from contextlib import suppress
from datetime import datetime
from functools import wraps
import json
import logging
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar
import pychromecast
from pychromecast.controllers.homeassistant import HomeAssistantController
@@ -18,6 +19,7 @@ from pychromecast.controllers.media import (
)
from pychromecast.controllers.multizone import MultizoneManager
from pychromecast.controllers.receiver import VOLUME_CONTROL_TYPE_FIXED
from pychromecast.error import PyChromecastError
from pychromecast.quick_play import quick_play
from pychromecast.socket_client import (
CONNECTION_STATUS_CONNECTED,
@@ -83,6 +85,34 @@ APP_IDS_UNRELIABLE_MEDIA_INFO = ("Netflix",)
CAST_SPLASH = "https://www.home-assistant.io/images/cast/splash.png"
_CastDeviceT = TypeVar("_CastDeviceT", bound="CastDevice")
_R = TypeVar("_R")
_P = ParamSpec("_P")
_FuncType = Callable[Concatenate[_CastDeviceT, _P], _R]
_ReturnFuncType = Callable[Concatenate[_CastDeviceT, _P], _R]
def api_error(
func: _FuncType[_CastDeviceT, _P, _R],
) -> _ReturnFuncType[_CastDeviceT, _P, _R]:
"""Handle PyChromecastError and reraise a HomeAssistantError."""
@wraps(func)
def wrapper(self: _CastDeviceT, *args: _P.args, **kwargs: _P.kwargs) -> _R:
"""Wrap a CastDevice method."""
try:
return_value = func(self, *args, **kwargs)
except PyChromecastError as err:
raise HomeAssistantError(
f"{self.__class__.__name__}.{func.__name__} Failed: {err}"
) from err
return return_value
return wrapper
@callback
def _async_create_cast_device(hass: HomeAssistant, info: ChromecastInfo):
"""Create a CastDevice entity or dynamic group from the chromecast object.
@@ -476,6 +506,21 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
return media_controller
@api_error
def _quick_play(self, app_name: str, data: dict[str, Any]) -> None:
"""Launch the app `app_name` and start playing media defined by `data`."""
quick_play(self._get_chromecast(), app_name, data)
@api_error
def _quit_app(self) -> None:
"""Quit the currently running app."""
self._get_chromecast().quit_app()
@api_error
def _start_app(self, app_id: str) -> None:
"""Start an app."""
self._get_chromecast().start_app(app_id)
def turn_on(self) -> None:
"""Turn on the cast device."""
@@ -486,52 +531,61 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
if chromecast.app_id is not None:
# Quit the previous app before starting splash screen or media player
chromecast.quit_app()
self._quit_app()
# The only way we can turn the Chromecast is on is by launching an app
if chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST:
app_data = {"media_id": CAST_SPLASH, "media_type": "image/png"}
quick_play(chromecast, "default_media_receiver", app_data)
self._quick_play("default_media_receiver", app_data)
else:
chromecast.start_app(pychromecast.config.APP_MEDIA_RECEIVER)
self._start_app(pychromecast.config.APP_MEDIA_RECEIVER)
@api_error
def turn_off(self) -> None:
"""Turn off the cast device."""
self._get_chromecast().quit_app()
@api_error
def mute_volume(self, mute: bool) -> None:
"""Mute the volume."""
self._get_chromecast().set_volume_muted(mute)
@api_error
def set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1."""
self._get_chromecast().set_volume(volume)
@api_error
def media_play(self) -> None:
"""Send play command."""
media_controller = self._media_controller()
media_controller.play()
@api_error
def media_pause(self) -> None:
"""Send pause command."""
media_controller = self._media_controller()
media_controller.pause()
@api_error
def media_stop(self) -> None:
"""Send stop command."""
media_controller = self._media_controller()
media_controller.stop()
@api_error
def media_previous_track(self) -> None:
"""Send previous track command."""
media_controller = self._media_controller()
media_controller.queue_prev()
@api_error
def media_next_track(self) -> None:
"""Send next track command."""
media_controller = self._media_controller()
media_controller.queue_next()
@api_error
def media_seek(self, position: float) -> None:
"""Seek the media to a specific location."""
media_controller = self._media_controller()
@@ -644,7 +698,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
if "app_id" in app_data:
app_id = app_data.pop("app_id")
_LOGGER.info("Starting Cast app by ID %s", app_id)
await self.hass.async_add_executor_job(chromecast.start_app, app_id)
await self.hass.async_add_executor_job(self._start_app, app_id)
if app_data:
_LOGGER.warning(
"Extra keys %s were ignored. Please use app_name to cast media",
@@ -655,7 +709,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
app_name = app_data.pop("app_name")
try:
await self.hass.async_add_executor_job(
quick_play, chromecast, app_name, app_data
self._quick_play, app_name, app_data
)
except NotImplementedError:
_LOGGER.error("App %s not supported", app_name)
@@ -729,7 +783,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
app_data,
)
await self.hass.async_add_executor_job(
quick_play, chromecast, "default_media_receiver", app_data
self._quick_play, "default_media_receiver", app_data
)
def _media_status(self):

View File

@@ -6,6 +6,7 @@
"fan_mode": {
"default": "mdi:circle-medium",
"state": {
"auto": "mdi:fan-auto",
"diffuse": "mdi:weather-windy",
"focus": "mdi:target",
"high": "mdi:speedometer",

View File

@@ -7,5 +7,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["hassil==1.6.1", "home-assistant-intents==2024.2.28"]
"requirements": ["hassil==1.6.1", "home-assistant-intents==2024.3.12"]
}

View File

@@ -165,6 +165,7 @@ class DeconzBaseLight(DeconzDevice[_LightDeviceT], LightEntity):
"""Representation of a deCONZ light."""
TYPE = DOMAIN
_attr_color_mode = ColorMode.UNKNOWN
def __init__(self, device: _LightDeviceT, gateway: DeconzGateway) -> None:
"""Set up light."""

View File

@@ -15,7 +15,7 @@
],
"quality_scale": "internal",
"requirements": [
"aiodhcpwatcher==0.8.0",
"aiodhcpwatcher==0.8.2",
"aiodiscover==1.6.1",
"cached_ipaddress==0.3.0"
]

View File

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

View File

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

View File

@@ -12,7 +12,6 @@ from gardena_bluetooth.exceptions import (
)
from gardena_bluetooth.parse import Characteristic, CharacteristicType
from homeassistant.components import bluetooth
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
@@ -117,13 +116,7 @@ class GardenaBluetoothEntity(CoordinatorEntity[Coordinator]):
@property
def available(self) -> bool:
"""Return if entity is available."""
return (
self.coordinator.last_update_success
and bluetooth.async_address_present(
self.hass, self.coordinator.address, True
)
and self._attr_available
)
return self.coordinator.last_update_success and self._attr_available
class GardenaBluetoothDescriptorEntity(GardenaBluetoothEntity):

View File

@@ -229,11 +229,11 @@ class GiosSensor(CoordinatorEntity[GiosDataUpdateCoordinator], SensorEntity):
@property
def available(self) -> bool:
"""Return if entity is available."""
available = super().available
sensor_data = getattr(self.coordinator.data, self.entity_description.key)
available = super().available and bool(sensor_data)
# Sometimes the API returns sensor data without indexes
if self.entity_description.subkey:
if self.entity_description.subkey and available:
return available and bool(sensor_data.index)
return available and bool(sensor_data)
return available

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/calendar.google",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==6.0.3", "oauth2client==4.1.3", "ical==7.0.0"]
"requirements": ["gcal-sync==6.0.3", "oauth2client==4.1.3", "ical==7.0.3"]
}

View File

@@ -2706,10 +2706,9 @@ class SensorStateTrait(_Trait):
name = TRAIT_SENSOR_STATE
commands: list[str] = []
def _air_quality_description_for_aqi(self, aqi):
if aqi is None or aqi.isnumeric() is False:
def _air_quality_description_for_aqi(self, aqi: float | None) -> str:
if aqi is None or aqi < 0:
return "unknown"
aqi = int(aqi)
if aqi <= 50:
return "healthy"
if aqi <= 100:
@@ -2764,11 +2763,17 @@ class SensorStateTrait(_Trait):
if device_class is None or data is None:
return {}
sensor_data = {"name": data[0], "rawValue": self.state.state}
try:
value = float(self.state.state)
except ValueError:
value = None
if self.state.state == STATE_UNKNOWN:
value = None
sensor_data = {"name": data[0], "rawValue": value}
if device_class == sensor.SensorDeviceClass.AQI:
sensor_data["currentSensorState"] = self._air_quality_description_for_aqi(
self.state.state
value
)
return {"currentSensorStateData": [sensor_data]}

View File

@@ -19,6 +19,7 @@ ATTR_HOMEASSISTANT = "homeassistant"
ATTR_HOMEASSISTANT_EXCLUDE_DATABASE = "homeassistant_exclude_database"
ATTR_INPUT = "input"
ATTR_ISSUES = "issues"
ATTR_MESSAGE = "message"
ATTR_METHOD = "method"
ATTR_PANELS = "panels"
ATTR_PASSWORD = "password"

View File

@@ -21,7 +21,7 @@ from homeassistant.const import SERVER_PORT
from homeassistant.core import HomeAssistant
from homeassistant.loader import bind_hass
from .const import ATTR_DISCOVERY, DOMAIN, X_HASS_SOURCE
from .const import ATTR_DISCOVERY, ATTR_MESSAGE, ATTR_RESULT, DOMAIN, X_HASS_SOURCE
_P = ParamSpec("_P")
@@ -262,10 +262,7 @@ async def async_update_core(
@bind_hass
@_api_bool
async def async_apply_suggestion(hass: HomeAssistant, suggestion_uuid: str) -> dict:
"""Apply a suggestion from supervisor's resolution center.
The caller of the function should handle HassioAPIError.
"""
"""Apply a suggestion from supervisor's resolution center."""
hassio: HassIO = hass.data[DOMAIN]
command = f"/resolution/suggestion/{suggestion_uuid}"
return await hassio.send_command(command, timeout=None)
@@ -576,7 +573,7 @@ class HassIO:
raise HassioAPIError()
try:
request = await self.websession.request(
response = await self.websession.request(
method,
joined_url,
json=payload,
@@ -589,14 +586,23 @@ class HassIO:
timeout=aiohttp.ClientTimeout(total=timeout),
)
if request.status != HTTPStatus.OK:
_LOGGER.error("%s return code %d", command, request.status)
if response.status != HTTPStatus.OK:
error = await response.json(encoding="utf-8")
if error.get(ATTR_RESULT) == "error":
raise HassioAPIError(error.get(ATTR_MESSAGE))
_LOGGER.error(
"Request to %s method %s returned with code %d",
command,
method,
response.status,
)
raise HassioAPIError()
if return_text:
return await request.text(encoding="utf-8")
return await response.text(encoding="utf-8")
return await request.json(encoding="utf-8")
return await response.json(encoding="utf-8")
except TimeoutError:
_LOGGER.error("Timeout on %s request", command)

View File

@@ -3,11 +3,13 @@ from __future__ import annotations
import asyncio
from dataclasses import dataclass, field
from datetime import datetime
import logging
from typing import Any, NotRequired, TypedDict
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HassJob, HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
@@ -35,6 +37,7 @@ from .const import (
EVENT_SUPPORTED_CHANGED,
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
PLACEHOLDER_KEY_REFERENCE,
REQUEST_REFRESH_DELAY,
UPDATE_KEY_SUPERVISOR,
SupervisorIssueContext,
)
@@ -302,12 +305,17 @@ class SupervisorIssues:
self._hass, EVENT_SUPERVISOR_EVENT, self._supervisor_events_to_issues
)
async def update(self) -> None:
async def update(self, _: datetime | None = None) -> None:
"""Update issues from Supervisor resolution center."""
try:
data = await self._client.get_resolution_info()
except HassioAPIError as err:
_LOGGER.error("Failed to update supervisor issues: %r", err)
async_call_later(
self._hass,
REQUEST_REFRESH_DELAY,
HassJob(self.update, cancel_on_shutdown=True),
)
return
self.unhealthy_reasons = set(data[ATTR_UNHEALTHY])
self.unsupported_reasons = set(data[ATTR_UNSUPPORTED])

View File

@@ -18,7 +18,7 @@ from .const import (
PLACEHOLDER_KEY_REFERENCE,
SupervisorIssueContext,
)
from .handler import HassioAPIError, async_apply_suggestion
from .handler import async_apply_suggestion
from .issues import Issue, Suggestion
SUGGESTION_CONFIRMATION_REQUIRED = {"system_execute_reboot"}
@@ -109,12 +109,9 @@ class SupervisorIssueRepairFlow(RepairsFlow):
if not confirmed and suggestion.key in SUGGESTION_CONFIRMATION_REQUIRED:
return self._async_form_for_suggestion(suggestion)
try:
await async_apply_suggestion(self.hass, suggestion.uuid)
except HassioAPIError:
return self.async_abort(reason="apply_suggestion_fail")
return self.async_create_entry(data={})
if await async_apply_suggestion(self.hass, suggestion.uuid):
return self.async_create_entry(data={})
return self.async_abort(reason="apply_suggestion_fail")
@staticmethod
def _async_step(

View File

@@ -21,7 +21,6 @@ from .const import (
ATTR_DATA,
ATTR_ENDPOINT,
ATTR_METHOD,
ATTR_RESULT,
ATTR_SESSION_DATA_USER_ID,
ATTR_TIMEOUT,
ATTR_WS_EVENT,
@@ -131,9 +130,6 @@ async def websocket_supervisor_api(
payload=payload,
source="core.websocket_api",
)
if result.get(ATTR_RESULT) == "error":
raise HassioAPIError(result.get("message"))
except HassioAPIError as err:
_LOGGER.error("Failed to to call %s - %s", msg[ATTR_ENDPOINT], err)
connection.send_error(

View File

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

View File

@@ -6,6 +6,11 @@ import contextlib
import logging
import aiohomekit
from aiohomekit.const import (
BLE_TRANSPORT_SUPPORTED,
COAP_TRANSPORT_SUPPORTED,
IP_TRANSPORT_SUPPORTED,
)
from aiohomekit.exceptions import (
AccessoryDisconnectedError,
AccessoryNotFoundError,
@@ -24,6 +29,15 @@ from .connection import HKDevice
from .const import DOMAIN, KNOWN_DEVICES
from .utils import async_get_controller
# Ensure all the controllers get imported in the executor
# since they are loaded late.
if BLE_TRANSPORT_SUPPORTED:
from aiohomekit.controller import ble # noqa: F401
if COAP_TRANSPORT_SUPPORTED:
from aiohomekit.controller import coap # noqa: F401
if IP_TRANSPORT_SUPPORTED:
from aiohomekit.controller import ip # noqa: F401
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)

View File

@@ -57,7 +57,7 @@ async def async_setup_entry(
event_type: EventType, resource: BehaviorInstance | LightLevel | Motion
) -> None:
"""Add entity from Hue resource."""
async_add_entities([switch_class(bridge, api.sensors.motion, resource)])
async_add_entities([switch_class(bridge, controller, resource)])
# add all current items in controller
for item in controller:

View File

@@ -269,10 +269,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
self._dynamic_mode_active = lights_in_dynamic_mode > 0
self._attr_supported_color_modes = supported_color_modes
# pick a winner for the current colormode
if (
lights_with_color_temp_support > 0
and lights_in_colortemp_mode == lights_with_color_temp_support
):
if lights_with_color_temp_support > 0 and lights_in_colortemp_mode > 0:
self._attr_color_mode = ColorMode.COLOR_TEMP
elif lights_with_color_support > 0:
self._attr_color_mode = ColorMode.XY

View File

@@ -6,7 +6,7 @@ from aioautomower.session import AutomowerSession
from aiohttp import ClientError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
@@ -17,7 +17,6 @@ from .coordinator import AutomowerDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.LAWN_MOWER, Platform.SENSOR, Platform.SWITCH]
@@ -38,13 +37,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await api_api.async_get_access_token()
except ClientError as err:
raise ConfigEntryNotReady from err
coordinator = AutomowerDataUpdateCoordinator(hass, automower_api)
coordinator = AutomowerDataUpdateCoordinator(hass, automower_api, entry)
await coordinator.async_config_entry_first_refresh()
entry.async_create_background_task(
hass,
coordinator.client_listen(hass, entry, automower_api),
"websocket_task",
)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.shutdown)
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -52,8 +53,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Handle unload of an entry."""
coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
await coordinator.shutdown()
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)

View File

@@ -2,7 +2,7 @@
import logging
from typing import Any
from aioautomower.utils import async_structure_token
from aioautomower.utils import structure_token
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.data_entry_flow import FlowResult
@@ -27,7 +27,7 @@ class HusqvarnaConfigFlowHandler(
"""Create an entry for the flow."""
token = data[CONF_TOKEN]
user_id = token[CONF_USER_ID]
structured_token = await async_structure_token(token[CONF_ACCESS_TOKEN])
structured_token = structure_token(token[CONF_ACCESS_TOKEN])
first_name = structured_token.user.first_name
last_name = structured_token.user.last_name
await self.async_set_unique_id(user_id)

View File

@@ -1,23 +1,28 @@
"""Data UpdateCoordinator for the Husqvarna Automower integration."""
import asyncio
from datetime import timedelta
import logging
from typing import Any
from aioautomower.exceptions import ApiException, HusqvarnaWSServerHandshakeError
from aioautomower.model import MowerAttributes
from aioautomower.session import AutomowerSession
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .api import AsyncConfigEntryAuth
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
MAX_WS_RECONNECT_TIME = 600
class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttributes]]):
"""Class to manage fetching Husqvarna data."""
def __init__(self, hass: HomeAssistant, api: AsyncConfigEntryAuth) -> None:
def __init__(
self, hass: HomeAssistant, api: AutomowerSession, entry: ConfigEntry
) -> None:
"""Initialize data updater."""
super().__init__(
hass,
@@ -35,13 +40,39 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib
await self.api.connect()
self.api.register_data_callback(self.callback)
self.ws_connected = True
return await self.api.get_status()
async def shutdown(self, *_: Any) -> None:
"""Close resources."""
await self.api.close()
try:
return await self.api.get_status()
except ApiException as err:
raise UpdateFailed(err) from err
@callback
def callback(self, ws_data: dict[str, MowerAttributes]) -> None:
"""Process websocket callbacks and write them to the DataUpdateCoordinator."""
self.async_set_updated_data(ws_data)
async def client_listen(
self,
hass: HomeAssistant,
entry: ConfigEntry,
automower_client: AutomowerSession,
reconnect_time: int = 2,
) -> None:
"""Listen with the client."""
try:
await automower_client.auth.websocket_connect()
reconnect_time = 2
await automower_client.start_listening()
except HusqvarnaWSServerHandshakeError as err:
_LOGGER.debug(
"Failed to connect to websocket. Trying to reconnect: %s", err
)
if not hass.is_stopping:
await asyncio.sleep(reconnect_time)
reconnect_time = min(reconnect_time * 2, MAX_WS_RECONNECT_TIME)
await self.client_listen(
hass=hass,
entry=entry,
automower_client=automower_client,
reconnect_time=reconnect_time,
)

View File

@@ -6,5 +6,6 @@
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower",
"iot_class": "cloud_push",
"requirements": ["aioautomower==2024.2.7"]
"loggers": ["aioautomower"],
"requirements": ["aioautomower==2024.3.3"]
}

View File

@@ -69,6 +69,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
exists_fn=lambda data: data.statistics.total_charging_time is not None,
value_fn=lambda data: data.statistics.total_charging_time,
),
AutomowerSensorEntityDescription(
@@ -79,6 +80,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
exists_fn=lambda data: data.statistics.total_cutting_time is not None,
value_fn=lambda data: data.statistics.total_cutting_time,
),
AutomowerSensorEntityDescription(
@@ -89,6 +91,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
exists_fn=lambda data: data.statistics.total_running_time is not None,
value_fn=lambda data: data.statistics.total_running_time,
),
AutomowerSensorEntityDescription(
@@ -99,6 +102,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
exists_fn=lambda data: data.statistics.total_searching_time is not None,
value_fn=lambda data: data.statistics.total_searching_time,
),
AutomowerSensorEntityDescription(
@@ -107,6 +111,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
icon="mdi:battery-sync-outline",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL,
exists_fn=lambda data: data.statistics.number_of_charging_cycles is not None,
value_fn=lambda data: data.statistics.number_of_charging_cycles,
),
AutomowerSensorEntityDescription(
@@ -115,6 +120,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
icon="mdi:counter",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL,
exists_fn=lambda data: data.statistics.number_of_collisions is not None,
value_fn=lambda data: data.statistics.number_of_collisions,
),
AutomowerSensorEntityDescription(
@@ -125,6 +131,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.METERS,
suggested_unit_of_measurement=UnitOfLength.KILOMETERS,
exists_fn=lambda data: data.statistics.total_drive_distance is not None,
value_fn=lambda data: data.statistics.total_drive_distance,
),
AutomowerSensorEntityDescription(

View File

@@ -52,7 +52,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
is_fixable=False,
severity=IssueSeverity.ERROR,
translation_key="deprecated_yaml_import_issue",
translation_placeholders={"error_type": error_type},
translation_placeholders={
"error_type": error_type,
"url": "/config/integrations/dashboard/add?domain=hydrawise",
},
)
return self.async_abort(reason=error_type)

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
"iot_class": "cloud_polling",
"loggers": ["pydrawise"],
"requirements": ["pydrawise==2024.2.0"]
"requirements": ["pydrawise==2024.3.0"]
}

View File

@@ -0,0 +1,28 @@
"""Diagnostics support for Internet Printing Protocol (IPP)."""
from __future__ import annotations
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import IPPDataUpdateCoordinator
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: IPPDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
return {
"entry": {
"data": {
**config_entry.data,
},
"unique_id": config_entry.unique_id,
},
"data": coordinator.data.as_dict(),
}

View File

@@ -8,6 +8,6 @@
"iot_class": "local_polling",
"loggers": ["deepmerge", "pyipp"],
"quality_scale": "platinum",
"requirements": ["pyipp==0.14.5"],
"requirements": ["pyipp==0.15.0"],
"zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."]
}

View File

@@ -149,7 +149,9 @@ class JellyfinMediaPlayer(JellyfinEntity, MediaPlayerEntity):
media_content_type = CONTENT_TYPE_MAP.get(self.now_playing["Type"], None)
media_content_id = self.now_playing["Id"]
media_title = self.now_playing["Name"]
media_duration = int(self.now_playing["RunTimeTicks"] / 10000000)
if "RunTimeTicks" in self.now_playing:
media_duration = int(self.now_playing["RunTimeTicks"] / 10000000)
if media_content_type == MediaType.EPISODE:
media_content_type = MediaType.TVSHOW

View File

@@ -1,37 +1,107 @@
"""The JuiceNet integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
import aiohttp
from pyjuicenet import Api, TokenError
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
DOMAIN = "juicenet"
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
from .device import JuiceNetApi
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
CONFIG_SCHEMA = vol.Schema(
vol.All(
cv.deprecated(DOMAIN),
{DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})},
),
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the JuiceNet component."""
conf = config.get(DOMAIN)
hass.data.setdefault(DOMAIN, {})
if not conf:
return True
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
)
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up JuiceNet from a config entry."""
ir.async_create_issue(
config = entry.data
session = async_get_clientsession(hass)
access_token = config[CONF_ACCESS_TOKEN]
api = Api(access_token, session)
juicenet = JuiceNetApi(api)
try:
await juicenet.setup()
except TokenError as error:
_LOGGER.error("JuiceNet Error %s", error)
return False
except aiohttp.ClientError as error:
_LOGGER.error("Could not reach the JuiceNet API %s", error)
raise ConfigEntryNotReady from error
if not juicenet.devices:
_LOGGER.error("No JuiceNet devices found for this account")
return False
_LOGGER.info("%d JuiceNet device(s) found", len(juicenet.devices))
async def async_update_data():
"""Update all device states from the JuiceNet API."""
for device in juicenet.devices:
await device.update_state(True)
return True
coordinator = DataUpdateCoordinator(
hass,
DOMAIN,
DOMAIN,
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="integration_removed",
translation_placeholders={
"entries": "/config/integrations/integration/juicenet",
},
_LOGGER,
name="JuiceNet",
update_method=async_update_data,
update_interval=timedelta(seconds=30),
)
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = {
JUICENET_API: juicenet,
JUICENET_COORDINATOR: coordinator,
}
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if all(
config_entry.state is ConfigEntryState.NOT_LOADED
for config_entry in hass.config_entries.async_entries(DOMAIN)
if config_entry.entry_id != entry.entry_id
):
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
return True
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -1,11 +1,77 @@
"""Config flow for JuiceNet integration."""
import logging
from homeassistant import config_entries
import aiohttp
from pyjuicenet import Api, TokenError
import voluptuous as vol
from . import DOMAIN
from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})
async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
session = async_get_clientsession(hass)
juicenet = Api(data[CONF_ACCESS_TOKEN], session)
try:
await juicenet.get_devices()
except TokenError as error:
_LOGGER.error("Token Error %s", error)
raise InvalidAuth from error
except aiohttp.ClientError as error:
_LOGGER.error("Error connecting %s", error)
raise CannotConnect from error
# Return info that you want to store in the config entry.
return {"title": "JuiceNet"}
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for JuiceNet."""
VERSION = 1
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
await self.async_set_unique_id(user_input[CONF_ACCESS_TOKEN])
self._abort_if_unique_id_configured()
try:
info = await validate_input(self.hass, user_input)
return self.async_create_entry(title=info["title"], data=user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
async def async_step_import(self, user_input):
"""Handle import."""
return await self.async_step_user(user_input)
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""

View File

@@ -0,0 +1,6 @@
"""Constants used by the JuiceNet component."""
DOMAIN = "juicenet"
JUICENET_API = "juicenet_api"
JUICENET_COORDINATOR = "juicenet_coordinator"

View File

@@ -0,0 +1,19 @@
"""Adapter to wrap the pyjuicenet api for home assistant."""
class JuiceNetApi:
"""Represent a connection to JuiceNet."""
def __init__(self, api):
"""Create an object from the provided API instance."""
self.api = api
self._devices = []
async def setup(self):
"""JuiceNet device setup.""" # noqa: D403
self._devices = await self.api.get_devices()
@property
def devices(self) -> list:
"""Get a list of devices managed by this account."""
return self._devices

View File

@@ -0,0 +1,34 @@
"""Adapter to wrap the pyjuicenet api for home assistant."""
from pyjuicenet import Charger
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from .const import DOMAIN
class JuiceNetDevice(CoordinatorEntity):
"""Represent a base JuiceNet device."""
_attr_has_entity_name = True
def __init__(
self, device: Charger, key: str, coordinator: DataUpdateCoordinator
) -> None:
"""Initialise the sensor."""
super().__init__(coordinator)
self.device = device
self.key = key
self._attr_unique_id = f"{device.id}-{key}"
self._attr_device_info = DeviceInfo(
configuration_url=(
f"https://home.juice.net/Portal/Details?unitID={device.id}"
),
identifiers={(DOMAIN, device.id)},
manufacturer="JuiceNet",
name=device.name,
)

View File

@@ -1,9 +1,10 @@
{
"domain": "juicenet",
"name": "JuiceNet",
"codeowners": [],
"codeowners": ["@jesserockz"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/juicenet",
"integration_type": "system",
"iot_class": "cloud_polling",
"requirements": []
"loggers": ["pyjuicenet"],
"requirements": ["python-juicenet==1.1.0"]
}

View File

@@ -0,0 +1,99 @@
"""Support for controlling juicenet/juicepoint/juicebox based EVSE numbers."""
from __future__ import annotations
from dataclasses import dataclass
from pyjuicenet import Api, Charger
from homeassistant.components.number import (
DEFAULT_MAX_VALUE,
NumberEntity,
NumberEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
from .entity import JuiceNetDevice
@dataclass(frozen=True)
class JuiceNetNumberEntityDescriptionMixin:
"""Mixin for required keys."""
setter_key: str
@dataclass(frozen=True)
class JuiceNetNumberEntityDescription(
NumberEntityDescription, JuiceNetNumberEntityDescriptionMixin
):
"""An entity description for a JuiceNetNumber."""
native_max_value_key: str | None = None
NUMBER_TYPES: tuple[JuiceNetNumberEntityDescription, ...] = (
JuiceNetNumberEntityDescription(
translation_key="amperage_limit",
key="current_charging_amperage_limit",
native_min_value=6,
native_max_value_key="max_charging_amperage",
native_step=1,
setter_key="set_charging_amperage_limit",
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the JuiceNet Numbers."""
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
api: Api = juicenet_data[JUICENET_API]
coordinator = juicenet_data[JUICENET_COORDINATOR]
entities = [
JuiceNetNumber(device, description, coordinator)
for device in api.devices
for description in NUMBER_TYPES
]
async_add_entities(entities)
class JuiceNetNumber(JuiceNetDevice, NumberEntity):
"""Implementation of a JuiceNet number."""
entity_description: JuiceNetNumberEntityDescription
def __init__(
self,
device: Charger,
description: JuiceNetNumberEntityDescription,
coordinator: DataUpdateCoordinator,
) -> None:
"""Initialise the number."""
super().__init__(device, description.key, coordinator)
self.entity_description = description
@property
def native_value(self) -> float | None:
"""Return the value of the entity."""
return getattr(self.device, self.entity_description.key, None)
@property
def native_max_value(self) -> float:
"""Return the maximum value."""
if self.entity_description.native_max_value_key is not None:
return getattr(self.device, self.entity_description.native_max_value_key)
if self.entity_description.native_max_value is not None:
return self.entity_description.native_max_value
return DEFAULT_MAX_VALUE
async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""
await getattr(self.device, self.entity_description.setter_key)(value)

View File

@@ -0,0 +1,116 @@
"""Support for monitoring juicenet/juicepoint/juicebox based EVSE sensors."""
from __future__ import annotations
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfPower,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
from .entity import JuiceNetDevice
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="status",
name="Charging Status",
),
SensorEntityDescription(
key="temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
),
SensorEntityDescription(
key="amps",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="watts",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="charge_time",
translation_key="charge_time",
native_unit_of_measurement=UnitOfTime.SECONDS,
icon="mdi:timer-outline",
),
SensorEntityDescription(
key="energy_added",
translation_key="energy_added",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the JuiceNet Sensors."""
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
api = juicenet_data[JUICENET_API]
coordinator = juicenet_data[JUICENET_COORDINATOR]
entities = [
JuiceNetSensorDevice(device, coordinator, description)
for device in api.devices
for description in SENSOR_TYPES
]
async_add_entities(entities)
class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity):
"""Implementation of a JuiceNet sensor."""
def __init__(
self, device, coordinator, description: SensorEntityDescription
) -> None:
"""Initialise the sensor."""
super().__init__(device, description.key, coordinator)
self.entity_description = description
@property
def icon(self):
"""Return the icon of the sensor."""
icon = None
if self.entity_description.key == "status":
status = self.device.status
if status == "standby":
icon = "mdi:power-plug-off"
elif status == "plugged":
icon = "mdi:power-plug"
elif status == "charging":
icon = "mdi:battery-positive"
else:
icon = self.entity_description.icon
return icon
@property
def native_value(self):
"""Return the state."""
return getattr(self.device, self.entity_description.key, None)

View File

@@ -1,8 +1,41 @@
{
"issues": {
"integration_removed": {
"title": "The JuiceNet integration has been removed",
"description": "Enel X has dropped support for JuiceNet in favor of JuicePass, and the JuiceNet integration has been removed from Home Assistant as it was no longer working.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing JuiceNet integration entries]({entries})."
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"api_token": "[%key:common::config_flow::data::api_token%]"
},
"description": "You will need the API Token from https://home.juice.net/Manage.",
"title": "Connect to JuiceNet"
}
}
},
"entity": {
"number": {
"amperage_limit": {
"name": "Amperage limit"
}
},
"sensor": {
"charge_time": {
"name": "Charge time"
},
"energy_added": {
"name": "Energy added"
}
},
"switch": {
"charge_now": {
"name": "Charge now"
}
}
}
}

View File

@@ -0,0 +1,49 @@
"""Support for monitoring juicenet/juicepoint/juicebox based EVSE switches."""
from typing import Any
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
from .entity import JuiceNetDevice
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the JuiceNet switches."""
entities = []
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
api = juicenet_data[JUICENET_API]
coordinator = juicenet_data[JUICENET_COORDINATOR]
for device in api.devices:
entities.append(JuiceNetChargeNowSwitch(device, coordinator))
async_add_entities(entities)
class JuiceNetChargeNowSwitch(JuiceNetDevice, SwitchEntity):
"""Implementation of a JuiceNet switch."""
_attr_translation_key = "charge_now"
def __init__(self, device, coordinator):
"""Initialise the switch."""
super().__init__(device, "charge_now", coordinator)
@property
def is_on(self):
"""Return true if switch is on."""
return self.device.override_time != 0
async def async_turn_on(self, **kwargs: Any) -> None:
"""Charge now."""
await self.device.set_override(True)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Don't charge now."""
await self.device.set_override(False)

View File

@@ -11,7 +11,7 @@
"loggers": ["xknx", "xknxproject"],
"quality_scale": "platinum",
"requirements": [
"xknx==2.12.1",
"xknx==2.12.2",
"xknxproject==3.7.0",
"knx-frontend==2024.1.20.105944"
]

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
"iot_class": "local_polling",
"loggers": ["ical"],
"requirements": ["ical==7.0.0"]
"requirements": ["ical==7.0.3"]
}

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/local_todo",
"iot_class": "local_polling",
"requirements": ["ical==7.0.0"]
"requirements": ["ical==7.0.3"]
}

View File

@@ -18,6 +18,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
from .const import CONF_TODO_LIST_NAME, DOMAIN
from .store import LocalTodoListStore
@@ -124,6 +125,9 @@ class LocalTodoListEntity(TodoListEntity):
self._attr_name = name.capitalize()
self._attr_unique_id = unique_id
def _new_todo_store(self) -> TodoStore:
return TodoStore(self._calendar, tzinfo=dt_util.DEFAULT_TIME_ZONE)
async def async_update(self) -> None:
"""Update entity state based on the local To-do items."""
todo_items = []
@@ -147,20 +151,20 @@ class LocalTodoListEntity(TodoListEntity):
async def async_create_todo_item(self, item: TodoItem) -> None:
"""Add an item to the To-do list."""
todo = _convert_item(item)
TodoStore(self._calendar).add(todo)
self._new_todo_store().add(todo)
await self.async_save()
await self.async_update_ha_state(force_refresh=True)
async def async_update_todo_item(self, item: TodoItem) -> None:
"""Update an item to the To-do list."""
todo = _convert_item(item)
TodoStore(self._calendar).edit(todo.uid, todo)
self._new_todo_store().edit(todo.uid, todo)
await self.async_save()
await self.async_update_ha_state(force_refresh=True)
async def async_delete_todo_items(self, uids: list[str]) -> None:
"""Delete an item from the To-do list."""
store = TodoStore(self._calendar)
store = self._new_todo_store()
for uid in uids:
store.delete(uid)
await self.async_save()

View File

@@ -7,7 +7,7 @@
"dependencies": ["webhook"],
"documentation": "https://www.home-assistant.io/integrations/loqed",
"iot_class": "local_push",
"requirements": ["loqedAPI==2.1.8"],
"requirements": ["loqedAPI==2.1.10"],
"zeroconf": [
{
"type": "_http._tcp.local.",

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/luci",
"iot_class": "local_polling",
"loggers": ["openwrt_luci_rpc"],
"requirements": ["openwrt-luci-rpc==1.1.16"]
"requirements": ["openwrt-luci-rpc==1.1.17"]
}

View File

@@ -148,7 +148,10 @@ async def async_resolve_media(
raise Unresolvable("Media Source not loaded")
if target_media_player is UNDEFINED:
report("calls media_source.async_resolve_media without passing an entity_id")
report(
"calls media_source.async_resolve_media without passing an entity_id",
{DOMAIN},
)
target_media_player = None
try:

View File

@@ -14,7 +14,7 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryError
from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.device_registry as dr
import homeassistant.helpers.entity_registry as er
@@ -41,9 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try:
await api.async_initialize()
except MinecraftServerAddressError as error:
raise ConfigEntryError(
f"Server address in configuration entry is invalid: {error}"
) from error
raise ConfigEntryNotReady(f"Initialization failed: {error}") from error
# Create coordinator instance.
coordinator = MinecraftServerCoordinator(hass, entry.data[CONF_NAME], api)

View File

@@ -134,12 +134,11 @@ class MjpegCamera(Camera):
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera."""
# DigestAuth is not supported
if (
self._authentication == HTTP_DIGEST_AUTHENTICATION
or self._still_image_url is None
):
return await self._async_digest_camera_image()
return await self._async_digest_or_fallback_camera_image()
websession = async_get_clientsession(self.hass, verify_ssl=self._verify_ssl)
try:
@@ -157,15 +156,17 @@ class MjpegCamera(Camera):
return None
def _get_digest_auth(self) -> httpx.DigestAuth:
"""Return a DigestAuth object."""
def _get_httpx_auth(self) -> httpx.Auth:
"""Return a httpx auth object."""
username = "" if self._username is None else self._username
return httpx.DigestAuth(username, self._password)
digest_auth = self._authentication == HTTP_DIGEST_AUTHENTICATION
cls = httpx.DigestAuth if digest_auth else httpx.BasicAuth
return cls(username, self._password)
async def _async_digest_camera_image(self) -> bytes | None:
async def _async_digest_or_fallback_camera_image(self) -> bytes | None:
"""Return a still image response from the camera using digest authentication."""
client = get_async_client(self.hass, verify_ssl=self._verify_ssl)
auth = self._get_digest_auth()
auth = self._get_httpx_auth()
try:
if self._still_image_url:
# Fallback to MJPEG stream if still image URL is not available
@@ -196,7 +197,7 @@ class MjpegCamera(Camera):
) -> web.StreamResponse | None:
"""Generate an HTTP MJPEG stream from the camera using digest authentication."""
async with get_async_client(self.hass, verify_ssl=self._verify_ssl).stream(
"get", self._mjpeg_url, auth=self._get_digest_auth(), timeout=TIMEOUT
"get", self._mjpeg_url, auth=self._get_httpx_auth(), timeout=TIMEOUT
) as stream:
response = web.StreamResponse(headers=stream.headers)
await response.prepare(request)

View File

@@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pymodbus"],
"quality_scale": "gold",
"requirements": ["pymodbus==3.6.4"]
"requirements": ["pymodbus==3.6.6"]
}

View File

@@ -301,16 +301,17 @@ def check_config(config: dict) -> dict:
def validate_entity(
hub_name: str,
component: str,
entity: dict,
minimum_scan_interval: int,
ent_names: set,
ent_addr: set,
) -> bool:
"""Validate entity."""
name = entity[CONF_NAME]
addr = str(entity[CONF_ADDRESS])
name = f"{component}.{entity[CONF_NAME]}"
addr = f"{hub_name}{entity[CONF_ADDRESS]}"
scan_interval = entity.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
if scan_interval < 5:
if 0 < scan_interval < 5:
_LOGGER.warning(
(
"%s %s scan_interval(%d) is lower than 5 seconds, "
@@ -335,11 +336,15 @@ def check_config(config: dict) -> dict:
loc_addr: set[str] = {addr}
if CONF_TARGET_TEMP in entity:
loc_addr.add(f"{entity[CONF_TARGET_TEMP]}_{inx}")
loc_addr.add(f"{hub_name}{entity[CONF_TARGET_TEMP]}_{inx}")
if CONF_HVAC_MODE_REGISTER in entity:
loc_addr.add(f"{entity[CONF_HVAC_MODE_REGISTER][CONF_ADDRESS]}_{inx}")
loc_addr.add(
f"{hub_name}{entity[CONF_HVAC_MODE_REGISTER][CONF_ADDRESS]}_{inx}"
)
if CONF_FAN_MODE_REGISTER in entity:
loc_addr.add(f"{entity[CONF_FAN_MODE_REGISTER][CONF_ADDRESS]}_{inx}")
loc_addr.add(
f"{hub_name}{entity[CONF_FAN_MODE_REGISTER][CONF_ADDRESS]}_{inx}"
)
dup_addrs = ent_addr.intersection(loc_addr)
if len(dup_addrs) > 0:
@@ -364,15 +369,18 @@ def check_config(config: dict) -> dict:
if not validate_modbus(hub, hub_name_inx):
del config[hub_inx]
continue
for _component, conf_key in PLATFORMS:
minimum_scan_interval = 9999
no_entities = True
for component, conf_key in PLATFORMS:
if conf_key not in hub:
continue
no_entities = False
entity_inx = 0
entities = hub[conf_key]
minimum_scan_interval = 9999
while entity_inx < len(entities):
if not validate_entity(
hub[CONF_NAME],
component,
entities[entity_inx],
minimum_scan_interval,
ent_names,
@@ -381,7 +389,11 @@ def check_config(config: dict) -> dict:
del entities[entity_inx]
else:
entity_inx += 1
if no_entities:
err = f"Modbus {hub[CONF_NAME]} contain no entities, this will cause instability, please add at least one entity!"
_LOGGER.warning(err)
# Ensure timeout is not started/handled.
hub[CONF_TIMEOUT] = -1
if hub[CONF_TIMEOUT] >= minimum_scan_interval:
hub[CONF_TIMEOUT] = minimum_scan_interval - 1
_LOGGER.warning(

View File

@@ -165,9 +165,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except NotionError as err:
raise ConfigEntryNotReady("Config entry failed to load") from err
# Always update the config entry with the latest refresh token and user UUID:
entry_updates["data"][CONF_REFRESH_TOKEN] = client.refresh_token
entry_updates["data"][CONF_USER_UUID] = client.user_uuid
# Update the Notion user UUID and refresh token if they've changed:
for key, value in (
(CONF_REFRESH_TOKEN, client.refresh_token),
(CONF_USER_UUID, client.user_uuid),
):
if entry.data[key] == value:
continue
entry_updates["data"][key] = value
hass.config_entries.async_update_entry(entry, **entry_updates)
@callback
def async_save_refresh_token(refresh_token: str) -> None:
@@ -180,8 +187,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Create a callback to save the refresh token when it changes:
entry.async_on_unload(client.add_refresh_token_callback(async_save_refresh_token))
hass.config_entries.async_update_entry(entry, **entry_updates)
async def async_update() -> NotionData:
"""Get the latest data from the Notion API."""
data = NotionData(hass=hass, entry=entry)

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aionotion"],
"requirements": ["aionotion==2024.02.2"]
"requirements": ["aionotion==2024.03.0"]
}

View File

@@ -3,7 +3,8 @@
"name": "Numato USB GPIO Expander",
"codeowners": ["@clssn"],
"documentation": "https://www.home-assistant.io/integrations/numato",
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["numato_gpio"],
"requirements": ["numato-gpio==0.10.0"]
"requirements": ["numato-gpio==0.12.0"]
}

View File

@@ -143,6 +143,8 @@ class OneWireBinarySensor(OneWireEntity, BinarySensorEntity):
entity_description: OneWireBinarySensorEntityDescription
@property
def is_on(self) -> bool:
def is_on(self) -> bool | None:
"""Return true if sensor is on."""
if self._state is None:
return None
return bool(self._state)

View File

@@ -204,8 +204,10 @@ class OneWireSwitch(OneWireEntity, SwitchEntity):
entity_description: OneWireSwitchEntityDescription
@property
def is_on(self) -> bool:
"""Return true if sensor is on."""
def is_on(self) -> bool | None:
"""Return true if switch is on."""
if self._state is None:
return None
return bool(self._state)
def turn_on(self, **kwargs: Any) -> None:

View File

@@ -19,7 +19,7 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
"requirements": ["pyoverkiz==1.13.7"],
"requirements": ["pyoverkiz==1.13.8"],
"zeroconf": [
{
"type": "_kizbox._tcp.local.",

View File

@@ -177,6 +177,7 @@ SENSOR_DESCRIPTIONS: tuple[PermobilSensorEntityDescription, ...] = (
key="record_distance",
translation_key="record_distance",
icon="mdi:map-marker-distance",
device_class=SensorDeviceClass.DISTANCE,
state_class=SensorStateClass.TOTAL_INCREASING,
),
)

View File

@@ -11,11 +11,10 @@ from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
from .const import CONF_SERIAL_NUMBER
from .coordinator import RainbirdData
from .coordinator import RainbirdData, async_create_clientsession
_LOGGER = logging.getLogger(__name__)
@@ -36,9 +35,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {})
clientsession = async_create_clientsession()
entry.async_on_unload(clientsession.close)
controller = AsyncRainbirdController(
AsyncRainbirdClient(
async_get_clientsession(hass),
clientsession,
entry.data[CONF_HOST],
entry.data[CONF_PASSWORD],
)

View File

@@ -20,7 +20,6 @@ from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_validation as cv, selector
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
from .const import (
@@ -30,6 +29,7 @@ from .const import (
DOMAIN,
TIMEOUT_SECONDS,
)
from .coordinator import async_create_clientsession
_LOGGER = logging.getLogger(__name__)
@@ -101,9 +101,10 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
Raises a ConfigFlowError on failure.
"""
clientsession = async_create_clientsession()
controller = AsyncRainbirdController(
AsyncRainbirdClient(
async_get_clientsession(self.hass),
clientsession,
host,
password,
)
@@ -124,6 +125,8 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
f"Error connecting to Rain Bird controller: {str(err)}",
"cannot_connect",
) from err
finally:
await clientsession.close()
async def async_finish(
self,

View File

@@ -9,6 +9,7 @@ from functools import cached_property
import logging
from typing import TypeVar
import aiohttp
from pyrainbird.async_client import (
AsyncRainbirdController,
RainbirdApiException,
@@ -18,6 +19,7 @@ from pyrainbird.data import ModelAndVersion, Schedule
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -28,6 +30,13 @@ UPDATE_INTERVAL = datetime.timedelta(minutes=1)
# changes, so we refresh it less often.
CALENDAR_UPDATE_INTERVAL = datetime.timedelta(minutes=15)
# The valves state are not immediately reflected after issuing a command. We add
# small delay to give additional time to reflect the new state.
DEBOUNCER_COOLDOWN = 5
# Rainbird devices can only accept a single request at a time
CONECTION_LIMIT = 1
_LOGGER = logging.getLogger(__name__)
_T = TypeVar("_T")
@@ -43,6 +52,13 @@ class RainbirdDeviceState:
rain_delay: int
def async_create_clientsession() -> aiohttp.ClientSession:
"""Create a rainbird async_create_clientsession with a connection limit."""
return aiohttp.ClientSession(
connector=aiohttp.TCPConnector(limit=CONECTION_LIMIT),
)
class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
"""Coordinator for rainbird API calls."""
@@ -60,6 +76,9 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
_LOGGER,
name=name,
update_interval=UPDATE_INTERVAL,
request_refresh_debouncer=Debouncer(
hass, _LOGGER, cooldown=DEBOUNCER_COOLDOWN, immediate=False
),
)
self._controller = controller
self._unique_id = unique_id

View File

@@ -103,6 +103,10 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity)
except RainbirdApiException as err:
raise HomeAssistantError("Rain Bird device failure") from err
# The device reflects the old state for a few moments. Update the
# state manually and trigger a refresh after a short debounced delay.
self.coordinator.data.active_zones.add(self._zone)
self.async_write_ha_state()
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs):
@@ -115,6 +119,11 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity)
) from err
except RainbirdApiException as err:
raise HomeAssistantError("Rain Bird device failure") from err
# The device reflects the old state for a few moments. Update the
# state manually and trigger a refresh after a short debounced delay.
self.coordinator.data.active_zones.remove(self._zone)
self.async_write_ha_state()
await self.coordinator.async_request_refresh()
@property

View File

@@ -132,16 +132,27 @@ class RAVEnDataCoordinator(DataUpdateCoordinator):
)
return None
async def async_shutdown(self) -> None:
"""Shutdown the coordinator."""
await self._cleanup_device()
await super().async_shutdown()
async def _async_update_data(self) -> dict[str, Any]:
try:
device = await self._get_device()
async with asyncio.timeout(5):
return await _get_all_data(device, self.entry.data[CONF_MAC])
except RAVEnConnectionError as err:
if self._raven_device:
await self._raven_device.close()
self._raven_device = None
await self._cleanup_device()
raise UpdateFailed(f"RAVEnConnectionError: {err}") from err
except TimeoutError:
await self._cleanup_device()
raise
async def _cleanup_device(self) -> None:
device, self._raven_device = self._raven_device, None
if device is not None:
await device.close()
async def _get_device(self) -> RAVEnSerialDevice:
if self._raven_device is not None:
@@ -149,15 +160,14 @@ class RAVEnDataCoordinator(DataUpdateCoordinator):
device = RAVEnSerialDevice(self.entry.data[CONF_DEVICE])
async with asyncio.timeout(5):
await device.open()
try:
try:
async with asyncio.timeout(5):
await device.open()
await device.synchronize()
self._device_info = await device.get_device_info()
except Exception:
await device.close()
raise
except:
await device.close()
raise
self._raven_device = device
return device

View File

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

View File

@@ -19,7 +19,7 @@
"title": "Random sensor"
},
"user": {
"description": "This helper allow you to create a helper that emits a random value.",
"description": "This helper allows you to create a helper that emits a random value.",
"menu_options": {
"binary_sensor": "Random binary sensor",
"sensor": "Random sensor"

View File

@@ -924,7 +924,7 @@ class Recorder(threading.Thread):
# that is pending before running the task
if TYPE_CHECKING:
assert isinstance(task, RecorderTask)
if not task.commit_before:
if task.commit_before:
self._commit_event_session_or_retry()
return task.run(self)
except exc.DatabaseError as err:

View File

@@ -6,7 +6,7 @@ from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.start import async_at_start
from .core import Recorder
from .util import get_instance, session_scope
from .util import filter_unique_constraint_integrity_error, get_instance, session_scope
_LOGGER = logging.getLogger(__name__)
@@ -61,7 +61,10 @@ def update_states_metadata(
)
return
with session_scope(session=instance.get_session()) as session:
with session_scope(
session=instance.get_session(),
exception_filter=filter_unique_constraint_integrity_error(instance, "state"),
) as session:
if not states_meta_manager.update_metadata(session, entity_id, new_entity_id):
_LOGGER.warning(
"Cannot migrate history for entity_id `%s` to `%s` "

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
from collections import defaultdict
from collections.abc import Callable, Iterable, Sequence
import contextlib
import dataclasses
from datetime import datetime, timedelta
from functools import lru_cache, partial
@@ -15,7 +14,7 @@ from typing import TYPE_CHECKING, Any, Literal, TypedDict, cast
from sqlalchemy import Select, and_, bindparam, func, lambda_stmt, select, text
from sqlalchemy.engine.row import Row
from sqlalchemy.exc import SQLAlchemyError, StatementError
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm.session import Session
from sqlalchemy.sql.lambdas import StatementLambdaElement
import voluptuous as vol
@@ -72,6 +71,7 @@ from .models import (
from .util import (
execute,
execute_stmt_lambda_element,
filter_unique_constraint_integrity_error,
get_instance,
retryable_database_job,
session_scope,
@@ -454,7 +454,9 @@ def compile_missing_statistics(instance: Recorder) -> bool:
with session_scope(
session=instance.get_session(),
exception_filter=_filter_unique_constraint_integrity_error(instance),
exception_filter=filter_unique_constraint_integrity_error(
instance, "statistic"
),
) as session:
# Find the newest statistics run, if any
if last_run := session.query(func.max(StatisticsRuns.start)).scalar():
@@ -486,7 +488,9 @@ def compile_statistics(instance: Recorder, start: datetime, fire_events: bool) -
# Return if we already have 5-minute statistics for the requested period
with session_scope(
session=instance.get_session(),
exception_filter=_filter_unique_constraint_integrity_error(instance),
exception_filter=filter_unique_constraint_integrity_error(
instance, "statistic"
),
) as session:
modified_statistic_ids = _compile_statistics(
instance, session, start, fire_events
@@ -737,7 +741,9 @@ def update_statistics_metadata(
if new_statistic_id is not UNDEFINED and new_statistic_id is not None:
with session_scope(
session=instance.get_session(),
exception_filter=_filter_unique_constraint_integrity_error(instance),
exception_filter=filter_unique_constraint_integrity_error(
instance, "statistic"
),
) as session:
statistics_meta_manager.update_statistic_id(
session, DOMAIN, statistic_id, new_statistic_id
@@ -2246,54 +2252,6 @@ def async_add_external_statistics(
_async_import_statistics(hass, metadata, statistics)
def _filter_unique_constraint_integrity_error(
instance: Recorder,
) -> Callable[[Exception], bool]:
def _filter_unique_constraint_integrity_error(err: Exception) -> bool:
"""Handle unique constraint integrity errors."""
if not isinstance(err, StatementError):
return False
assert instance.engine is not None
dialect_name = instance.engine.dialect.name
ignore = False
if (
dialect_name == SupportedDialect.SQLITE
and "UNIQUE constraint failed" in str(err)
):
ignore = True
if (
dialect_name == SupportedDialect.POSTGRESQL
and err.orig
and hasattr(err.orig, "pgcode")
and err.orig.pgcode == "23505"
):
ignore = True
if (
dialect_name == SupportedDialect.MYSQL
and err.orig
and hasattr(err.orig, "args")
):
with contextlib.suppress(TypeError):
if err.orig.args[0] == 1062:
ignore = True
if ignore:
_LOGGER.warning(
(
"Blocked attempt to insert duplicated statistic rows, please report"
" at %s"
),
"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+recorder%22",
exc_info=err,
)
return ignore
return _filter_unique_constraint_integrity_error
def _import_statistics_with_session(
instance: Recorder,
session: Session,
@@ -2397,7 +2355,9 @@ def import_statistics(
with session_scope(
session=instance.get_session(),
exception_filter=_filter_unique_constraint_integrity_error(instance),
exception_filter=filter_unique_constraint_integrity_error(
instance, "statistic"
),
) as session:
return _import_statistics_with_session(
instance, session, metadata, statistics, table

View File

@@ -307,11 +307,18 @@ class StatisticsMetaManager:
recorder thread.
"""
self._assert_in_recorder_thread()
if self.get(session, new_statistic_id):
_LOGGER.error(
"Cannot rename statistic_id `%s` to `%s` because the new statistic_id is already in use",
old_statistic_id,
new_statistic_id,
)
return
session.query(StatisticsMeta).filter(
(StatisticsMeta.statistic_id == old_statistic_id)
& (StatisticsMeta.source == source)
).update({StatisticsMeta.statistic_id: new_statistic_id})
self._clear_cache([old_statistic_id, new_statistic_id])
self._clear_cache([old_statistic_id])
def delete(self, session: Session, statistic_ids: list[str]) -> None:
"""Clear statistics for a list of statistic_ids.

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Callable, Collection, Generator, Iterable, Sequence
import contextlib
from contextlib import contextmanager
from datetime import date, datetime, timedelta
import functools
@@ -21,7 +22,7 @@ import ciso8601
from sqlalchemy import inspect, text
from sqlalchemy.engine import Result, Row
from sqlalchemy.engine.interfaces import DBAPIConnection
from sqlalchemy.exc import OperationalError, SQLAlchemyError
from sqlalchemy.exc import OperationalError, SQLAlchemyError, StatementError
from sqlalchemy.orm.query import Query
from sqlalchemy.orm.session import Session
from sqlalchemy.sql.lambdas import StatementLambdaElement
@@ -906,3 +907,54 @@ def get_index_by_name(session: Session, table_name: str, index_name: str) -> str
),
None,
)
def filter_unique_constraint_integrity_error(
instance: Recorder, row_type: str
) -> Callable[[Exception], bool]:
"""Create a filter for unique constraint integrity errors."""
def _filter_unique_constraint_integrity_error(err: Exception) -> bool:
"""Handle unique constraint integrity errors."""
if not isinstance(err, StatementError):
return False
assert instance.engine is not None
dialect_name = instance.engine.dialect.name
ignore = False
if (
dialect_name == SupportedDialect.SQLITE
and "UNIQUE constraint failed" in str(err)
):
ignore = True
if (
dialect_name == SupportedDialect.POSTGRESQL
and err.orig
and hasattr(err.orig, "pgcode")
and err.orig.pgcode == "23505"
):
ignore = True
if (
dialect_name == SupportedDialect.MYSQL
and err.orig
and hasattr(err.orig, "args")
):
with contextlib.suppress(TypeError):
if err.orig.args[0] == 1062:
ignore = True
if ignore:
_LOGGER.warning(
(
"Blocked attempt to insert duplicated %s rows, please report"
" at %s"
),
row_type,
"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+recorder%22",
exc_info=err,
)
return ignore
return _filter_unique_constraint_integrity_error

View File

@@ -18,5 +18,5 @@
"documentation": "https://www.home-assistant.io/integrations/reolink",
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"requirements": ["reolink-aio==0.8.8"]
"requirements": ["reolink-aio==0.8.9"]
}

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