Compare commits

..

113 Commits

Author SHA1 Message Date
Franck Nijhof
2f244b2b66 2025.3.4 (#141081) 2025-03-21 21:21:43 +01:00
Martin Hjelmare
1b7e53fd01 Improve Home Connect appliances test fixture (#139787)
Improve Home Connect appliances fixture
2025-03-21 19:45:18 +00:00
Franck Nijhof
bfabf972a8 Bump version to 2025.3.4 2025-03-21 19:35:24 +00:00
Luke Lashley
c0c997eed8 Bump python-snoo to 0.6.4 (#141030) 2025-03-21 19:35:03 +00:00
Luke Lashley
14b07087dc Bump Python-Snoo to 0.6.3 (#140628)
Bump python-Snoo to 0.6.3
2025-03-21 19:34:59 +00:00
puddly
f54a634563 Bump ZHA to 0.0.53 (#141025)
* Bump ZHA to 0.0.53

* Regenerate snapshot
2025-03-21 19:33:41 +00:00
J. Diego Rodríguez Royo
e98d518b0b Fix some Home Connect options keys (#141023)
Fix some options keys
2025-03-21 19:33:38 +00:00
starkillerOG
121ee27105 Reolink fix playback headers (#141015) 2025-03-21 19:33:35 +00:00
Ivan Lopez Hernandez
5681f4f2ea Ensure file is correctly uploaded by the GenAI SDK (#140969)
Opened the file outside of the SDK
2025-03-21 19:33:32 +00:00
Joost Lekkerkerker
8a63fa3bb7 Log SmartThings subscription error on exception (#140939) 2025-03-21 19:33:28 +00:00
Josef Zweck
983a2f513d Bump pylamarzocco to 1.4.9 (#140916) 2025-03-21 19:33:24 +00:00
Joost Lekkerkerker
aab349e787 Fix SmartThings ACs without supported AC modes (#140744) 2025-03-21 19:31:08 +00:00
Joost Lekkerkerker
21ced23c3c Bump pySmartThings to 2.7.4 (#140720)
* Bump pySmartThings to 2.7.3

* Bump pySmartThings to 2.7.3

* Fix

* Fix

* Fix
2025-03-21 19:25:33 +00:00
Josef Zweck
a453e9d4c2 Don't reload onedrive on options flow (#140712) 2025-03-21 19:21:22 +00:00
Adam Feldman
3f493dce06 Fix broken core integration Smart Meter Texas by switching it to use HA's SSL Context (#140694)
* Update __init__.py to use HA's SSLContext

* Update config_flow.py to use HA's SSLContext

* Use default context for config_flow.py

* Use default context instead in __init__.py

Co-authored-by: Josef Zweck <josef@zweck.dev>

* Fix import in __init__.py

* Fix import in config_flow.py

---------

Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-03-21 19:19:06 +00:00
Joost Lekkerkerker
403fe36489 Check Celsius in SmartThings oven setpoint (#140687) 2025-03-21 19:19:03 +00:00
J. Nick Koston
66fd7d9e8a Bump PySwitchBot to 0.57.1 (#140681)
changelog: https://github.com/sblibs/pySwitchbot/compare/0.56.1...0.57.1

fixes #140405
2025-03-21 19:19:00 +00:00
Glenn Waters
c9ceade10d Fix Elk-M1 missing TLS 1.2 check (#140672)
* Fix for missing TLS 1.2 check

* Fix error message.

* combine startswith

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2025-03-21 19:18:57 +00:00
Joost Lekkerkerker
85b6b3a360 Make Oven setpoint follow temperature UoM in SmartThings (#140666) 2025-03-21 19:18:08 +00:00
Maikel Punie
a2102f9b98 Fix optional password in Velbus config flow (#140615)
* Fix velbusconfigflow

* add tests

* Paramtize the tests

* Removed duplicate test in favor of another case

* more comments
2025-03-21 19:12:20 +00:00
J. Diego Rodríguez Royo
28cad1d085 Handle non documented options at Home Connect select entities (#140608)
* Allow non documented options at select entities

* Don't allow undocumented options
2025-03-21 19:12:17 +00:00
J. Diego Rodríguez Royo
9d8dbfbf3f Add 700 RPM option to washer spin speed options at Home Connect (#140607)
Add 700 RPM option to washer spin speed options
2025-03-21 19:12:14 +00:00
Hessel
1382a001e3 Change max ICP value to fixed value for Wallbox Integration (#140592)
change max ICP value to fixed value

Co-authored-by: Hessel van Es <hessel@datadragons.nl>
2025-03-21 19:12:10 +00:00
Pete Sage
88e3dcccda Album art not available for Sonos media library favorites (#140557)
* get album art uri for favorites

* add tests

* update typing

* update typing

* update typing

* simplify
2025-03-21 19:12:07 +00:00
J. Diego Rodríguez Royo
43e24cf833 Handle API rate limit error on Home Connect entities fetch (#139384)
* Handle API rate limit error on entities fetch

* Apply suggestions

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

* Add decorator (does not work)

* Fix decorator

* Apply suggestions

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

* Add test

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-03-21 19:12:03 +00:00
J. Diego Rodríguez Royo
65aef40a3f Fix initial fetch of Home Connect appliance data to handle API rate limit errors (#139379)
* Fix initial fetch of appliance data to handle API rate limit errors

* Apply comments

* Delete stale function

* Handle api rate limit error at options fetching

* Update appliances after stream non-breaking error

* Always initialize coordinator data

* Improve device update

* Update test description

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

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-03-21 19:11:58 +00:00
Franck Nijhof
4d1c89f0d1 2025.3.3 (#140583) 2025-03-14 12:55:51 +01:00
Franck Nijhof
831f2dc30e Bump version to 2025.3.3 2025-03-14 09:56:13 +00:00
ashionky
1566ab3b28 Fix missing UnitOfPower.MILLIWATT in sensor and number allowed units (#140567)
* MILLIWATT

* MILLIWATT
2025-03-14 09:53:08 +00:00
Joost Lekkerkerker
c852e1398c Set unit of measurement for SmartThings oven setpoint (#140560) 2025-03-14 09:53:05 +00:00
Joost Lekkerkerker
761be9342e Fix windowShadeLevel capability in SmartThings (#140552) 2025-03-14 09:52:58 +00:00
Maciej Bieniek
54ad44a574 Fix Shelly diagnostics for devices without WebSocket Outbound support (#140501)
* Don't assume that `ws` is always in config

* Fix device
2025-03-14 09:49:52 +00:00
Matthias Alphart
fed4015bab Update xknxproject to 3.8.2 (#140499) 2025-03-14 09:49:49 +00:00
Brett Adams
019a0ebf9b Bump Tesla Fleet API to 0.9.13 (#140485) 2025-03-14 09:49:45 +00:00
Joost Lekkerkerker
7607b7d494 Mark value in number.set_value action as required (#140445) 2025-03-14 09:49:40 +00:00
Maikel Punie
8b96a9606d Bump velbusaio to 2025.3.1 (#140443) 2025-03-14 09:49:37 +00:00
Jan-Philipp Benecke
6349821037 Only do WebDAV path migration when path differs (#140402) 2025-03-14 09:49:33 +00:00
Louis Christ
db26a42734 Use only IPv4 for zeroconf in bluesound integration (#140226)
* Use only ipv4 for zeroconf

* Fix tests

* Use only ip_address for ip version check

* Add test

* Reduce test
2025-03-14 09:49:30 +00:00
Glenn Waters
74fe35f44e Bump upb-lib to 0.6.1 (#140212) 2025-03-14 09:49:26 +00:00
jb101010-2
e648716ddf Bump pysuezV2 to 2.0.4 (#139824) 2025-03-14 09:49:23 +00:00
Luke Lashley
2e20245cdf Fix bug with all Roborock maps being set to the wrong map when empty (#138493)
* Fix bug with all maps being set to the same when empty

* fix parens

* fix other parens

* rework some of the logic

* few small updates

* Remove test that is no longer relevant

* remove updated time bump
2025-03-14 09:49:19 +00:00
Franck Nijhof
a12915fc14 2025.3.2 (#140392)
* Don't allow creating backups if Home Assistant is not running (#139499)

* Don't allow creating backups if hass is not running

* Revert "Don't allow creating backups if hass is not running"

This reverts commit 1bf545eb25f20fc27fe161691a94531cba7e005c.

* Set backup manager to idle only after Home Assistant has started

* Update according to discussion, add tests

* Add more test

* Bump govee_ble to 0.43.1 (#139862)

Bump govee_ble to 0.43.0

* Label emergency heat switch (#139872)

* Add label to emergency heat switch

* Use sentence case names

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

---------

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

* Bump sense-energy lib to 0.13.7 (#140068)

* Update jinja to 3.1.6 (#140069)

* Update evohome-async to 1.0.3 (#140083)

bump client to 1.0.3

* Fix HEOS discovery error when previously ignored (#140091)

Abort ignored discovery

* Map prewash job state in SmartThings (#140097)

* Check support for thermostat operating state in SmartThings (#140103)

* Handle None options in SmartThings (#140110)

* Handle None options in SmartThings

* Handle None options in SmartThings

* Fix MQTT JSON light not reporting color temp status if color is not supported (#140113)

* Fix HEOS user initiated setup when discovery is waiting confirmation (#140119)

* Support null supported Thermostat modes in SmartThings (#140101)

* Set device class for Oven Completion time in SmartThings (#140139)

* Revert "Check if the unit of measurement is valid before creating the entity" (#140155)

Revert "Check if the unit of measurement is valid before creating the entity …"

This reverts commit 99e1a7a676.

* Fix the order of the group members attribute of the Music Assistant integration (#140204)

* Fix events without user in Bring integration (#140213)

Fix events without publicUserUuid

* Log broad exception in Electricity Maps config flow (#140219)

* Bump evohome-async to 1.0.4 to fix  #140194 (#140230)

bump client, add test for fix  #140194

* Refresh Home Connect token during config entry setup (#140233)

* Refresh token during config entry setup

* Test 500 error

---------

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

* Add 900 RPM option to washer spin speed options at Home Connect (#140234)

Add 900 RPM option to washer spin speed options

* Fix todo tool broken with Gemini 2.0 models. (#140246)

* Change tool name for addlist item

* Change to HasListAddItem

* extract to function

* Fix version not always available in onewire (#140260)

* Fix `client_id` not generated when connecting to the MQTT broker (#140264)

Fix client_id not generated when connecting to the MQTT broker

* Bump velbusaio to 2025.3.0 (#140267)

* Fix dryer operating state in SmartThings (#140277)

* FGLair : Upgrade to ayla-iot-unofficial 1.4.7 (#140296)

Upgrade to ayla-iot-unofficial 1.4.7

* Bump pyheos to v1.0.3 (#140310)

Bump pyheos v1.0.3

* Bump ZHA to 0.0.52 (#140325)

* Bump pydrawise to 2025.3.0 (#140330)

* Bump teslemetry-stream (#140335)

Bump

* Fix no temperature unit in SmartThings (#140363)

* Fix double space quoting in WebDAV (#140364)

* Bump python-roborock to 2.12.2 (#140368)

bump python roboorck to 2.12.2

* Handle incomplete power consumption reports in SmartThings (#140370)

* Fix browsing Audible Favorites in Sonos (#140378)

* initial commit

* updates

* update test data

* Make sure SmartThings light can deal with unknown states (#140190)

* Fix

* add comment

* Make light unknown

* Make light unknown

* Delete subscription on shutdown of SmartThings (#140135)

* Cache subscription url in SmartThings

* Cache subscription url in SmartThings

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Bump pysmartthings to 2.7.1

* 2.7.2

---------

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

* Bump version to 2025.3.2

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Evan Farrell <evan@evanfarrell.com>
Co-authored-by: John Hillery <34005807+jrhillery@users.noreply.github.com>
Co-authored-by: Keilin Bickar <TrumpetGod@gmail.com>
Co-authored-by: David Bonnes <zxdavb@bonnes.me>
Co-authored-by: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Co-authored-by: msm595 <msm595@users.noreply.github.com>
Co-authored-by: Manu <4445816+tr4nt0r@users.noreply.github.com>
Co-authored-by: Jan-Philipp Benecke <jan-philipp@bnck.me>
Co-authored-by: J. Diego Rodríguez Royo <jdrr1998@hotmail.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Luke Lashley <conway220@gmail.com>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Maikel Punie <maikel.punie@gmail.com>
Co-authored-by: Antoine Reversat <a.reversat@gmail.com>
Co-authored-by: puddly <32534428+puddly@users.noreply.github.com>
Co-authored-by: David Knowles <dknowles2@gmail.com>
Co-authored-by: Brett Adams <Bre77@users.noreply.github.com>
Co-authored-by: Pete Sage <76050312+PeteRager@users.noreply.github.com>
2025-03-11 17:36:00 +01:00
Franck Nijhof
3d5e4b980f Bump version to 2025.3.2 2025-03-11 15:22:38 +00:00
Joost Lekkerkerker
f2f653efcf Delete subscription on shutdown of SmartThings (#140135)
* Cache subscription url in SmartThings

* Cache subscription url in SmartThings

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Bump pysmartthings to 2.7.1

* 2.7.2

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-03-11 15:21:27 +00:00
Joost Lekkerkerker
b5c7bdd98f Make sure SmartThings light can deal with unknown states (#140190)
* Fix

* add comment

* Make light unknown

* Make light unknown
2025-03-11 14:58:36 +00:00
Pete Sage
38e6133202 Fix browsing Audible Favorites in Sonos (#140378)
* initial commit

* updates

* update test data
2025-03-11 14:58:02 +00:00
Joost Lekkerkerker
8541dc5bde Handle incomplete power consumption reports in SmartThings (#140370) 2025-03-11 14:57:58 +00:00
Luke Lashley
5327996bad Bump python-roborock to 2.12.2 (#140368)
bump python roboorck to 2.12.2
2025-03-11 14:57:55 +00:00
Jan-Philipp Benecke
4ddc43a9d9 Fix double space quoting in WebDAV (#140364) 2025-03-11 14:57:51 +00:00
Joost Lekkerkerker
e6dea4179b Fix no temperature unit in SmartThings (#140363) 2025-03-11 14:57:47 +00:00
Brett Adams
0318b85517 Bump teslemetry-stream (#140335)
Bump
2025-03-11 14:57:43 +00:00
David Knowles
29987d443e Bump pydrawise to 2025.3.0 (#140330) 2025-03-11 14:57:27 +00:00
puddly
cbfd8707b9 Bump ZHA to 0.0.52 (#140325) 2025-03-11 14:57:18 +00:00
Andrew Sayre
5f158f5c87 Bump pyheos to v1.0.3 (#140310)
Bump pyheos v1.0.3
2025-03-11 14:57:14 +00:00
Antoine Reversat
d67ccd2fce FGLair : Upgrade to ayla-iot-unofficial 1.4.7 (#140296)
Upgrade to ayla-iot-unofficial 1.4.7
2025-03-11 14:57:10 +00:00
Joost Lekkerkerker
29c9d3804b Fix dryer operating state in SmartThings (#140277) 2025-03-11 14:57:07 +00:00
Maikel Punie
76d478c84f Bump velbusaio to 2025.3.0 (#140267) 2025-03-11 14:56:54 +00:00
Jan Bouwhuis
5d9d6f099c Fix client_id not generated when connecting to the MQTT broker (#140264)
Fix client_id not generated when connecting to the MQTT broker
2025-03-11 14:56:49 +00:00
epenet
e4b31640b3 Fix version not always available in onewire (#140260) 2025-03-11 14:56:43 +00:00
Luke Lashley
c43f6a67d0 Fix todo tool broken with Gemini 2.0 models. (#140246)
* Change tool name for addlist item

* Change to HasListAddItem

* extract to function
2025-03-11 14:55:51 +00:00
J. Diego Rodríguez Royo
0bbab63193 Add 900 RPM option to washer spin speed options at Home Connect (#140234)
Add 900 RPM option to washer spin speed options
2025-03-11 14:52:37 +00:00
J. Diego Rodríguez Royo
06188b8fbd Refresh Home Connect token during config entry setup (#140233)
* Refresh token during config entry setup

* Test 500 error

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-03-11 14:52:33 +00:00
David Bonnes
bbbb5cadd4 Bump evohome-async to 1.0.4 to fix #140194 (#140230)
bump client, add test for fix  #140194
2025-03-11 14:52:29 +00:00
Jan-Philipp Benecke
52fcdda429 Log broad exception in Electricity Maps config flow (#140219) 2025-03-11 14:52:25 +00:00
Manu
7d93ceb0f0 Fix events without user in Bring integration (#140213)
Fix events without publicUserUuid
2025-03-11 14:52:22 +00:00
msm595
873e4b77eb Fix the order of the group members attribute of the Music Assistant integration (#140204) 2025-03-11 14:52:17 +00:00
Jan Bouwhuis
61f0eabcbb Revert "Check if the unit of measurement is valid before creating the entity" (#140155)
Revert "Check if the unit of measurement is valid before creating the entity …"

This reverts commit 99e1a7a676.
2025-03-11 14:50:10 +00:00
Joost Lekkerkerker
134b5319e1 Set device class for Oven Completion time in SmartThings (#140139) 2025-03-11 14:50:05 +00:00
Joost Lekkerkerker
ee78e21950 Support null supported Thermostat modes in SmartThings (#140101) 2025-03-11 14:47:50 +00:00
Andrew Sayre
323bc54efc Fix HEOS user initiated setup when discovery is waiting confirmation (#140119) 2025-03-11 14:46:18 +00:00
Jan Bouwhuis
fd2dee3c11 Fix MQTT JSON light not reporting color temp status if color is not supported (#140113) 2025-03-11 14:46:14 +00:00
Joost Lekkerkerker
fc53322c07 Handle None options in SmartThings (#140110)
* Handle None options in SmartThings

* Handle None options in SmartThings
2025-03-11 14:46:08 +00:00
Joost Lekkerkerker
faf9977abb Check support for thermostat operating state in SmartThings (#140103) 2025-03-11 14:46:03 +00:00
Joost Lekkerkerker
7336c8fc07 Map prewash job state in SmartThings (#140097) 2025-03-11 14:36:17 +00:00
Andrew Sayre
5cfaeda95b Fix HEOS discovery error when previously ignored (#140091)
Abort ignored discovery
2025-03-11 14:36:13 +00:00
David Bonnes
a78e9039c6 Update evohome-async to 1.0.3 (#140083)
bump client to 1.0.3
2025-03-11 14:36:10 +00:00
Franck Nijhof
227f3cea25 Update jinja to 3.1.6 (#140069) 2025-03-11 14:36:06 +00:00
Keilin Bickar
cab4890246 Bump sense-energy lib to 0.13.7 (#140068) 2025-03-11 14:36:03 +00:00
John Hillery
95fd096bdd Label emergency heat switch (#139872)
* Add label to emergency heat switch

* Use sentence case names

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

---------

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2025-03-11 14:35:59 +00:00
Evan Farrell
91cf8cb547 Bump govee_ble to 0.43.1 (#139862)
Bump govee_ble to 0.43.0
2025-03-11 14:35:56 +00:00
Erik Montnemery
3ce4f3f918 Don't allow creating backups if Home Assistant is not running (#139499)
* Don't allow creating backups if hass is not running

* Revert "Don't allow creating backups if hass is not running"

This reverts commit 1bf545eb25f20fc27fe161691a94531cba7e005c.

* Set backup manager to idle only after Home Assistant has started

* Update according to discussion, add tests

* Add more test
2025-03-11 14:35:46 +00:00
Franck Nijhof
4e89948b5c 2025.3.1 (#140061) 2025-03-07 18:54:39 +01:00
Franck Nijhof
9f95383201 Bump version to 2025.3.1 2025-03-07 17:03:29 +00:00
Joost Lekkerkerker
7e452521c8 Restore SmartThings button event (#140044)
* Restore SmartThings button event

* Fix
2025-03-07 17:03:16 +00:00
Michael
991de6f1d0 Bump py-synologydsm-api to 2.7.1 (#140052)
bump py-synologydsm-api to 2.7.1
2025-03-07 16:49:07 +00:00
Joost Lekkerkerker
be32e3fe8f Only keep valid powerConsumptionReports in SmartThings (#140049)
* power consumption report

* Only keep valid powerConsumptionReports in SmartThings
2025-03-07 16:49:03 +00:00
Joost Lekkerkerker
d6eb61e9ec Bump pysmartthings to 2.7.0 (#140047) 2025-03-07 16:49:00 +00:00
Joost Lekkerkerker
e74fe69d65 Fix SmartThings thermostat climate check (#140046)
* Fix SmartThings thermostat climate check

* Add tests
2025-03-07 16:48:55 +00:00
Joost Lekkerkerker
208406123e Fix SmartThings disabling working capabilities (#140039) 2025-03-07 16:03:40 +00:00
David Bonnes
8bcd135f3d Fix evohome to gracefully handle null schedules (#140036)
* extend tests to catch null schedules

* add fixture with null schedule

* remove null schedules for now

* fic the typing for _schedule attr (is list, not dict)

* add valid schedule to fixture

* update ssetpoints only if there is a schedule

* snapshot to match last change

* refactor: dont update switchpoints if no schedule

* add in warnings for null schedules

* add fixture for DHW without schedule
2025-03-07 16:03:36 +00:00
hahn-th
e7ea0e435e Add description for HomematicIP HCU1 in homematicip_cloud setup config flow (#140025)
add description for hcu1
2025-03-07 16:03:33 +00:00
Brett Adams
b15b680cfe Fix shift state default in Teslemetry and Tessie (#140018)
* Fix again

* Fix Tessie

* Update snap
2025-03-07 16:03:29 +00:00
Brett Adams
5e26d98bdf Fix powerwall 0% in Tessie and Tesla Fleet (#140017)
Fix powerwall zero
2025-03-07 16:03:26 +00:00
Martin Hjelmare
9f94ee280a Bump aiohomeconnect to 0.16.3 (#140014) 2025-03-07 16:03:23 +00:00
J. Diego Rodríguez Royo
efa98539fa Check operation state on Home Connect program sensor update (#140011)
Check operation state on program sensor update
2025-03-07 16:03:19 +00:00
David Bonnes
113cd4bfcc Fix regression to evohome debug logging (#140000)
* fix regression in debug logging

* lint
2025-03-07 16:03:15 +00:00
Ivan Lopez Hernandez
ccbaf76e44 Correctly retrieve only loaded Google Generative AI config_entries (#139999)
* Correctly retrieve only loaded config_entries

* Ruff
2025-03-07 16:03:08 +00:00
Jan-Philipp Benecke
5d9d93d3a1 Bump aiowebdav2 to 0.4.1 (#139988) 2025-03-07 16:03:04 +00:00
J. Nick Koston
c2c5274aac Bump nexia to 2.2.2 (#139986)
changelog: https://github.com/bdraco/nexia/compare/2.2.1...2.2.2
2025-03-07 16:03:01 +00:00
Joost Lekkerkerker
89756394c9 Fix SmartThings dust sensor UoM (#139977) 2025-03-07 16:02:57 +00:00
Bram Kragten
352aa88e79 Update frontend to 20250306.0 (#139965) 2025-03-07 16:02:54 +00:00
Joost Lekkerkerker
714962bd7a Fix SmartThings fan (#139962) 2025-03-07 16:02:50 +00:00
Luke Lashley
fb4c50b5dc Bump to python-snoo 0.6.1 (#139954) 2025-03-07 16:02:47 +00:00
Jan-Philipp Benecke
b4794b2029 Set content length when uploading files to WebDAV (#139950) 2025-03-07 16:02:43 +00:00
Joost Lekkerkerker
3a8c8accfe Add config entry level diagnostics to SmartThings (#139939)
* Add config entry level diagnostics to SmartThings

* Add config entry level diagnostics to SmartThings

* Add config entry level diagnostics to SmartThings
2025-03-07 16:02:40 +00:00
Jan-Philipp Benecke
844adfc590 Bump aiowebdav2 to 0.4.0 (#139938) 2025-03-07 16:02:36 +00:00
Joost Lekkerkerker
a279e23fb5 Bump pysmartthings to 2.6.1 (#139936)
* Bump pysmartthings to 2.6.1

* Bump pysmartthings to 2.6.1
2025-03-07 15:58:00 +00:00
Jan Bouwhuis
af9bbd0585 Check if the unit of measurement is valid before creating the entity (#139932) 2025-03-07 15:50:55 +00:00
Joost Lekkerkerker
1304194f09 Deduplicate climate modes in SmartThings (#139930)
* Deduplicate climate modes in SmartThings

* Deduplicate climate modes in SmartThings
2025-03-07 15:50:51 +00:00
J. Nick Koston
e909417a3f Bump thermobeacon-ble to 0.8.1 (#139919)
changelog: https://github.com/Bluetooth-Devices/thermobeacon-ble/compare/v0.8.0...v0.8.1

fixes #139917
2025-03-07 15:50:48 +00:00
Ivan Lopez Hernandez
02706c116d Trim the Schema allowed keys to match the Public Gemini API docs. (#139876)
* Trim the Schema allowed types to match the Public API docs, not the SDK types as those do not match

* Testing
2025-03-07 15:50:43 +00:00
peteS-UK
3af6b5cb4c Fix Unit of Measurement for Squeezebox duration sensor entity on LMS service (#139861)
UOM Fix
2025-03-07 15:42:02 +00:00
Ishima
35c1bb1ec5 Check support for demand load control in SmartThings AC (#139616)
* Check support for demand load control in SmartThings AC

* Fix

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
2025-03-07 15:41:52 +00:00
196 changed files with 15317 additions and 2074 deletions

View File

@@ -118,6 +118,7 @@ class BackupManagerState(StrEnum):
IDLE = "idle"
CREATE_BACKUP = "create_backup"
BLOCKED = "blocked"
RECEIVE_BACKUP = "receive_backup"
RESTORE_BACKUP = "restore_backup"
@@ -226,6 +227,13 @@ class RestoreBackupEvent(ManagerStateEvent):
state: RestoreBackupState
@dataclass(frozen=True, kw_only=True, slots=True)
class BlockedEvent(ManagerStateEvent):
"""Backup manager blocked, Home Assistant is starting."""
manager_state: BackupManagerState = BackupManagerState.BLOCKED
class BackupPlatformProtocol(Protocol):
"""Define the format that backup platforms can have."""
@@ -340,7 +348,7 @@ class BackupManager:
self.remove_next_delete_event: Callable[[], None] | None = None
# Latest backup event and backup event subscribers
self.last_event: ManagerStateEvent = IdleEvent()
self.last_event: ManagerStateEvent = BlockedEvent()
self.last_non_idle_event: ManagerStateEvent | None = None
self._backup_event_subscriptions = hass.data[
DATA_BACKUP
@@ -354,10 +362,19 @@ class BackupManager:
self.known_backups.load(stored["backups"])
await self._reader_writer.async_validate_config(config=self.config)
await self._reader_writer.async_resume_restore_progress_after_restart(
on_progress=self.async_on_backup_event
)
async def set_manager_idle_after_start(hass: HomeAssistant) -> None:
"""Set manager to idle after start."""
self.async_on_backup_event(IdleEvent())
if self.state == BackupManagerState.BLOCKED:
# If we're not finishing a restore job, set the manager to idle after start
start.async_at_started(self.hass, set_manager_idle_after_start)
await self.load_platforms()
@property
@@ -1293,7 +1310,7 @@ class BackupManager:
if (current_state := self.state) != (new_state := event.manager_state):
LOGGER.debug("Backup state: %s -> %s", current_state, new_state)
self.last_event = event
if not isinstance(event, IdleEvent):
if not isinstance(event, (BlockedEvent, IdleEvent)):
self.last_non_idle_event = event
for subscription in self._backup_event_subscriptions:
subscription(event)

View File

@@ -75,6 +75,9 @@ class BluesoundConfigFlow(ConfigFlow, domain=DOMAIN):
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle a flow initialized by zeroconf discovery."""
# the player can have an ipv6 address, but the api is only available on ipv4
if discovery_info.ip_address.version != 4:
return self.async_abort(reason="no_ipv4_address")
if discovery_info.port is not None:
self._port = discovery_info.port

View File

@@ -19,7 +19,8 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"no_ipv4_address": "No IPv4 address found."
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"

View File

@@ -77,9 +77,12 @@ class BringEventEntity(BringBaseEntity, EventEntity):
attributes = asdict(activity.content)
attributes["last_activity_by"] = next(
x.name
for x in bring_list.users.users
if x.publicUuid == activity.content.publicUserUuid
(
x.name
for x in bring_list.users.users
if x.publicUuid == activity.content.publicUserUuid
),
None,
)
self._trigger_event(

View File

@@ -3,11 +3,11 @@
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
from aioelectricitymaps import (
ElectricityMaps,
ElectricityMapsError,
ElectricityMapsInvalidTokenError,
ElectricityMapsNoDataError,
)
@@ -36,6 +36,8 @@ TYPE_USE_HOME = "use_home_location"
TYPE_SPECIFY_COORDINATES = "specify_coordinates"
TYPE_SPECIFY_COUNTRY = "specify_country_code"
_LOGGER = logging.getLogger(__name__)
class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Co2signal."""
@@ -158,7 +160,8 @@ class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "invalid_auth"
except ElectricityMapsNoDataError:
errors["base"] = "no_data"
except ElectricityMapsError:
except Exception:
_LOGGER.exception("Unexpected error occurred while checking API key")
errors["base"] = "unknown"
else:
if self.source == SOURCE_REAUTH:

View File

@@ -101,9 +101,11 @@ def hostname_from_url(url: str) -> str:
def _host_validator(config: dict[str, str]) -> dict[str, str]:
"""Validate that a host is properly configured."""
if config[CONF_HOST].startswith("elks://"):
if config[CONF_HOST].startswith(("elks://", "elksv1_2://")):
if CONF_USERNAME not in config or CONF_PASSWORD not in config:
raise vol.Invalid("Specify username and password for elks://")
raise vol.Invalid(
"Specify username and password for elks:// or elksv1_2://"
)
elif not config[CONF_HOST].startswith("elk://") and not config[
CONF_HOST
].startswith("serial://"):

View File

@@ -6,5 +6,5 @@
"iot_class": "local_push",
"loggers": ["sense_energy"],
"quality_scale": "internal",
"requirements": ["sense-energy==0.13.6"]
"requirements": ["sense-energy==0.13.7"]
}

View File

@@ -11,6 +11,7 @@ from typing import Any
import evohomeasync as ec1
import evohomeasync2 as ec2
from evohomeasync2.const import (
SZ_DHW,
SZ_GATEWAY_ID,
SZ_GATEWAY_INFO,
SZ_GATEWAYS,
@@ -19,8 +20,9 @@ from evohomeasync2.const import (
SZ_TEMPERATURE_CONTROL_SYSTEMS,
SZ_TIME_ZONE,
SZ_USE_DAYLIGHT_SAVE_SWITCHING,
SZ_ZONES,
)
from evohomeasync2.schemas.typedefs import EvoLocStatusResponseT
from evohomeasync2.schemas.typedefs import EvoLocStatusResponseT, EvoTcsConfigResponseT
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant
@@ -113,17 +115,19 @@ class EvoDataUpdateCoordinator(DataUpdateCoordinator):
SZ_USE_DAYLIGHT_SAVE_SWITCHING
],
}
tcs_info: EvoTcsConfigResponseT = self.tcs.config # type: ignore[assignment]
tcs_info[SZ_ZONES] = [zone.config for zone in self.tcs.zones]
if self.tcs.hotwater:
tcs_info[SZ_DHW] = self.tcs.hotwater.config
gwy_info = {
SZ_GATEWAY_ID: self.loc.gateways[0].id,
SZ_TEMPERATURE_CONTROL_SYSTEMS: [
self.loc.gateways[0].systems[0].config
],
SZ_TEMPERATURE_CONTROL_SYSTEMS: [tcs_info],
}
config = {
SZ_LOCATION_INFO: loc_info,
SZ_GATEWAYS: [{SZ_GATEWAY_INFO: gwy_info}],
}
self.logger.debug("Config = %s", config)
self.logger.debug("Config = %s", [config])
async def call_client_api(
self,
@@ -203,10 +207,18 @@ class EvoDataUpdateCoordinator(DataUpdateCoordinator):
async def _update_v2_schedules(self) -> None:
for zone in self.tcs.zones:
await zone.get_schedule()
try:
await zone.get_schedule()
except ec2.InvalidScheduleError as err:
self.logger.warning(
"Zone '%s' has an invalid/missing schedule: %r", zone.name, err
)
if dhw := self.tcs.hotwater:
await dhw.get_schedule()
try:
await dhw.get_schedule()
except ec2.InvalidScheduleError as err:
self.logger.warning("DHW has an invalid/missing schedule: %r", err)
async def _async_update_data(self) -> EvoLocStatusResponseT: # type: ignore[override]
"""Fetch the latest state of an entire TCC Location.

View File

@@ -6,6 +6,7 @@ import logging
from typing import Any
import evohomeasync2 as evo
from evohomeasync2.schemas.typedefs import DayOfWeekDhwT
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -102,7 +103,7 @@ class EvoChild(EvoEntity):
self._evo_tcs = evo_device.tcs
self._schedule: dict[str, Any] | None = None
self._schedule: list[DayOfWeekDhwT] | None = None
self._setpoints: dict[str, Any] = {}
@property
@@ -123,6 +124,9 @@ class EvoChild(EvoEntity):
Only Zones & DHW controllers (but not the TCS) can have schedules.
"""
if not self._schedule:
return self._setpoints
this_sp_dtm, this_sp_val = self._evo_device.this_switchpoint
next_sp_dtm, next_sp_val = self._evo_device.next_switchpoint
@@ -152,10 +156,10 @@ class EvoChild(EvoEntity):
self._evo_device,
err,
)
self._schedule = {}
self._schedule = []
return
else:
self._schedule = schedule or {} # mypy hint
self._schedule = schedule # type: ignore[assignment]
_LOGGER.debug("Schedule['%s'] = %s", self.name, schedule)

View File

@@ -6,5 +6,5 @@
"iot_class": "cloud_polling",
"loggers": ["evohome", "evohomeasync", "evohomeasync2"],
"quality_scale": "legacy",
"requirements": ["evohome-async==1.0.2"]
"requirements": ["evohome-async==1.0.4"]
}

View File

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

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/fujitsu_fglair",
"iot_class": "cloud_polling",
"requirements": ["ayla-iot-unofficial==1.4.5"]
"requirements": ["ayla-iot-unofficial==1.4.7"]
}

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import mimetypes
from pathlib import Path
from google import genai # type: ignore[attr-defined]
@@ -65,9 +66,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
prompt_parts = [call.data[CONF_PROMPT]]
config_entry: GoogleGenerativeAIConfigEntry = hass.config_entries.async_entries(
DOMAIN
)[0]
config_entry: GoogleGenerativeAIConfigEntry = (
hass.config_entries.async_loaded_entries(DOMAIN)[0]
)
client = config_entry.runtime_data
@@ -83,7 +84,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
if not Path(filename).exists():
raise HomeAssistantError(f"`{filename}` does not exist")
prompt_parts.append(client.files.upload(file=filename))
mimetype = mimetypes.guess_type(filename)[0]
with open(filename, "rb") as file:
uploaded_file = client.files.upload(
file=file, config={"mime_type": mimetype}
)
prompt_parts.append(uploaded_file)
await hass.async_add_executor_job(append_files_to_prompt)

View File

@@ -64,28 +64,18 @@ async def async_setup_entry(
SUPPORTED_SCHEMA_KEYS = {
"min_items",
"example",
"property_ordering",
"pattern",
"minimum",
"default",
"any_of",
"max_length",
"title",
"min_properties",
"min_length",
"max_items",
"maximum",
"nullable",
"max_properties",
# Gemini API does not support all of the OpenAPI schema
# SoT: https://ai.google.dev/api/caching#Schema
"type",
"description",
"enum",
"format",
"items",
"description",
"nullable",
"enum",
"max_items",
"min_items",
"properties",
"required",
"items",
}
@@ -109,9 +99,7 @@ def _format_schema(schema: dict[str, Any]) -> Schema:
key = _camel_to_snake(key)
if key not in SUPPORTED_SCHEMA_KEYS:
continue
if key == "any_of":
val = [_format_schema(subschema) for subschema in val]
elif key == "type":
if key == "type":
val = val.upper()
elif key == "format":
# Gemini API does not support all formats, see: https://ai.google.dev/api/caching#Schema
@@ -288,6 +276,13 @@ class GoogleGenerativeAIConversationEntity(
):
return await self._async_handle_message(user_input, chat_log)
def _fix_tool_name(self, tool_name: str) -> str:
"""Fix tool name if needed."""
# The Gemini 2.0+ tokenizer seemingly has a issue with the HassListAddItem tool
# name. This makes sure when it incorrectly changes the name, that we change it
# back for HA to call.
return tool_name if tool_name != "HasListAddItem" else "HassListAddItem"
async def _async_handle_message(
self,
user_input: conversation.ConversationInput,
@@ -447,7 +442,10 @@ class GoogleGenerativeAIConversationEntity(
tool_name = tool_call.name
tool_args = _escape_decode(tool_call.args)
tool_calls.append(
llm.ToolInput(tool_name=tool_name, tool_args=tool_args)
llm.ToolInput(
tool_name=self._fix_tool_name(tool_name),
tool_args=tool_args,
)
)
chat_request = _create_google_tool_response_content(

View File

@@ -135,5 +135,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/govee_ble",
"iot_class": "local_push",
"requirements": ["govee-ble==0.43.0"]
"requirements": ["govee-ble==0.43.1"]
}

View File

@@ -14,7 +14,12 @@ from pyheos import (
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
from homeassistant.config_entries import (
SOURCE_IGNORE,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.helpers import selector
@@ -141,8 +146,10 @@ class HeosFlowHandler(ConfigFlow, domain=DOMAIN):
hostname = urlparse(discovery_info.ssdp_location).hostname
assert hostname is not None
# Abort early when discovered host is part of the current system
if entry and hostname in _get_current_hosts(entry):
# Abort early when discovery is ignored or host is part of the current system
if entry and (
entry.source == SOURCE_IGNORE or hostname in _get_current_hosts(entry)
):
return self.async_abort(reason="single_instance_allowed")
# Connect to discovered host and get system information
@@ -198,7 +205,7 @@ class HeosFlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Obtain host and validate connection."""
await self.async_set_unique_id(DOMAIN)
await self.async_set_unique_id(DOMAIN, raise_on_progress=False)
self._abort_if_unique_id_configured(error="single_instance_allowed")
# Try connecting to host if provided
errors: dict[str, str] = {}

View File

@@ -159,13 +159,12 @@ class HeosCoordinator(DataUpdateCoordinator[None]):
async def _async_on_reconnected(self) -> None:
"""Handle when reconnected so resources are updated and entities marked available."""
await self._async_update_players()
await self._async_update_sources()
_LOGGER.warning("Successfully reconnected to HEOS host %s", self.host)
self.async_update_listeners()
async def _async_on_controller_event(
self, event: str, data: PlayerUpdateResult | None
self, event: str, data: PlayerUpdateResult | None = None
) -> None:
"""Handle a controller event, such as players or groups changed."""
if event == const.EVENT_PLAYERS_CHANGED:

View File

@@ -8,7 +8,7 @@
"iot_class": "local_push",
"loggers": ["pyheos"],
"quality_scale": "platinum",
"requirements": ["pyheos==1.0.2"],
"requirements": ["pyheos==1.0.3"],
"ssdp": [
{
"st": "urn:schemas-denon-com:device:ACT-Denon:1"

View File

@@ -16,11 +16,17 @@ from aiohomeconnect.model import (
SettingKey,
)
from aiohomeconnect.model.error import HomeConnectError
import aiohttp
import voluptuous as vol
from homeassistant.const import ATTR_DEVICE_ID, Platform
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
ServiceValidationError,
)
from homeassistant.helpers import (
config_entry_oauth2_flow,
config_validation as cv,
@@ -611,18 +617,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeConnectConfigEntry)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
config_entry_auth = AsyncConfigEntryAuth(hass, session)
try:
await config_entry_auth.async_get_access_token()
except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed from err
raise ConfigEntryNotReady from err
except aiohttp.ClientError as err:
raise ConfigEntryNotReady from err
home_connect_client = HomeConnectClient(config_entry_auth)
coordinator = HomeConnectCoordinator(hass, entry, home_connect_client)
await coordinator.async_config_entry_first_refresh()
await coordinator.async_setup()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.runtime_data.start_event_listener()
entry.async_create_background_task(
hass,
coordinator.async_refresh(),
f"home_connect-initial-full-refresh-{entry.entry_id}",
)
return True

View File

@@ -137,41 +137,6 @@ def setup_home_connect_entry(
defaultdict(list)
)
entities: list[HomeConnectEntity] = []
for appliance in entry.runtime_data.data.values():
entities_to_add = get_entities_for_appliance(entry, appliance)
if get_option_entities_for_appliance:
entities_to_add.extend(get_option_entities_for_appliance(entry, appliance))
for event_key in (
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
):
changed_options_listener_remove_callback = (
entry.runtime_data.async_add_listener(
partial(
_create_option_entities,
entry,
appliance,
known_entity_unique_ids,
get_option_entities_for_appliance,
async_add_entities,
),
(appliance.info.ha_id, event_key),
)
)
entry.async_on_unload(changed_options_listener_remove_callback)
changed_options_listener_remove_callbacks[appliance.info.ha_id].append(
changed_options_listener_remove_callback
)
known_entity_unique_ids.update(
{
cast(str, entity.unique_id): appliance.info.ha_id
for entity in entities_to_add
}
)
entities.extend(entities_to_add)
async_add_entities(entities)
entry.async_on_unload(
entry.runtime_data.async_add_special_listener(
partial(

View File

@@ -10,6 +10,7 @@ from .utils import bsh_key_to_translation_key
DOMAIN = "home_connect"
API_DEFAULT_RETRY_AFTER = 60
APPLIANCES_WITH_PROGRAMS = (
"CleaningRobot",
@@ -284,7 +285,9 @@ SPIN_SPEED_OPTIONS = {
"LaundryCare.Washer.EnumType.SpinSpeed.Off",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM400",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM600",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM700",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM800",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM900",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1000",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1200",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1400",

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
import asyncio
from asyncio import sleep as asyncio_sleep
from collections import defaultdict
from collections.abc import Callable
from dataclasses import dataclass
@@ -29,6 +29,7 @@ from aiohomeconnect.model.error import (
HomeConnectApiError,
HomeConnectError,
HomeConnectRequestError,
TooManyRequestsError,
UnauthorizedError,
)
from aiohomeconnect.model.program import EnumerateProgram, ProgramDefinitionOption
@@ -36,11 +37,11 @@ from propcache.api import cached_property
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import APPLIANCES_WITH_PROGRAMS, DOMAIN
from .const import API_DEFAULT_RETRY_AFTER, APPLIANCES_WITH_PROGRAMS, DOMAIN
from .utils import get_dict_from_home_connect_error
_LOGGER = logging.getLogger(__name__)
@@ -154,7 +155,7 @@ class HomeConnectCoordinator(
f"home_connect-events_listener_task-{self.config_entry.entry_id}",
)
async def _event_listener(self) -> None:
async def _event_listener(self) -> None: # noqa: C901
"""Match event with listener for event type."""
retry_time = 10
while True:
@@ -269,7 +270,7 @@ class HomeConnectCoordinator(
type(error).__name__,
retry_time,
)
await asyncio.sleep(retry_time)
await asyncio_sleep(retry_time)
retry_time = min(retry_time * 2, 3600)
except HomeConnectApiError as error:
_LOGGER.error("Error while listening for events: %s", error)
@@ -278,6 +279,13 @@ class HomeConnectCoordinator(
)
break
# Trigger to delete the possible depaired device entities
# from known_entities variable at common.py
for listener, context in self._special_listeners.values():
assert isinstance(context, tuple)
if EventKey.BSH_COMMON_APPLIANCE_DEPAIRED in context:
listener()
@callback
def _call_event_listener(self, event_message: EventMessage) -> None:
"""Call listener for event."""
@@ -295,6 +303,42 @@ class HomeConnectCoordinator(
async def _async_update_data(self) -> dict[str, HomeConnectApplianceData]:
"""Fetch data from Home Connect."""
await self._async_setup()
for appliance_data in self.data.values():
appliance = appliance_data.info
ha_id = appliance.ha_id
while True:
try:
self.data[ha_id] = await self._get_appliance_data(
appliance, self.data.get(ha_id)
)
except TooManyRequestsError as err:
_LOGGER.debug(
"Rate limit exceeded on initial fetch: %s",
err,
)
await asyncio_sleep(err.retry_after or API_DEFAULT_RETRY_AFTER)
else:
break
for listener, context in self._special_listeners.values():
assert isinstance(context, tuple)
if EventKey.BSH_COMMON_APPLIANCE_PAIRED in context:
listener()
return self.data
async def async_setup(self) -> None:
"""Set up the devices."""
try:
await self._async_setup()
except UpdateFailed as err:
raise ConfigEntryNotReady from err
async def _async_setup(self) -> None:
"""Set up the devices."""
old_appliances = set(self.data.keys())
try:
appliances = await self.client.get_home_appliances()
except UnauthorizedError as error:
@@ -312,12 +356,38 @@ class HomeConnectCoordinator(
translation_placeholders=get_dict_from_home_connect_error(error),
) from error
return {
appliance.ha_id: await self._get_appliance_data(
appliance, self.data.get(appliance.ha_id)
for appliance in appliances.homeappliances:
self.device_registry.async_get_or_create(
config_entry_id=self.config_entry.entry_id,
identifiers={(DOMAIN, appliance.ha_id)},
manufacturer=appliance.brand,
name=appliance.name,
model=appliance.vib,
)
for appliance in appliances.homeappliances
}
if appliance.ha_id not in self.data:
self.data[appliance.ha_id] = HomeConnectApplianceData(
commands=set(),
events={},
info=appliance,
options={},
programs=[],
settings={},
status={},
)
else:
self.data[appliance.ha_id].info.connected = appliance.connected
old_appliances.remove(appliance.ha_id)
for ha_id in old_appliances:
self.data.pop(ha_id, None)
device = self.device_registry.async_get_device(
identifiers={(DOMAIN, ha_id)}
)
if device:
self.device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
async def _get_appliance_data(
self,
@@ -339,6 +409,8 @@ class HomeConnectCoordinator(
await self.client.get_settings(appliance.ha_id)
).settings
}
except TooManyRequestsError:
raise
except HomeConnectError as error:
_LOGGER.debug(
"Error fetching settings for %s: %s",
@@ -353,6 +425,8 @@ class HomeConnectCoordinator(
status.key: status
for status in (await self.client.get_status(appliance.ha_id)).status
}
except TooManyRequestsError:
raise
except HomeConnectError as error:
_LOGGER.debug(
"Error fetching status for %s: %s",
@@ -369,6 +443,8 @@ class HomeConnectCoordinator(
if appliance.type in APPLIANCES_WITH_PROGRAMS:
try:
all_programs = await self.client.get_all_programs(appliance.ha_id)
except TooManyRequestsError:
raise
except HomeConnectError as error:
_LOGGER.debug(
"Error fetching programs for %s: %s",
@@ -427,6 +503,8 @@ class HomeConnectCoordinator(
await self.client.get_available_commands(appliance.ha_id)
).commands
}
except TooManyRequestsError:
raise
except HomeConnectError:
commands = set()
@@ -461,6 +539,8 @@ class HomeConnectCoordinator(
).options
or []
}
except TooManyRequestsError:
raise
except HomeConnectError as error:
_LOGGER.debug(
"Error fetching options for %s: %s",

View File

@@ -1,21 +1,28 @@
"""Home Connect entity base class."""
from abc import abstractmethod
from collections.abc import Callable, Coroutine
import contextlib
from datetime import datetime
import logging
from typing import cast
from typing import Any, Concatenate, cast
from aiohomeconnect.model import EventKey, OptionKey
from aiohomeconnect.model.error import ActiveProgramNotSetError, HomeConnectError
from aiohomeconnect.model.error import (
ActiveProgramNotSetError,
HomeConnectError,
TooManyRequestsError,
)
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .const import API_DEFAULT_RETRY_AFTER, DOMAIN
from .coordinator import HomeConnectApplianceData, HomeConnectCoordinator
from .utils import get_dict_from_home_connect_error
@@ -127,3 +134,34 @@ class HomeConnectOptionEntity(HomeConnectEntity):
def bsh_key(self) -> OptionKey:
"""Return the BSH key."""
return cast(OptionKey, self.entity_description.key)
def constraint_fetcher[_EntityT: HomeConnectEntity, **_P](
func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]],
) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]:
"""Decorate the function to catch Home Connect too many requests error and retry later.
If it needs to be called later, it will call async_write_ha_state function
"""
async def handler_to_return(
self: _EntityT, *args: _P.args, **kwargs: _P.kwargs
) -> None:
async def handler(_datetime: datetime | None = None) -> None:
try:
await func(self, *args, **kwargs)
except TooManyRequestsError as err:
if (retry_after := err.retry_after) is None:
retry_after = API_DEFAULT_RETRY_AFTER
async_call_later(self.hass, retry_after, handler)
except HomeConnectError as err:
_LOGGER.error(
"Error fetching constraints for %s: %s", self.entity_id, err
)
else:
if _datetime is not None:
self.async_write_ha_state()
await handler()
return handler_to_return

View File

@@ -7,6 +7,6 @@
"documentation": "https://www.home-assistant.io/integrations/home_connect",
"iot_class": "cloud_push",
"loggers": ["aiohomeconnect"],
"requirements": ["aiohomeconnect==0.16.2"],
"requirements": ["aiohomeconnect==0.16.3"],
"single_config_entry": true
}

View File

@@ -25,7 +25,7 @@ from .const import (
UNIT_MAP,
)
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
from .entity import HomeConnectEntity, HomeConnectOptionEntity
from .entity import HomeConnectEntity, HomeConnectOptionEntity, constraint_fetcher
from .utils import get_dict_from_home_connect_error
_LOGGER = logging.getLogger(__name__)
@@ -189,19 +189,25 @@ class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
},
) from err
@constraint_fetcher
async def async_fetch_constraints(self) -> None:
"""Fetch the max and min values and step for the number entity."""
try:
setting_key = cast(SettingKey, self.bsh_key)
data = self.appliance.settings.get(setting_key)
if not data or not data.unit or not data.constraints:
data = await self.coordinator.client.get_setting(
self.appliance.info.ha_id, setting_key=SettingKey(self.bsh_key)
self.appliance.info.ha_id, setting_key=setting_key
)
except HomeConnectError as err:
_LOGGER.error("An error occurred: %s", err)
else:
if data.unit:
self._attr_native_unit_of_measurement = data.unit
self.set_constraints(data)
def set_constraints(self, setting: GetSetting) -> None:
"""Set constraints for the number entity."""
if setting.unit:
self._attr_native_unit_of_measurement = UNIT_MAP.get(
setting.unit, setting.unit
)
if not (constraints := setting.constraints):
return
if constraints.max:
@@ -222,10 +228,10 @@ class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
"""When entity is added to hass."""
await super().async_added_to_hass()
data = self.appliance.settings[cast(SettingKey, self.bsh_key)]
self._attr_native_unit_of_measurement = data.unit
self.set_constraints(data)
if (
not hasattr(self, "_attr_native_min_value")
not hasattr(self, "_attr_native_unit_of_measurement")
or not hasattr(self, "_attr_native_min_value")
or not hasattr(self, "_attr_native_max_value")
or not hasattr(self, "_attr_native_step")
):
@@ -253,7 +259,6 @@ class HomeConnectOptionNumberEntity(HomeConnectOptionEntity, NumberEntity):
or candidate_unit != self._attr_native_unit_of_measurement
):
self._attr_native_unit_of_measurement = candidate_unit
self.__dict__.pop("unit_of_measurement", None)
option_constraints = option_definition.constraints
if option_constraints:
if (

View File

@@ -1,8 +1,8 @@
"""Provides a select platform for Home Connect."""
from collections.abc import Callable, Coroutine
import contextlib
from dataclasses import dataclass
import logging
from typing import Any, cast
from aiohomeconnect.client import Client as HomeConnectClient
@@ -47,9 +47,11 @@ from .coordinator import (
HomeConnectConfigEntry,
HomeConnectCoordinator,
)
from .entity import HomeConnectEntity, HomeConnectOptionEntity
from .entity import HomeConnectEntity, HomeConnectOptionEntity, constraint_fetcher
from .utils import bsh_key_to_translation_key, get_dict_from_home_connect_error
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
FUNCTIONAL_LIGHT_COLOR_TEMPERATURE_ENUM = {
@@ -413,6 +415,7 @@ class HomeConnectSelectEntity(HomeConnectEntity, SelectEntity):
"""Select setting class for Home Connect."""
entity_description: HomeConnectSelectEntityDescription
_original_option_keys: set[str | None]
def __init__(
self,
@@ -421,6 +424,7 @@ class HomeConnectSelectEntity(HomeConnectEntity, SelectEntity):
desc: HomeConnectSelectEntityDescription,
) -> None:
"""Initialize the entity."""
self._original_option_keys = set(desc.values_translation_key)
super().__init__(
coordinator,
appliance,
@@ -458,23 +462,29 @@ class HomeConnectSelectEntity(HomeConnectEntity, SelectEntity):
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
await self.async_fetch_options()
@constraint_fetcher
async def async_fetch_options(self) -> None:
"""Fetch options from the API."""
setting = self.appliance.settings.get(cast(SettingKey, self.bsh_key))
if (
not setting
or not setting.constraints
or not setting.constraints.allowed_values
):
with contextlib.suppress(HomeConnectError):
setting = await self.coordinator.client.get_setting(
self.appliance.info.ha_id,
setting_key=cast(SettingKey, self.bsh_key),
)
setting = await self.coordinator.client.get_setting(
self.appliance.info.ha_id,
setting_key=cast(SettingKey, self.bsh_key),
)
if setting and setting.constraints and setting.constraints.allowed_values:
self._original_option_keys = set(setting.constraints.allowed_values)
self._attr_options = [
self.entity_description.values_translation_key[option]
for option in setting.constraints.allowed_values
if option in self.entity_description.values_translation_key
for option in self._original_option_keys
if option is not None
and option in self.entity_description.values_translation_key
]
@@ -491,7 +501,7 @@ class HomeConnectSelectOptionEntity(HomeConnectOptionEntity, SelectEntity):
desc: HomeConnectSelectEntityDescription,
) -> None:
"""Initialize the entity."""
self._original_option_keys = set(desc.values_translation_key.keys())
self._original_option_keys = set(desc.values_translation_key)
super().__init__(
coordinator,
appliance,
@@ -524,5 +534,5 @@ class HomeConnectSelectOptionEntity(HomeConnectOptionEntity, SelectEntity):
self.entity_description.values_translation_key[option]
for option in self._original_option_keys
if option is not None
and option in self.entity_description.values_translation_key
]
self.__dict__.pop("options", None)

View File

@@ -1,12 +1,11 @@
"""Provides a sensor for Home Connect."""
import contextlib
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import cast
from aiohomeconnect.model import EventKey, StatusKey
from aiohomeconnect.model.error import HomeConnectError
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -28,7 +27,9 @@ from .const import (
UNIT_MAP,
)
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
from .entity import HomeConnectEntity
from .entity import HomeConnectEntity, constraint_fetcher
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
@@ -335,16 +336,14 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity):
else:
await self.fetch_unit()
@constraint_fetcher
async def fetch_unit(self) -> None:
"""Fetch the unit of measurement."""
with contextlib.suppress(HomeConnectError):
data = await self.coordinator.client.get_status_value(
self.appliance.info.ha_id, status_key=cast(StatusKey, self.bsh_key)
)
if data.unit:
self._attr_native_unit_of_measurement = UNIT_MAP.get(
data.unit, data.unit
)
data = await self.coordinator.client.get_status_value(
self.appliance.info.ha_id, status_key=cast(StatusKey, self.bsh_key)
)
if data.unit:
self._attr_native_unit_of_measurement = UNIT_MAP.get(data.unit, data.unit)
class HomeConnectProgramSensor(HomeConnectSensor):
@@ -386,6 +385,13 @@ class HomeConnectProgramSensor(HomeConnectSensor):
def update_native_value(self) -> None:
"""Update the program sensor's status."""
self.program_running = (
status := self.appliance.status.get(StatusKey.BSH_COMMON_OPERATION_STATE)
) is not None and status.value in [
BSH_OPERATION_STATE_RUN,
BSH_OPERATION_STATE_PAUSE,
BSH_OPERATION_STATE_FINISHED,
]
event = self.appliance.events.get(cast(EventKey, self.bsh_key))
if event:
self._update_native_value(event.value)

View File

@@ -468,11 +468,11 @@ set_program_and_options:
translation_key: venting_level
options:
- cooking_hood_enum_type_stage_fan_off
- cooking_hood_enum_type_stage_fan_stage01
- cooking_hood_enum_type_stage_fan_stage02
- cooking_hood_enum_type_stage_fan_stage03
- cooking_hood_enum_type_stage_fan_stage04
- cooking_hood_enum_type_stage_fan_stage05
- cooking_hood_enum_type_stage_fan_stage_01
- cooking_hood_enum_type_stage_fan_stage_02
- cooking_hood_enum_type_stage_fan_stage_03
- cooking_hood_enum_type_stage_fan_stage_04
- cooking_hood_enum_type_stage_fan_stage_05
cooking_hood_option_intensive_level:
example: cooking_hood_enum_type_intensive_stage_intensive_stage1
required: false
@@ -528,7 +528,7 @@ set_program_and_options:
collapsed: true
fields:
laundry_care_washer_option_temperature:
example: laundry_care_washer_enum_type_temperature_g_c40
example: laundry_care_washer_enum_type_temperature_g_c_40
required: false
selector:
select:
@@ -536,14 +536,14 @@ set_program_and_options:
translation_key: washer_temperature
options:
- laundry_care_washer_enum_type_temperature_cold
- laundry_care_washer_enum_type_temperature_g_c20
- laundry_care_washer_enum_type_temperature_g_c30
- laundry_care_washer_enum_type_temperature_g_c40
- laundry_care_washer_enum_type_temperature_g_c50
- laundry_care_washer_enum_type_temperature_g_c60
- laundry_care_washer_enum_type_temperature_g_c70
- laundry_care_washer_enum_type_temperature_g_c80
- laundry_care_washer_enum_type_temperature_g_c90
- laundry_care_washer_enum_type_temperature_g_c_20
- laundry_care_washer_enum_type_temperature_g_c_30
- laundry_care_washer_enum_type_temperature_g_c_40
- laundry_care_washer_enum_type_temperature_g_c_50
- laundry_care_washer_enum_type_temperature_g_c_60
- laundry_care_washer_enum_type_temperature_g_c_70
- laundry_care_washer_enum_type_temperature_g_c_80
- laundry_care_washer_enum_type_temperature_g_c_90
- laundry_care_washer_enum_type_temperature_ul_cold
- laundry_care_washer_enum_type_temperature_ul_warm
- laundry_care_washer_enum_type_temperature_ul_hot
@@ -557,13 +557,15 @@ set_program_and_options:
translation_key: spin_speed
options:
- laundry_care_washer_enum_type_spin_speed_off
- laundry_care_washer_enum_type_spin_speed_r_p_m400
- laundry_care_washer_enum_type_spin_speed_r_p_m600
- laundry_care_washer_enum_type_spin_speed_r_p_m800
- laundry_care_washer_enum_type_spin_speed_r_p_m1000
- laundry_care_washer_enum_type_spin_speed_r_p_m1200
- laundry_care_washer_enum_type_spin_speed_r_p_m1400
- laundry_care_washer_enum_type_spin_speed_r_p_m1600
- laundry_care_washer_enum_type_spin_speed_r_p_m_400
- laundry_care_washer_enum_type_spin_speed_r_p_m_600
- laundry_care_washer_enum_type_spin_speed_r_p_m_700
- laundry_care_washer_enum_type_spin_speed_r_p_m_800
- laundry_care_washer_enum_type_spin_speed_r_p_m_900
- laundry_care_washer_enum_type_spin_speed_r_p_m_1000
- laundry_care_washer_enum_type_spin_speed_r_p_m_1200
- laundry_care_washer_enum_type_spin_speed_r_p_m_1400
- laundry_care_washer_enum_type_spin_speed_r_p_m_1600
- laundry_care_washer_enum_type_spin_speed_ul_off
- laundry_care_washer_enum_type_spin_speed_ul_low
- laundry_care_washer_enum_type_spin_speed_ul_medium

View File

@@ -417,11 +417,11 @@
"venting_level": {
"options": {
"cooking_hood_enum_type_stage_fan_off": "Fan off",
"cooking_hood_enum_type_stage_fan_stage01": "Fan stage 1",
"cooking_hood_enum_type_stage_fan_stage02": "Fan stage 2",
"cooking_hood_enum_type_stage_fan_stage03": "Fan stage 3",
"cooking_hood_enum_type_stage_fan_stage04": "Fan stage 4",
"cooking_hood_enum_type_stage_fan_stage05": "Fan stage 5"
"cooking_hood_enum_type_stage_fan_stage_01": "Fan stage 1",
"cooking_hood_enum_type_stage_fan_stage_02": "Fan stage 2",
"cooking_hood_enum_type_stage_fan_stage_03": "Fan stage 3",
"cooking_hood_enum_type_stage_fan_stage_04": "Fan stage 4",
"cooking_hood_enum_type_stage_fan_stage_05": "Fan stage 5"
}
},
"intensive_level": {
@@ -441,14 +441,14 @@
"washer_temperature": {
"options": {
"laundry_care_washer_enum_type_temperature_cold": "Cold",
"laundry_care_washer_enum_type_temperature_g_c20": "20ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c30": "30ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c40": "40ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c50": "50ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c60": "60ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c70": "70ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c80": "80ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c90": "90ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c_20": "20ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c_30": "30ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c_40": "40ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c_50": "50ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c_60": "60ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c_70": "70ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c_80": "80ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c_90": "90ºC clothes",
"laundry_care_washer_enum_type_temperature_ul_cold": "Cold",
"laundry_care_washer_enum_type_temperature_ul_warm": "Warm",
"laundry_care_washer_enum_type_temperature_ul_hot": "Hot",
@@ -458,13 +458,15 @@
"spin_speed": {
"options": {
"laundry_care_washer_enum_type_spin_speed_off": "Off",
"laundry_care_washer_enum_type_spin_speed_r_p_m400": "400 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m600": "600 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m800": "800 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m1000": "1000 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m1200": "1200 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m1400": "1400 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m1600": "1600 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m_400": "400 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m_600": "600 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m_700": "700 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m_800": "800 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m_900": "900 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m_1000": "1000 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m_1200": "1200 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "1400 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "1600 rpm",
"laundry_care_washer_enum_type_spin_speed_ul_off": "Off",
"laundry_care_washer_enum_type_spin_speed_ul_low": "Low",
"laundry_care_washer_enum_type_spin_speed_ul_medium": "Medium",
@@ -1382,11 +1384,11 @@
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_hood_option_venting_level::name%]",
"state": {
"cooking_hood_enum_type_stage_fan_off": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_off%]",
"cooking_hood_enum_type_stage_fan_stage01": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage01%]",
"cooking_hood_enum_type_stage_fan_stage02": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage02%]",
"cooking_hood_enum_type_stage_fan_stage03": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage03%]",
"cooking_hood_enum_type_stage_fan_stage04": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage04%]",
"cooking_hood_enum_type_stage_fan_stage05": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage05%]"
"cooking_hood_enum_type_stage_fan_stage_01": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage_01%]",
"cooking_hood_enum_type_stage_fan_stage_02": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage_02%]",
"cooking_hood_enum_type_stage_fan_stage_03": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage_03%]",
"cooking_hood_enum_type_stage_fan_stage_04": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage_04%]",
"cooking_hood_enum_type_stage_fan_stage_05": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage_05%]"
}
},
"intensive_level": {
@@ -1409,14 +1411,14 @@
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_temperature::name%]",
"state": {
"laundry_care_washer_enum_type_temperature_cold": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_cold%]",
"laundry_care_washer_enum_type_temperature_g_c20": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c20%]",
"laundry_care_washer_enum_type_temperature_g_c30": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c30%]",
"laundry_care_washer_enum_type_temperature_g_c40": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c40%]",
"laundry_care_washer_enum_type_temperature_g_c50": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c50%]",
"laundry_care_washer_enum_type_temperature_g_c60": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c60%]",
"laundry_care_washer_enum_type_temperature_g_c70": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c70%]",
"laundry_care_washer_enum_type_temperature_g_c80": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c80%]",
"laundry_care_washer_enum_type_temperature_g_c90": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c90%]",
"laundry_care_washer_enum_type_temperature_g_c_20": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_20%]",
"laundry_care_washer_enum_type_temperature_g_c_30": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_30%]",
"laundry_care_washer_enum_type_temperature_g_c_40": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_40%]",
"laundry_care_washer_enum_type_temperature_g_c_50": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_50%]",
"laundry_care_washer_enum_type_temperature_g_c_60": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_60%]",
"laundry_care_washer_enum_type_temperature_g_c_70": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_70%]",
"laundry_care_washer_enum_type_temperature_g_c_80": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_80%]",
"laundry_care_washer_enum_type_temperature_g_c_90": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_90%]",
"laundry_care_washer_enum_type_temperature_ul_cold": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_ul_cold%]",
"laundry_care_washer_enum_type_temperature_ul_warm": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_ul_warm%]",
"laundry_care_washer_enum_type_temperature_ul_hot": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_ul_hot%]",
@@ -1427,13 +1429,15 @@
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_spin_speed::name%]",
"state": {
"laundry_care_washer_enum_type_spin_speed_off": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_off%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m400%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m600%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m800": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m800%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m1000": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1000%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m1200": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1200%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m1400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1400%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m1600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1600%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m_400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_400%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m_600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_600%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m_700": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_700%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m_800": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_800%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m_900": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_900%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m_1000": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1000%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m_1200": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1200%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1400%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1600%]",
"laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_off%]",
"laundry_care_washer_enum_type_spin_speed_ul_low": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_low%]",
"laundry_care_washer_enum_type_spin_speed_ul_medium": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_medium%]",

View File

@@ -3,6 +3,7 @@
"step": {
"init": {
"title": "Pick Homematic IP access point",
"description": "If you are about to register a **Homematic IP HCU1**, please press the button on top of the device before you continue.\n\nThe registration process must be completed within 5 minutes.",
"data": {
"hapid": "Access point ID (SGTIN)",
"pin": "[%key:common::config_flow::data::pin%]",

View File

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

View File

@@ -11,7 +11,7 @@
"loggers": ["xknx", "xknxproject"],
"requirements": [
"xknx==3.6.0",
"xknxproject==3.8.1",
"xknxproject==3.8.2",
"knx-frontend==2025.1.30.194235"
],
"single_config_entry": true

View File

@@ -61,6 +61,42 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
client=client,
)
# initialize the firmware update coordinator early to check the firmware version
firmware_device = LaMarzoccoMachine(
model=entry.data[CONF_MODEL],
serial_number=entry.unique_id,
name=entry.data[CONF_NAME],
cloud_client=cloud_client,
)
firmware_coordinator = LaMarzoccoFirmwareUpdateCoordinator(
hass, entry, firmware_device
)
await firmware_coordinator.async_config_entry_first_refresh()
gateway_version = version.parse(
firmware_device.firmware[FirmwareType.GATEWAY].current_version
)
if gateway_version >= version.parse("v5.0.9"):
# remove host from config entry, it is not supported anymore
data = {k: v for k, v in entry.data.items() if k != CONF_HOST}
hass.config_entries.async_update_entry(
entry,
data=data,
)
elif gateway_version < version.parse("v3.4-rc5"):
# incompatible gateway firmware, create an issue
ir.async_create_issue(
hass,
DOMAIN,
"unsupported_gateway_firmware",
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="unsupported_gateway_firmware",
translation_placeholders={"gateway_version": str(gateway_version)},
)
# initialize local API
local_client: LaMarzoccoLocalClient | None = None
if (host := entry.data.get(CONF_HOST)) is not None:
@@ -117,30 +153,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
coordinators = LaMarzoccoRuntimeData(
LaMarzoccoConfigUpdateCoordinator(hass, entry, device, local_client),
LaMarzoccoFirmwareUpdateCoordinator(hass, entry, device),
firmware_coordinator,
LaMarzoccoStatisticsUpdateCoordinator(hass, entry, device),
)
# API does not like concurrent requests, so no asyncio.gather here
await coordinators.config_coordinator.async_config_entry_first_refresh()
await coordinators.firmware_coordinator.async_config_entry_first_refresh()
await coordinators.statistics_coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinators
gateway_version = device.firmware[FirmwareType.GATEWAY].current_version
if version.parse(gateway_version) < version.parse("v3.4-rc5"):
# incompatible gateway firmware, create an issue
ir.async_create_issue(
hass,
DOMAIN,
"unsupported_gateway_firmware",
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="unsupported_gateway_firmware",
translation_placeholders={"gateway_version": gateway_version},
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def update_listener(

View File

@@ -37,5 +37,5 @@
"iot_class": "cloud_polling",
"loggers": ["pylamarzocco"],
"quality_scale": "platinum",
"requirements": ["pylamarzocco==1.4.7"]
"requirements": ["pylamarzocco==1.4.9"]
}

View File

@@ -144,9 +144,12 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = (
set_value_fn=lambda machine, value, key: machine.set_prebrew_time(
prebrew_off_time=value, key=key
),
native_value_fn=lambda config, key: config.prebrew_configuration[key].off_time,
native_value_fn=lambda config, key: config.prebrew_configuration[key][
0
].off_time,
available_fn=lambda device: len(device.config.prebrew_configuration) > 0
and device.config.prebrew_mode == PrebrewMode.PREBREW,
and device.config.prebrew_mode
in (PrebrewMode.PREBREW, PrebrewMode.PREBREW_ENABLED),
supported_fn=lambda coordinator: coordinator.device.model
!= MachineModel.GS3_MP,
),
@@ -162,9 +165,12 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = (
set_value_fn=lambda machine, value, key: machine.set_prebrew_time(
prebrew_on_time=value, key=key
),
native_value_fn=lambda config, key: config.prebrew_configuration[key].off_time,
native_value_fn=lambda config, key: config.prebrew_configuration[key][
0
].off_time,
available_fn=lambda device: len(device.config.prebrew_configuration) > 0
and device.config.prebrew_mode == PrebrewMode.PREBREW,
and device.config.prebrew_mode
in (PrebrewMode.PREBREW, PrebrewMode.PREBREW_ENABLED),
supported_fn=lambda coordinator: coordinator.device.model
!= MachineModel.GS3_MP,
),
@@ -180,8 +186,8 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = (
set_value_fn=lambda machine, value, key: machine.set_preinfusion_time(
preinfusion_time=value, key=key
),
native_value_fn=lambda config, key: config.prebrew_configuration[
key
native_value_fn=lambda config, key: config.prebrew_configuration[key][
1
].preinfusion_time,
available_fn=lambda device: len(device.config.prebrew_configuration) > 0
and device.config.prebrew_mode == PrebrewMode.PREINFUSION,

View File

@@ -38,6 +38,7 @@ STEAM_LEVEL_LM_TO_HA = {value: key for key, value in STEAM_LEVEL_HA_TO_LM.items(
PREBREW_MODE_HA_TO_LM = {
"disabled": PrebrewMode.DISABLED,
"prebrew": PrebrewMode.PREBREW,
"prebrew_enabled": PrebrewMode.PREBREW_ENABLED,
"preinfusion": PrebrewMode.PREINFUSION,
}

View File

@@ -148,6 +148,7 @@
"state": {
"disabled": "Disabled",
"prebrew": "Prebrew",
"prebrew_enabled": "Prebrew",
"preinfusion": "Preinfusion"
}
},

View File

@@ -15,6 +15,7 @@ import socket
import ssl
import time
from typing import TYPE_CHECKING, Any
from uuid import uuid4
import certifi
@@ -292,7 +293,7 @@ class MqttClientSetup:
"""
# We don't import on the top because some integrations
# should be able to optionally rely on MQTT.
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
from paho.mqtt import client as mqtt # pylint: disable=import-outside-toplevel
# pylint: disable-next=import-outside-toplevel
from .async_client import AsyncMQTTClient
@@ -309,9 +310,10 @@ class MqttClientSetup:
clean_session = True
if (client_id := config.get(CONF_CLIENT_ID)) is None:
# PAHO MQTT relies on the MQTT server to generate random client IDs.
# However, that feature is not mandatory so we generate our own.
client_id = None
# PAHO MQTT relies on the MQTT server to generate random client ID
# for protocol version 3.1, however, that feature is not mandatory
# so we generate our own.
client_id = mqtt._base62(uuid4().int, padding=22) # noqa: SLF001
transport: str = config.get(CONF_TRANSPORT, DEFAULT_TRANSPORT)
self._client = AsyncMQTTClient(
callback_api_version=mqtt.CallbackAPIVersion.VERSION2,

View File

@@ -31,7 +31,6 @@ from homeassistant.components.light import (
LightEntity,
LightEntityFeature,
brightness_supported,
color_supported,
valid_supported_color_modes,
)
from homeassistant.const import (
@@ -293,7 +292,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
elif values["state"] is None:
self._attr_is_on = None
if color_supported(self.supported_color_modes) and "color_mode" in values:
if "color_mode" in values:
self._update_color(values)
if brightness_supported(self.supported_color_modes):

View File

@@ -276,22 +276,26 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
self._attr_state = MediaPlayerState(player.state.value)
else:
self._attr_state = MediaPlayerState(STATE_OFF)
group_members_entity_ids: list[str] = []
group_members: list[str] = []
if player.group_childs:
# translate MA group_childs to HA group_members as entity id's
entity_registry = er.async_get(self.hass)
group_members_entity_ids = [
entity_id
for child_id in player.group_childs
if (
entity_id := entity_registry.async_get_entity_id(
self.platform.domain, DOMAIN, child_id
)
group_members = player.group_childs
elif player.synced_to and (parent := self.mass.players.get(player.synced_to)):
group_members = parent.group_childs
# translate MA group_childs to HA group_members as entity id's
entity_registry = er.async_get(self.hass)
group_members_entity_ids: list[str] = [
entity_id
for child_id in group_members
if (
entity_id := entity_registry.async_get_entity_id(
self.platform.domain, DOMAIN, child_id
)
]
# NOTE: we sort the group_members for now,
# until the MA API returns them sorted (group_childs is now a set)
self._attr_group_members = sorted(group_members_entity_ids)
)
]
self._attr_group_members = group_members_entity_ids
self._attr_volume_level = (
player.volume_level / 100 if player.volume_level is not None else None
)

View File

@@ -12,5 +12,5 @@
"documentation": "https://www.home-assistant.io/integrations/nexia",
"iot_class": "cloud_polling",
"loggers": ["nexia"],
"requirements": ["nexia==2.2.1"]
"requirements": ["nexia==2.2.2"]
}

View File

@@ -58,6 +58,9 @@
"switch": {
"hold": {
"name": "Hold"
},
"emergency_heat": {
"name": "Emergency heat"
}
}
},

View File

@@ -486,6 +486,7 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
NumberDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.POWER_FACTOR: {PERCENTAGE, None},
NumberDeviceClass.POWER: {
UnitOfPower.MILLIWATT,
UnitOfPower.WATT,
UnitOfPower.KILO_WATT,
UnitOfPower.MEGA_WATT,

View File

@@ -7,5 +7,6 @@ set_value:
fields:
value:
example: 42
required: true
selector:
text:

View File

@@ -97,11 +97,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: OneDriveConfigEntry) ->
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def update_listener(hass: HomeAssistant, entry: OneDriveConfigEntry) -> None:
await hass.config_entries.async_reload(entry.entry_id)
entry.async_on_unload(entry.add_update_listener(update_listener))
def async_notify_backup_listeners() -> None:
for listener in hass.data.get(DATA_BACKUP_AGENT_LISTENERS, []):
listener()

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import contextlib
from datetime import datetime, timedelta
import logging
import os
@@ -58,7 +59,7 @@ class OneWireHub:
owproxy: protocol._Proxy
devices: list[OWDeviceDescription]
_version: str
_version: str | None = None
def __init__(self, hass: HomeAssistant, config_entry: OneWireConfigEntry) -> None:
"""Initialize."""
@@ -74,7 +75,9 @@ class OneWireHub:
port = self._config_entry.data[CONF_PORT]
_LOGGER.debug("Initializing connection to %s:%s", host, port)
self.owproxy = protocol.proxy(host, port)
self._version = self.owproxy.read(protocol.PTH_VERSION).decode()
with contextlib.suppress(protocol.OwnetError):
# Version is not available on all servers
self._version = self.owproxy.read(protocol.PTH_VERSION).decode()
self.devices = _discover_devices(self.owproxy)
async def initialize(self) -> None:

View File

@@ -83,7 +83,16 @@ class PlaybackProxyView(HomeAssistantView):
_LOGGER.warning("Reolink playback proxy error: %s", str(err))
return web.Response(body=str(err), status=HTTPStatus.BAD_REQUEST)
headers = dict(request.headers)
headers.pop("Host", None)
headers.pop("Referer", None)
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug(
"Requested Playback Proxy Method %s, Headers: %s",
request.method,
headers,
)
_LOGGER.debug(
"Opening VOD stream from %s: %s",
host.api.camera_name(ch),
@@ -93,6 +102,7 @@ class PlaybackProxyView(HomeAssistantView):
try:
reolink_response = await self.session.get(
reolink_url,
headers=headers,
timeout=ClientTimeout(
connect=15, sock_connect=15, sock_read=5, total=None
),
@@ -118,18 +128,25 @@ class PlaybackProxyView(HomeAssistantView):
]:
err_str = f"Reolink playback expected video/mp4 but got {reolink_response.content_type}"
_LOGGER.error(err_str)
if reolink_response.content_type == "text/html":
text = await reolink_response.text()
_LOGGER.debug(text)
return web.Response(body=err_str, status=HTTPStatus.BAD_REQUEST)
response = web.StreamResponse(
status=200,
reason="OK",
headers={
"Content-Type": "video/mp4",
},
response_headers = dict(reolink_response.headers)
_LOGGER.debug(
"Response Playback Proxy Status %s:%s, Headers: %s",
reolink_response.status,
reolink_response.reason,
response_headers,
)
response_headers["Content-Type"] = "video/mp4"
if reolink_response.content_length is not None:
response.content_length = reolink_response.content_length
response = web.StreamResponse(
status=reolink_response.status,
reason=reolink_response.reason,
headers=response_headers,
)
await response.prepare(request)
@@ -141,7 +158,8 @@ class PlaybackProxyView(HomeAssistantView):
"Timeout while reading Reolink playback from %s, writing EOF",
host.api.nvr_name,
)
finally:
reolink_response.release()
reolink_response.release()
await response.write_eof()
return response

View File

@@ -112,19 +112,6 @@ class RoborockMap(RoborockCoordinatedEntityV1, ImageEntity):
"""Return if this map is the currently selected map."""
return self.map_flag == self.coordinator.current_map
def is_map_valid(self) -> bool:
"""Update the map if it is valid.
Update this map if it is the currently active map, and the
vacuum is cleaning, or if it has never been set at all.
"""
return self.cached_map == b"" or (
self.is_selected
and self.image_last_updated is not None
and self.coordinator.roborock_device_info.props.status is not None
and bool(self.coordinator.roborock_device_info.props.status.in_cleaning)
)
async def async_added_to_hass(self) -> None:
"""When entity is added to hass load any previously cached maps from disk."""
await super().async_added_to_hass()
@@ -137,15 +124,22 @@ class RoborockMap(RoborockCoordinatedEntityV1, ImageEntity):
# Bump last updated every third time the coordinator runs, so that async_image
# will be called and we will evaluate on the new coordinator data if we should
# update the cache.
if (
dt_util.utcnow() - self.image_last_updated
).total_seconds() > IMAGE_CACHE_INTERVAL and self.is_map_valid():
if self.is_selected and (
(
(dt_util.utcnow() - self.image_last_updated).total_seconds()
> IMAGE_CACHE_INTERVAL
and self.coordinator.roborock_device_info.props.status is not None
and bool(self.coordinator.roborock_device_info.props.status.in_cleaning)
)
or self.cached_map == b""
):
# This will tell async_image it should update.
self._attr_image_last_updated = dt_util.utcnow()
super()._handle_coordinator_update()
async def async_image(self) -> bytes | None:
"""Update the image if it is not cached."""
if self.is_map_valid():
if self.is_selected:
response = await asyncio.gather(
*(
self.cloud_api.get_map_v1(),

View File

@@ -7,7 +7,7 @@
"iot_class": "local_polling",
"loggers": ["roborock"],
"requirements": [
"python-roborock==2.11.1",
"python-roborock==2.12.2",
"vacuum-map-parser-roborock==0.1.2"
]
}

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/sense",
"iot_class": "cloud_polling",
"loggers": ["sense_energy"],
"requirements": ["sense-energy==0.13.6"]
"requirements": ["sense-energy==0.13.7"]
}

View File

@@ -582,6 +582,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
SensorDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.POWER_FACTOR: {PERCENTAGE, None},
SensorDeviceClass.POWER: {
UnitOfPower.MILLIWATT,
UnitOfPower.WATT,
UnitOfPower.KILO_WATT,
UnitOfPower.MEGA_WATT,

View File

@@ -74,12 +74,14 @@ async def async_get_config_entry_diagnostics(
device_settings = {
k: v for k, v in rpc_coordinator.device.config.items() if k in ["cloud"]
}
ws_config = rpc_coordinator.device.config["ws"]
device_settings["ws_outbound_enabled"] = ws_config["enable"]
if ws_config["enable"]:
device_settings["ws_outbound_server_valid"] = bool(
ws_config["server"] == get_rpc_ws_url(hass)
)
if not (ws_config := rpc_coordinator.device.config.get("ws", {})):
device_settings["ws_outbound"] = "not supported"
if (ws_outbound_enabled := ws_config.get("enable")) is not None:
device_settings["ws_outbound_enabled"] = ws_outbound_enabled
if ws_outbound_enabled:
device_settings["ws_outbound_server_valid"] = bool(
ws_config["server"] == get_rpc_ws_url(hass)
)
device_status = {
k: v
for k, v in rpc_coordinator.device.status.items()

View File

@@ -3,7 +3,7 @@
import logging
import ssl
from smart_meter_texas import Account, Client, ClientSSLContext
from smart_meter_texas import Account, Client
from smart_meter_texas.exceptions import (
SmartMeterTexasAPIError,
SmartMeterTexasAuthError,
@@ -16,6 +16,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.ssl import get_default_context
from .const import (
DATA_COORDINATOR,
@@ -38,8 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
account = Account(username, password)
client_ssl_context = ClientSSLContext()
ssl_context = await client_ssl_context.get_ssl_context()
ssl_context = get_default_context()
smart_meter_texas_data = SmartMeterTexasData(hass, entry, account, ssl_context)
try:

View File

@@ -4,7 +4,7 @@ import logging
from typing import Any
from aiohttp import ClientError
from smart_meter_texas import Account, Client, ClientSSLContext
from smart_meter_texas import Account, Client
from smart_meter_texas.exceptions import (
SmartMeterTexasAPIError,
SmartMeterTexasAuthError,
@@ -16,6 +16,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import aiohttp_client
from homeassistant.util.ssl import get_default_context
from .const import DOMAIN
@@ -31,8 +32,7 @@ async def validate_input(hass: HomeAssistant, data):
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
client_ssl_context = ClientSSLContext()
ssl_context = await client_ssl_context.get_ssl_context()
ssl_context = get_default_context()
client_session = aiohttp_client.async_get_clientsession(hass)
account = Account(data["username"], data["password"])
client = Client(client_session, account, ssl_context)

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import TYPE_CHECKING, cast
@@ -11,15 +12,22 @@ from pysmartthings import (
Attribute,
Capability,
Device,
DeviceEvent,
Scene,
SmartThings,
SmartThingsAuthenticationFailedError,
SmartThingsSinkError,
Status,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.const import (
CONF_ACCESS_TOKEN,
CONF_TOKEN,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -28,7 +36,15 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
async_get_config_entry_implementation,
)
from .const import CONF_INSTALLED_APP_ID, CONF_LOCATION_ID, DOMAIN, MAIN, OLD_DATA
from .const import (
CONF_INSTALLED_APP_ID,
CONF_LOCATION_ID,
CONF_SUBSCRIPTION_ID,
DOMAIN,
EVENT_BUTTON,
MAIN,
OLD_DATA,
)
_LOGGER = logging.getLogger(__name__)
@@ -90,6 +106,54 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartThingsConfigEntry)
client.refresh_token_function = _refresh_token
def _handle_max_connections() -> None:
_LOGGER.debug("We hit the limit of max connections")
hass.config_entries.async_schedule_reload(entry.entry_id)
client.max_connections_reached_callback = _handle_max_connections
def _handle_new_subscription_identifier(identifier: str | None) -> None:
"""Handle a new subscription identifier."""
hass.config_entries.async_update_entry(
entry,
data={
**entry.data,
CONF_SUBSCRIPTION_ID: identifier,
},
)
if identifier is not None:
_LOGGER.debug("Updating subscription ID to %s", identifier)
else:
_LOGGER.debug("Removing subscription ID")
client.new_subscription_id_callback = _handle_new_subscription_identifier
if (old_identifier := entry.data.get(CONF_SUBSCRIPTION_ID)) is not None:
_LOGGER.debug("Trying to delete old subscription %s", old_identifier)
await client.delete_subscription(old_identifier)
_LOGGER.debug("Trying to create a new subscription")
try:
subscription = await client.create_subscription(
entry.data[CONF_LOCATION_ID],
entry.data[CONF_TOKEN][CONF_INSTALLED_APP_ID],
)
except SmartThingsSinkError as err:
_LOGGER.exception("Couldn't create a new subscription")
raise ConfigEntryNotReady from err
subscription_id = subscription.subscription_id
_handle_new_subscription_identifier(subscription_id)
entry.async_create_background_task(
hass,
client.subscribe(
entry.data[CONF_LOCATION_ID],
entry.data[CONF_TOKEN][CONF_INSTALLED_APP_ID],
subscription,
),
"smartthings_socket",
)
device_status: dict[str, FullDevice] = {}
try:
devices = await client.get_devices()
@@ -114,12 +178,34 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartThingsConfigEntry)
scenes=scenes,
)
entry.async_create_background_task(
hass,
client.subscribe(
entry.data[CONF_LOCATION_ID], entry.data[CONF_TOKEN][CONF_INSTALLED_APP_ID]
),
"smartthings_webhook",
def handle_button_press(event: DeviceEvent) -> None:
"""Handle a button press."""
if (
event.capability is Capability.BUTTON
and event.attribute is Attribute.BUTTON
):
hass.bus.async_fire(
EVENT_BUTTON,
{
"component_id": event.component_id,
"device_id": event.device_id,
"location_id": event.location_id,
"value": event.value,
"name": entry.runtime_data.devices[event.device_id].device.label,
"data": event.data,
},
)
entry.async_on_unload(
client.add_unspecified_device_event_listener(handle_button_press)
)
async def _handle_shutdown(_: Event) -> None:
"""Handle shutdown."""
await client.delete_subscription(subscription_id)
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _handle_shutdown)
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -145,6 +231,9 @@ async def async_unload_entry(
hass: HomeAssistant, entry: SmartThingsConfigEntry
) -> bool:
"""Unload a config entry."""
client = entry.runtime_data.client
if (subscription_id := entry.data.get(CONF_SUBSCRIPTION_ID)) is not None:
await client.delete_subscription(subscription_id)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -160,26 +249,39 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
KEEP_CAPABILITY_QUIRK: dict[
Capability | str, Callable[[dict[Attribute | str, Status]], bool]
] = {
Capability.DRYER_OPERATING_STATE: (
lambda status: status[Attribute.SUPPORTED_MACHINE_STATES].value is not None
),
Capability.WASHER_OPERATING_STATE: (
lambda status: status[Attribute.SUPPORTED_MACHINE_STATES].value is not None
),
Capability.DEMAND_RESPONSE_LOAD_CONTROL: lambda _: True,
}
def process_status(
status: dict[str, dict[Capability | str, dict[Attribute | str, Status]]],
) -> dict[str, dict[Capability | str, dict[Attribute | str, Status]]]:
"""Remove disabled capabilities from status."""
if (main_component := status.get("main")) is None or (
if (main_component := status.get(MAIN)) is None:
return status
if (
disabled_capabilities_capability := main_component.get(
Capability.CUSTOM_DISABLED_CAPABILITIES
)
) is None:
return status
disabled_capabilities = cast(
list[Capability | str],
disabled_capabilities_capability[Attribute.DISABLED_CAPABILITIES].value,
)
if disabled_capabilities is not None:
for capability in disabled_capabilities:
# We still need to make sure the climate entity can work without this capability
if (
capability in main_component
and capability != Capability.DEMAND_RESPONSE_LOAD_CONTROL
):
del main_component[capability]
) is not None:
disabled_capabilities = cast(
list[Capability | str],
disabled_capabilities_capability[Attribute.DISABLED_CAPABILITIES].value,
)
if disabled_capabilities is not None:
for capability in disabled_capabilities:
if capability in main_component and (
capability not in KEEP_CAPABILITY_QUIRK
or not KEEP_CAPABILITY_QUIRK[capability](main_component[capability])
):
del main_component[capability]
return status

View File

@@ -161,9 +161,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity):
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
if self.get_attribute_value(
Capability.THERMOSTAT_FAN_MODE, Attribute.THERMOSTAT_FAN_MODE
):
if self.supports_capability(Capability.THERMOSTAT_FAN_MODE):
flags |= ClimateEntityFeature.FAN_MODE
return flags
@@ -253,6 +251,8 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity):
@property
def hvac_action(self) -> HVACAction | None:
"""Return the current running hvac operation if supported."""
if not self.supports_capability(Capability.THERMOSTAT_OPERATING_STATE):
return None
return OPERATING_STATE_TO_ACTION.get(
self.get_attribute_value(
Capability.THERMOSTAT_OPERATING_STATE,
@@ -272,11 +272,15 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity):
@property
def hvac_modes(self) -> list[HVACMode]:
"""Return the list of available operation modes."""
return [
state
for mode in self.get_attribute_value(
if (
supported_thermostat_modes := self.get_attribute_value(
Capability.THERMOSTAT_MODE, Attribute.SUPPORTED_THERMOSTAT_MODES
)
) is None:
return []
return [
state
for mode in supported_thermostat_modes
if (state := AC_MODE_TO_STATE.get(mode)) is not None
]
@@ -314,10 +318,14 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity):
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
unit = self._internal_state[Capability.TEMPERATURE_MEASUREMENT][
Attribute.TEMPERATURE
].unit
assert unit
# Offline third party thermostats may not have a unit
# Since climate always requires a unit, default to Celsius
if (
unit := self._internal_state[Capability.TEMPERATURE_MEASUREMENT][
Attribute.TEMPERATURE
].unit
) is None:
return UnitOfTemperature.CELSIUS
return UNIT_MAP[unit]
@@ -445,12 +453,15 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
)
@property
def extra_state_attributes(self) -> dict[str, Any]:
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return device specific state attributes.
Include attributes from the Demand Response Load Control (drlc)
and Power Consumption capabilities.
"""
if not self.supports_capability(Capability.DEMAND_RESPONSE_LOAD_CONTROL):
return None
drlc_status = self.get_attribute_value(
Capability.DEMAND_RESPONSE_LOAD_CONTROL,
Attribute.DEMAND_RESPONSE_LOAD_CONTROL_STATUS,
@@ -554,11 +565,15 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
def _determine_hvac_modes(self) -> list[HVACMode]:
"""Determine the supported HVAC modes."""
modes = [HVACMode.OFF]
modes.extend(
state
for mode in self.get_attribute_value(
if (
ac_modes := self.get_attribute_value(
Capability.AIR_CONDITIONER_MODE, Attribute.SUPPORTED_AC_MODES
)
if (state := AC_MODE_TO_STATE.get(mode)) is not None
)
) is not None:
modes.extend(
state
for mode in ac_modes
if (state := AC_MODE_TO_STATE.get(mode)) is not None
if state not in modes
)
return modes

View File

@@ -32,3 +32,6 @@ CONF_REFRESH_TOKEN = "refresh_token"
MAIN = "main"
OLD_DATA = "old_data"
CONF_SUBSCRIPTION_ID = "subscription_id"
EVENT_BUTTON = "smartthings.button"

View File

@@ -118,6 +118,10 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity):
self._attr_current_cover_position = self.get_attribute_value(
Capability.SWITCH_LEVEL, Attribute.LEVEL
)
elif self.supports_capability(Capability.WINDOW_SHADE_LEVEL):
self._attr_current_cover_position = self.get_attribute_value(
Capability.WINDOW_SHADE_LEVEL, Attribute.SHADE_LEVEL
)
self._attr_extra_state_attributes = {}
if self.supports_capability(Capability.BATTERY):

View File

@@ -17,6 +17,15 @@ from .const import DOMAIN
EVENT_WAIT_TIME = 5
async def async_get_config_entry_diagnostics(
hass: HomeAssistant,
entry: SmartThingsConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
client = entry.runtime_data.client
return {"devices": await client.get_raw_devices()}
async def async_get_device_diagnostics(
hass: HomeAssistant, entry: SmartThingsConfigEntry, device: DeviceEntry
) -> dict[str, Any]:
@@ -26,7 +35,8 @@ async def async_get_device_diagnostics(
identifier for identifier in device.identifiers if identifier[0] == DOMAIN
)[1]
device_status = await client.get_device_status(device_id)
device_status = await client.get_raw_device_status(device_id)
device_info = await client.get_raw_device(device_id)
events: list[DeviceEvent] = []
@@ -39,11 +49,8 @@ async def async_get_device_diagnostics(
listener()
status: dict[str, Any] = {}
for component, capabilities in device_status.items():
status[component] = {}
for capability, attributes in capabilities.items():
status[component][capability] = {}
for attribute, value in attributes.items():
status[component][capability][attribute] = asdict(value)
return {"events": [asdict(event) for event in events], "status": status}
return {
"events": [asdict(event) for event in events],
"status": device_status,
"info": device_info,
}

View File

@@ -48,7 +48,9 @@ class SmartThingsEntity(Entity):
self._attr_device_info.update(
{
"manufacturer": ocf.manufacturer_name,
"model": ocf.model_number.split("|")[0],
"model": (
(ocf.model_number.split("|")[0]) if ocf.model_number else None
),
"hw_version": ocf.hardware_version,
"sw_version": ocf.firmware_version,
}

View File

@@ -116,7 +116,7 @@ class SmartThingsFan(SmartThingsEntity, FanEntity):
@property
def is_on(self) -> bool:
"""Return true if fan is on."""
return self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH)
return self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "on"
@property
def percentage(self) -> int | None:
@@ -132,6 +132,8 @@ class SmartThingsFan(SmartThingsEntity, FanEntity):
Requires FanEntityFeature.PRESET_MODE.
"""
if not self.supports_capability(Capability.AIR_CONDITIONER_FAN_MODE):
return None
return self.get_attribute_value(
Capability.AIR_CONDITIONER_FAN_MODE, Attribute.FAN_MODE
)
@@ -142,6 +144,8 @@ class SmartThingsFan(SmartThingsEntity, FanEntity):
Requires FanEntityFeature.PRESET_MODE.
"""
if not self.supports_capability(Capability.AIR_CONDITIONER_FAN_MODE):
return None
return self.get_attribute_value(
Capability.AIR_CONDITIONER_FAN_MODE, Attribute.SUPPORTED_AC_FAN_MODES
)

View File

@@ -147,14 +147,21 @@ class SmartThingsLight(SmartThingsEntity, LightEntity, RestoreEntity):
"""Update entity attributes when the device status has changed."""
# Brightness and transition
if brightness_supported(self._attr_supported_color_modes):
self._attr_brightness = int(
convert_scale(
self.get_attribute_value(Capability.SWITCH_LEVEL, Attribute.LEVEL),
100,
255,
0,
if (
brightness := self.get_attribute_value(
Capability.SWITCH_LEVEL, Attribute.LEVEL
)
) is None:
self._attr_brightness = None
else:
self._attr_brightness = int(
convert_scale(
brightness,
100,
255,
0,
)
)
)
# Color Temperature
if ColorMode.COLOR_TEMP in self._attr_supported_color_modes:
self._attr_color_temp_kelvin = self.get_attribute_value(
@@ -162,16 +169,21 @@ class SmartThingsLight(SmartThingsEntity, LightEntity, RestoreEntity):
)
# Color
if ColorMode.HS in self._attr_supported_color_modes:
self._attr_hs_color = (
convert_scale(
self.get_attribute_value(Capability.COLOR_CONTROL, Attribute.HUE),
100,
360,
),
self.get_attribute_value(
Capability.COLOR_CONTROL, Attribute.SATURATION
),
)
if (
hue := self.get_attribute_value(Capability.COLOR_CONTROL, Attribute.HUE)
) is None:
self._attr_hs_color = None
else:
self._attr_hs_color = (
convert_scale(
hue,
100,
360,
),
self.get_attribute_value(
Capability.COLOR_CONTROL, Attribute.SATURATION
),
)
async def async_set_color(self, hs_color):
"""Set the color of the device."""
@@ -217,6 +229,10 @@ class SmartThingsLight(SmartThingsEntity, LightEntity, RestoreEntity):
super()._update_handler(event)
@property
def is_on(self) -> bool:
def is_on(self) -> bool | None:
"""Return true if light is on."""
return self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "on"
if (
state := self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH)
) is None:
return None
return state == "on"

View File

@@ -29,5 +29,5 @@
"documentation": "https://www.home-assistant.io/integrations/smartthings",
"iot_class": "cloud_push",
"loggers": ["pysmartthings"],
"requirements": ["pysmartthings==2.5.0"]
"requirements": ["pysmartthings==2.7.4"]
}

View File

@@ -5,9 +5,9 @@ from __future__ import annotations
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from datetime import datetime
from typing import Any
from typing import Any, cast
from pysmartthings import Attribute, Capability, SmartThings
from pysmartthings import Attribute, Capability, SmartThings, Status
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -57,6 +57,7 @@ JOB_STATE_MAP = {
"freezeProtection": "freeze_protection",
"preDrain": "pre_drain",
"preWash": "pre_wash",
"prewash": "pre_wash",
"wrinklePrevent": "wrinkle_prevent",
"unknown": None,
}
@@ -130,7 +131,8 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription):
unique_id_separator: str = "."
capability_ignore_list: list[set[Capability]] | None = None
options_attribute: Attribute | None = None
except_if_state_none: bool = False
exists_fn: Callable[[Status], bool] | None = None
use_temperature_unit: bool = False
CAPABILITY_TO_SENSORS: dict[
@@ -561,6 +563,8 @@ CAPABILITY_TO_SENSORS: dict[
SmartThingsSensorEntityDescription(
key=Attribute.COMPLETION_TIME,
translation_key="completion_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
)
],
},
@@ -569,6 +573,10 @@ CAPABILITY_TO_SENSORS: dict[
SmartThingsSensorEntityDescription(
key=Attribute.OVEN_SETPOINT,
translation_key="oven_setpoint",
device_class=SensorDeviceClass.TEMPERATURE,
use_temperature_unit=True,
# Set the value to None if it is 0 F (-17 C)
value_fn=lambda value: None if value in {0, -17} else value,
)
]
},
@@ -581,7 +589,10 @@ CAPABILITY_TO_SENSORS: dict[
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["energy"] / 1000,
suggested_display_precision=2,
except_if_state_none=True,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "energy" in value
),
),
SmartThingsSensorEntityDescription(
key="power_meter",
@@ -591,7 +602,10 @@ CAPABILITY_TO_SENSORS: dict[
value_fn=lambda value: value["power"],
extra_state_attributes_fn=power_attributes,
suggested_display_precision=2,
except_if_state_none=True,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "power" in value
),
),
SmartThingsSensorEntityDescription(
key="deltaEnergy_meter",
@@ -601,7 +615,10 @@ CAPABILITY_TO_SENSORS: dict[
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["deltaEnergy"] / 1000,
suggested_display_precision=2,
except_if_state_none=True,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "deltaEnergy" in value
),
),
SmartThingsSensorEntityDescription(
key="powerEnergy_meter",
@@ -611,7 +628,10 @@ CAPABILITY_TO_SENSORS: dict[
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["powerEnergy"] / 1000,
suggested_display_precision=2,
except_if_state_none=True,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "powerEnergy" in value
),
),
SmartThingsSensorEntityDescription(
key="energySaved_meter",
@@ -621,7 +641,10 @@ CAPABILITY_TO_SENSORS: dict[
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["energySaved"] / 1000,
suggested_display_precision=2,
except_if_state_none=True,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "energySaved" in value
),
),
]
},
@@ -951,6 +974,7 @@ UNITS = {
"F": UnitOfTemperature.FAHRENHEIT,
"lux": LIGHT_LUX,
"mG": None,
"μg/m^3": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
}
@@ -976,8 +1000,8 @@ async def async_setup_entry(
)
)
and (
not description.except_if_state_none
or device.status[MAIN][capability][attribute].value is not None
not description.exists_fn
or description.exists_fn(device.status[MAIN][capability][attribute])
)
)
@@ -996,7 +1020,10 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity):
attribute: Attribute,
) -> None:
"""Init the class."""
super().__init__(client, device, {capability})
capabilities_to_subscribe = {capability}
if entity_description.use_temperature_unit:
capabilities_to_subscribe.add(Capability.TEMPERATURE_MEASUREMENT)
super().__init__(client, device, capabilities_to_subscribe)
self._attr_unique_id = f"{device.device.device_id}{entity_description.unique_id_separator}{entity_description.key}"
self._attribute = attribute
self.capability = capability
@@ -1011,7 +1038,12 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity):
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit this state is expressed in."""
unit = self._internal_state[self.capability][self._attribute].unit
if self.entity_description.use_temperature_unit:
unit = self._internal_state[Capability.TEMPERATURE_MEASUREMENT][
Attribute.TEMPERATURE
].unit
else:
unit = self._internal_state[self.capability][self._attribute].unit
return (
UNITS.get(unit, unit)
if unit
@@ -1031,8 +1063,11 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity):
def options(self) -> list[str] | None:
"""Return the options for this sensor."""
if self.entity_description.options_attribute:
options = self.get_attribute_value(
self.capability, self.entity_description.options_attribute
)
if (
options := self.get_attribute_value(
self.capability, self.entity_description.options_attribute
)
) is None:
return []
return [option.lower() for option in options]
return super().options

View File

@@ -7,5 +7,5 @@
"iot_class": "cloud_push",
"loggers": ["snoo"],
"quality_scale": "bronze",
"requirements": ["python-snoo==0.6.0"]
"requirements": ["python-snoo==0.6.4"]
}

View File

@@ -32,6 +32,7 @@ SONOS_TRACKS = "tracks"
SONOS_COMPOSER = "composers"
SONOS_RADIO = "radio"
SONOS_OTHER_ITEM = "other items"
SONOS_AUDIO_BOOK = "audio book"
SONOS_STATE_PLAYING = "PLAYING"
SONOS_STATE_TRANSITIONING = "TRANSITIONING"
@@ -67,6 +68,7 @@ SONOS_TO_MEDIA_CLASSES = {
"object.item": MediaClass.TRACK,
"object.item.audioItem.musicTrack": MediaClass.TRACK,
"object.item.audioItem.audioBroadcast": MediaClass.GENRE,
"object.item.audioItem.audioBook": MediaClass.TRACK,
}
SONOS_TO_MEDIA_TYPES = {
@@ -84,6 +86,7 @@ SONOS_TO_MEDIA_TYPES = {
"object.container.playlistContainer.sameArtist": MediaType.ARTIST,
"object.container.playlistContainer": MediaType.PLAYLIST,
"object.item.audioItem.musicTrack": MediaType.TRACK,
"object.item.audioItem.audioBook": MediaType.TRACK,
}
MEDIA_TYPES_TO_SONOS: dict[MediaType | str, str] = {
@@ -113,6 +116,7 @@ SONOS_TYPES_MAPPING = {
"object.item": SONOS_OTHER_ITEM,
"object.item.audioItem.musicTrack": SONOS_TRACKS,
"object.item.audioItem.audioBroadcast": SONOS_RADIO,
"object.item.audioItem.audioBook": SONOS_AUDIO_BOOK,
}
LIBRARY_TITLES_MAPPING = {

View File

@@ -105,7 +105,7 @@ class SonosFavorites(SonosHouseholdCoordinator):
@soco_error()
def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool:
"""Update cache of known favorites and return if cache has changed."""
new_favorites = soco.music_library.get_sonos_favorites()
new_favorites = soco.music_library.get_sonos_favorites(full_album_art_uri=True)
# Polled update_id values do not match event_id values
# Each speaker can return a different polled update_id

View File

@@ -165,6 +165,8 @@ async def async_browse_media(
favorites_folder_payload,
speaker.favorites,
media_content_id,
media,
get_browse_image_url,
)
payload = {
@@ -443,7 +445,10 @@ def favorites_payload(favorites: SonosFavorites) -> BrowseMedia:
def favorites_folder_payload(
favorites: SonosFavorites, media_content_id: str
favorites: SonosFavorites,
media_content_id: str,
media: SonosMedia,
get_browse_image_url: GetBrowseImageUrlType,
) -> BrowseMedia:
"""Create response payload to describe all items of a type of favorite.
@@ -463,7 +468,14 @@ def favorites_folder_payload(
media_content_type="favorite_item_id",
can_play=True,
can_expand=False,
thumbnail=getattr(favorite, "album_art_uri", None),
thumbnail=get_thumbnail_url_full(
media=media,
is_internal=True,
media_content_type="favorite_item_id",
media_content_id=favorite.item_id,
get_browse_image_url=get_browse_image_url,
item=favorite,
),
)
)

View File

@@ -43,6 +43,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
state_class=SensorStateClass.TOTAL,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
),
SensorEntityDescription(
key=STATUS_SENSOR_INFO_TOTAL_GENRES,

View File

@@ -20,8 +20,8 @@ class SuezWaterAggregatedAttributes:
this_month_consumption: dict[str, float]
previous_month_consumption: dict[str, float]
last_year_overall: dict[str, float]
this_year_overall: dict[str, float]
last_year_overall: int
this_year_overall: int
history: dict[str, float]
highest_monthly_consumption: float

View File

@@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["pysuez", "regex"],
"quality_scale": "bronze",
"requirements": ["pysuezV2==2.0.3"]
"requirements": ["pysuezV2==2.0.4"]
}

View File

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

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
"iot_class": "local_polling",
"loggers": ["synology_dsm"],
"requirements": ["py-synologydsm-api==2.7.0"],
"requirements": ["py-synologydsm-api==2.7.1"],
"ssdp": [
{
"manufacturer": "Synology",

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/tesla_fleet",
"iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"],
"requirements": ["tesla-fleet-api==0.9.12"]
"requirements": ["tesla-fleet-api==0.9.13"]
}

View File

@@ -466,6 +466,7 @@ async def async_setup_entry(
for energysite in entry.runtime_data.energysites
for description in ENERGY_LIVE_DESCRIPTIONS
if description.key in energysite.live_coordinator.data
or description.key == "percentage_charged"
),
( # Add energy site history
TeslaFleetEnergyHistorySensorEntity(energysite, description)

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/teslemetry",
"iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"],
"requirements": ["tesla-fleet-api==0.9.12", "teslemetry-stream==0.6.10"]
"requirements": ["tesla-fleet-api==0.9.13", "teslemetry-stream==0.6.12"]
}

View File

@@ -68,7 +68,7 @@ class TeslemetryVehicleSensorEntityDescription(SensorEntityDescription):
polling: bool = False
polling_value_fn: Callable[[StateType], StateType] = lambda x: x
polling_available_fn: Callable[[StateType], bool] = lambda x: x is not None
nullable: bool = False
streaming_key: Signal | None = None
streaming_value_fn: Callable[[str | int | float], StateType] = lambda x: x
streaming_firmware: str = "2024.26"
@@ -210,7 +210,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
TeslemetryVehicleSensorEntityDescription(
key="drive_state_shift_state",
polling=True,
polling_available_fn=lambda x: True,
nullable=True,
polling_value_fn=lambda x: SHIFT_STATES.get(str(x), "p"),
streaming_key=Signal.GEAR,
streaming_value_fn=lambda x: str(ShiftState.get(x, "P")).lower(),
@@ -622,10 +622,10 @@ class TeslemetryStreamSensorEntity(TeslemetryVehicleStreamEntity, RestoreSensor)
def _async_value_from_stream(self, value) -> None:
"""Update the value of the entity."""
if value is None:
self._attr_native_value = None
else:
if self.entity_description.nullable or value is not None:
self._attr_native_value = self.entity_description.streaming_value_fn(value)
else:
self._attr_native_value = None
class TeslemetryVehicleSensorEntity(TeslemetryVehicleEntity, SensorEntity):
@@ -644,7 +644,7 @@ class TeslemetryVehicleSensorEntity(TeslemetryVehicleEntity, SensorEntity):
def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor."""
if self.entity_description.polling_available_fn(self._value):
if self.entity_description.nullable or self._value is not None:
self._attr_available = True
self._attr_native_value = self.entity_description.polling_value_fn(
self._value

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/tessie",
"iot_class": "cloud_polling",
"loggers": ["tessie", "tesla-fleet-api"],
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==0.9.12"]
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==0.9.13"]
}

View File

@@ -148,7 +148,7 @@ DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = (
key="drive_state_shift_state",
options=["p", "d", "r", "n"],
device_class=SensorDeviceClass.ENUM,
value_fn=lambda x: x.lower() if isinstance(x, str) else x,
value_fn=lambda x: x.lower() if isinstance(x, str) else "p",
),
TessieSensorEntityDescription(
key="vehicle_state_odometer",
@@ -397,6 +397,7 @@ async def async_setup_entry(
for energysite in entry.runtime_data.energysites
for description in ENERGY_LIVE_DESCRIPTIONS
if description.key in energysite.live_coordinator.data
or description.key == "percentage_charged"
),
( # Add wall connectors
TessieWallConnectorSensorEntity(energysite, din, description)
@@ -449,7 +450,6 @@ class TessieEnergyLiveSensorEntity(TessieEnergyEntity, SensorEntity):
def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor."""
self._attr_available = self._value is not None
self._attr_native_value = self.entity_description.value_fn(self._value)

View File

@@ -54,5 +54,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/thermobeacon",
"iot_class": "local_push",
"requirements": ["thermobeacon-ble==0.8.0"]
"requirements": ["thermobeacon-ble==0.8.1"]
}

View File

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

View File

@@ -63,7 +63,7 @@ class VelbusConfigFlow(ConfigFlow, domain=DOMAIN):
self._device = "tls://"
else:
self._device = ""
if user_input[CONF_PASSWORD] != "":
if CONF_PASSWORD in user_input and user_input[CONF_PASSWORD] != "":
self._device += f"{user_input[CONF_PASSWORD]}@"
self._device += f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}"
self._async_abort_entries_match({CONF_PORT: self._device})

View File

@@ -14,7 +14,7 @@
"velbus-protocol"
],
"quality_scale": "bronze",
"requirements": ["velbus-aio==2025.1.1"],
"requirements": ["velbus-aio==2025.3.1"],
"usb": [
{
"vid": "10CF",

View File

@@ -71,9 +71,7 @@ NUMBER_TYPES: dict[str, WallboxNumberEntityDescription] = {
CHARGER_MAX_ICP_CURRENT_KEY: WallboxNumberEntityDescription(
key=CHARGER_MAX_ICP_CURRENT_KEY,
translation_key="maximum_icp_current",
max_value_fn=lambda coordinator: cast(
float, coordinator.data[CHARGER_MAX_AVAILABLE_POWER_KEY]
),
max_value_fn=lambda _: 255,
min_value_fn=lambda _: 6,
set_value_fn=lambda coordinator: coordinator.async_set_icp_current,
native_step=1,

View File

@@ -13,7 +13,11 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from .const import CONF_BACKUP_PATH, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
from .helpers import async_create_client, async_ensure_path_exists
from .helpers import (
async_create_client,
async_ensure_path_exists,
async_migrate_wrong_folder_path,
)
type WebDavConfigEntry = ConfigEntry[Client]
@@ -46,10 +50,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: WebDavConfigEntry) -> bo
translation_key="cannot_connect",
)
path = entry.data.get(CONF_BACKUP_PATH, "/")
await async_migrate_wrong_folder_path(client, path)
# Ensure the backup directory exists
if not await async_ensure_path_exists(
client, entry.data.get(CONF_BACKUP_PATH, "/")
):
if not await async_ensure_path_exists(client, path):
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_access_or_create_backup_path",

View File

@@ -171,6 +171,7 @@ class WebDavBackupAgent(BackupAgent):
await open_stream(),
f"{self._backup_path}/{filename_tar}",
timeout=BACKUP_TIMEOUT,
content_length=backup.size,
)
_LOGGER.debug(

View File

@@ -1,10 +1,18 @@
"""Helper functions for the WebDAV component."""
import logging
from aiowebdav2.client import Client, ClientOptions
from aiowebdav2.exceptions import WebDavError
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@callback
def async_create_client(
@@ -36,3 +44,25 @@ async def async_ensure_path_exists(client: Client, path: str) -> bool:
return False
return True
async def async_migrate_wrong_folder_path(client: Client, path: str) -> None:
"""Migrate the wrong encoded folder path to the correct one."""
wrong_path = path.replace(" ", "%20")
# migrate folder when the old folder exists
if wrong_path != path and await client.check(wrong_path):
try:
await client.move(wrong_path, path)
except WebDavError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="failed_to_migrate_folder",
translation_placeholders={
"wrong_path": wrong_path,
"correct_path": path,
},
) from err
_LOGGER.debug(
"Migrated wrong encoded folder path from %s to %s", wrong_path, path
)

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aiowebdav2"],
"quality_scale": "bronze",
"requirements": ["aiowebdav2==0.3.1"]
"requirements": ["aiowebdav2==0.4.2"]
}

View File

@@ -36,6 +36,9 @@
},
"cannot_access_or_create_backup_path": {
"message": "Cannot access or create backup path. Please check the path and permissions."
},
"failed_to_migrate_folder": {
"message": "Failed to migrate wrong encoded folder \"{wrong_path}\" to \"{correct_path}\"."
}
}
}

View File

@@ -21,7 +21,7 @@
"zha",
"universal_silabs_flasher"
],
"requirements": ["zha==0.0.51"],
"requirements": ["zha==0.0.53"],
"usb": [
{
"vid": "10C4",

View File

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

View File

@@ -37,11 +37,11 @@ habluetooth==3.24.1
hass-nabucasa==0.92.0
hassil==2.2.3
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20250305.0
home-assistant-frontend==20250306.0
home-assistant-intents==2025.3.5
httpx==0.28.1
ifaddr==0.2.0
Jinja2==3.1.5
Jinja2==3.1.6
lru-dict==1.3.0
mutagen==1.47.0
orjson==3.10.12

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2025.3.0"
version = "2025.3.4"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
@@ -52,7 +52,7 @@ dependencies = [
"httpx==0.28.1",
"home-assistant-bluetooth==1.13.1",
"ifaddr==0.2.0",
"Jinja2==3.1.5",
"Jinja2==3.1.6",
"lru-dict==1.3.0",
"PyJWT==2.10.1",
# PyJWT has loose dependency. We want the latest one.

2
requirements.txt generated
View File

@@ -25,7 +25,7 @@ hass-nabucasa==0.92.0
httpx==0.28.1
home-assistant-bluetooth==1.13.1
ifaddr==0.2.0
Jinja2==3.1.5
Jinja2==3.1.6
lru-dict==1.3.0
PyJWT==2.10.1
cryptography==44.0.1

48
requirements_all.txt generated
View File

@@ -84,7 +84,7 @@ PyQRCode==1.2.1
PyRMVtransport==0.3.3
# homeassistant.components.switchbot
PySwitchbot==0.56.1
PySwitchbot==0.57.1
# homeassistant.components.switchmate
PySwitchmate==0.5.1
@@ -264,7 +264,7 @@ aioharmony==0.4.1
aiohasupervisor==0.3.0
# homeassistant.components.home_connect
aiohomeconnect==0.16.2
aiohomeconnect==0.16.3
# homeassistant.components.homekit_controller
aiohomekit==3.2.8
@@ -422,7 +422,7 @@ aiowaqi==3.1.0
aiowatttime==0.1.1
# homeassistant.components.webdav
aiowebdav2==0.3.1
aiowebdav2==0.4.2
# homeassistant.components.webostv
aiowebostv==0.7.3
@@ -557,7 +557,7 @@ av==13.1.0
axis==64
# homeassistant.components.fujitsu_fglair
ayla-iot-unofficial==1.4.5
ayla-iot-unofficial==1.4.7
# homeassistant.components.azure_event_hub
azure-eventhub==5.11.1
@@ -899,7 +899,7 @@ eufylife-ble-client==0.1.8
# evdev==1.6.1
# homeassistant.components.evohome
evohome-async==1.0.2
evohome-async==1.0.4
# homeassistant.components.bryant_evolution
evolutionhttp==0.0.18
@@ -1058,7 +1058,7 @@ goslide-api==0.7.0
gotailwind==0.3.0
# homeassistant.components.govee_ble
govee-ble==0.43.0
govee-ble==0.43.1
# homeassistant.components.govee_light_local
govee-local-api==2.0.1
@@ -1152,7 +1152,7 @@ hole==0.8.0
holidays==0.68
# homeassistant.components.frontend
home-assistant-frontend==20250305.0
home-assistant-frontend==20250306.0
# homeassistant.components.conversation
home-assistant-intents==2025.3.5
@@ -1483,7 +1483,7 @@ nettigo-air-monitor==4.0.0
neurio==0.3.1
# homeassistant.components.nexia
nexia==2.2.1
nexia==2.2.2
# homeassistant.components.nextcloud
nextcloudmonitor==1.5.1
@@ -1755,7 +1755,7 @@ py-schluter==0.1.7
py-sucks==0.9.10
# homeassistant.components.synology_dsm
py-synologydsm-api==2.7.0
py-synologydsm-api==2.7.1
# homeassistant.components.atome
pyAtome==0.1.1
@@ -1906,7 +1906,7 @@ pydiscovergy==3.0.2
pydoods==1.0.2
# homeassistant.components.hydrawise
pydrawise==2025.2.0
pydrawise==2025.3.0
# homeassistant.components.android_ip_webcam
pydroid-ipcam==2.0.0
@@ -1996,7 +1996,7 @@ pygti==0.9.4
pyhaversion==22.8.0
# homeassistant.components.heos
pyheos==1.0.2
pyheos==1.0.3
# homeassistant.components.hive
pyhive-integration==1.0.2
@@ -2077,7 +2077,7 @@ pykwb==0.0.8
pylacrosse==0.4
# homeassistant.components.lamarzocco
pylamarzocco==1.4.7
pylamarzocco==1.4.9
# homeassistant.components.lastfm
pylast==5.1.0
@@ -2310,7 +2310,7 @@ pysma==0.7.5
pysmappee==0.2.29
# homeassistant.components.smartthings
pysmartthings==2.5.0
pysmartthings==2.7.4
# homeassistant.components.smarty
pysmarty2==0.10.2
@@ -2346,7 +2346,7 @@ pysqueezebox==0.12.0
pystiebeleltron==0.0.1.dev2
# homeassistant.components.suez_water
pysuezV2==2.0.3
pysuezV2==2.0.4
# homeassistant.components.switchbee
pyswitchbee==1.8.3
@@ -2461,13 +2461,13 @@ python-rabbitair==0.0.8
python-ripple-api==0.0.3
# homeassistant.components.roborock
python-roborock==2.11.1
python-roborock==2.12.2
# homeassistant.components.smarttub
python-smarttub==0.0.39
# homeassistant.components.snoo
python-snoo==0.6.0
python-snoo==0.6.4
# homeassistant.components.songpal
python-songpal==0.16.2
@@ -2694,7 +2694,7 @@ sendgrid==6.8.2
# homeassistant.components.emulated_kasa
# homeassistant.components.sense
sense-energy==0.13.6
sense-energy==0.13.7
# homeassistant.components.sensirion_ble
sensirion-ble==0.1.1
@@ -2872,7 +2872,7 @@ temperusb==1.6.1
# homeassistant.components.tesla_fleet
# homeassistant.components.teslemetry
# homeassistant.components.tessie
tesla-fleet-api==0.9.12
tesla-fleet-api==0.9.13
# homeassistant.components.powerwall
tesla-powerwall==0.5.2
@@ -2881,7 +2881,7 @@ tesla-powerwall==0.5.2
tesla-wall-connector==1.0.2
# homeassistant.components.teslemetry
teslemetry-stream==0.6.10
teslemetry-stream==0.6.12
# homeassistant.components.tessie
tessie-api==0.1.1
@@ -2890,7 +2890,7 @@ tessie-api==0.1.1
# tf-models-official==2.5.0
# homeassistant.components.thermobeacon
thermobeacon-ble==0.8.0
thermobeacon-ble==0.8.1
# homeassistant.components.thermopro
thermopro-ble==0.11.0
@@ -2977,7 +2977,7 @@ unifiled==0.11
universal-silabs-flasher==0.0.29
# homeassistant.components.upb
upb-lib==0.6.0
upb-lib==0.6.1
# homeassistant.components.upcloud
upcloud-api==2.6.0
@@ -3000,7 +3000,7 @@ vallox-websocket-api==5.3.0
vehicle==2.2.2
# homeassistant.components.velbus
velbus-aio==2025.1.1
velbus-aio==2025.3.1
# homeassistant.components.venstar
venstarcolortouch==0.19
@@ -3091,7 +3091,7 @@ xiaomi-ble==0.33.0
xknx==3.6.0
# homeassistant.components.knx
xknxproject==3.8.1
xknxproject==3.8.2
# homeassistant.components.fritz
# homeassistant.components.rest
@@ -3149,7 +3149,7 @@ zeroconf==0.145.1
zeversolar==0.3.2
# homeassistant.components.zha
zha==0.0.51
zha==0.0.53
# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13

View File

@@ -81,7 +81,7 @@ PyQRCode==1.2.1
PyRMVtransport==0.3.3
# homeassistant.components.switchbot
PySwitchbot==0.56.1
PySwitchbot==0.57.1
# homeassistant.components.syncthru
PySyncThru==0.8.0
@@ -249,7 +249,7 @@ aioharmony==0.4.1
aiohasupervisor==0.3.0
# homeassistant.components.home_connect
aiohomeconnect==0.16.2
aiohomeconnect==0.16.3
# homeassistant.components.homekit_controller
aiohomekit==3.2.8
@@ -404,7 +404,7 @@ aiowaqi==3.1.0
aiowatttime==0.1.1
# homeassistant.components.webdav
aiowebdav2==0.3.1
aiowebdav2==0.4.2
# homeassistant.components.webostv
aiowebostv==0.7.3
@@ -506,7 +506,7 @@ av==13.1.0
axis==64
# homeassistant.components.fujitsu_fglair
ayla-iot-unofficial==1.4.5
ayla-iot-unofficial==1.4.7
# homeassistant.components.azure_event_hub
azure-eventhub==5.11.1
@@ -765,7 +765,7 @@ eternalegypt==0.0.16
eufylife-ble-client==0.1.8
# homeassistant.components.evohome
evohome-async==1.0.2
evohome-async==1.0.4
# homeassistant.components.bryant_evolution
evolutionhttp==0.0.18
@@ -908,7 +908,7 @@ goslide-api==0.7.0
gotailwind==0.3.0
# homeassistant.components.govee_ble
govee-ble==0.43.0
govee-ble==0.43.1
# homeassistant.components.govee_light_local
govee-local-api==2.0.1
@@ -981,7 +981,7 @@ hole==0.8.0
holidays==0.68
# homeassistant.components.frontend
home-assistant-frontend==20250305.0
home-assistant-frontend==20250306.0
# homeassistant.components.conversation
home-assistant-intents==2025.3.5
@@ -1246,7 +1246,7 @@ netmap==0.7.0.2
nettigo-air-monitor==4.0.0
# homeassistant.components.nexia
nexia==2.2.1
nexia==2.2.2
# homeassistant.components.nextcloud
nextcloudmonitor==1.5.1
@@ -1453,7 +1453,7 @@ py-nightscout==1.2.2
py-sucks==0.9.10
# homeassistant.components.synology_dsm
py-synologydsm-api==2.7.0
py-synologydsm-api==2.7.1
# homeassistant.components.hdmi_cec
pyCEC==0.5.2
@@ -1556,7 +1556,7 @@ pydexcom==0.2.3
pydiscovergy==3.0.2
# homeassistant.components.hydrawise
pydrawise==2025.2.0
pydrawise==2025.3.0
# homeassistant.components.android_ip_webcam
pydroid-ipcam==2.0.0
@@ -1625,7 +1625,7 @@ pygti==0.9.4
pyhaversion==22.8.0
# homeassistant.components.heos
pyheos==1.0.2
pyheos==1.0.3
# homeassistant.components.hive
pyhive-integration==1.0.2
@@ -1691,7 +1691,7 @@ pykrakenapi==0.1.8
pykulersky==0.5.2
# homeassistant.components.lamarzocco
pylamarzocco==1.4.7
pylamarzocco==1.4.9
# homeassistant.components.lastfm
pylast==5.1.0
@@ -1882,7 +1882,7 @@ pysma==0.7.5
pysmappee==0.2.29
# homeassistant.components.smartthings
pysmartthings==2.5.0
pysmartthings==2.7.4
# homeassistant.components.smarty
pysmarty2==0.10.2
@@ -1915,7 +1915,7 @@ pyspeex-noise==1.0.2
pysqueezebox==0.12.0
# homeassistant.components.suez_water
pysuezV2==2.0.3
pysuezV2==2.0.4
# homeassistant.components.switchbee
pyswitchbee==1.8.3
@@ -1994,13 +1994,13 @@ python-picnic-api2==1.2.2
python-rabbitair==0.0.8
# homeassistant.components.roborock
python-roborock==2.11.1
python-roborock==2.12.2
# homeassistant.components.smarttub
python-smarttub==0.0.39
# homeassistant.components.snoo
python-snoo==0.6.0
python-snoo==0.6.4
# homeassistant.components.songpal
python-songpal==0.16.2
@@ -2173,7 +2173,7 @@ securetar==2025.2.1
# homeassistant.components.emulated_kasa
# homeassistant.components.sense
sense-energy==0.13.6
sense-energy==0.13.7
# homeassistant.components.sensirion_ble
sensirion-ble==0.1.1
@@ -2312,7 +2312,7 @@ temperusb==1.6.1
# homeassistant.components.tesla_fleet
# homeassistant.components.teslemetry
# homeassistant.components.tessie
tesla-fleet-api==0.9.12
tesla-fleet-api==0.9.13
# homeassistant.components.powerwall
tesla-powerwall==0.5.2
@@ -2321,13 +2321,13 @@ tesla-powerwall==0.5.2
tesla-wall-connector==1.0.2
# homeassistant.components.teslemetry
teslemetry-stream==0.6.10
teslemetry-stream==0.6.12
# homeassistant.components.tessie
tessie-api==0.1.1
# homeassistant.components.thermobeacon
thermobeacon-ble==0.8.0
thermobeacon-ble==0.8.1
# homeassistant.components.thermopro
thermopro-ble==0.11.0
@@ -2393,7 +2393,7 @@ ultraheat-api==0.5.7
unifi-discovery==1.2.0
# homeassistant.components.upb
upb-lib==0.6.0
upb-lib==0.6.1
# homeassistant.components.upcloud
upcloud-api==2.6.0
@@ -2416,7 +2416,7 @@ vallox-websocket-api==5.3.0
vehicle==2.2.2
# homeassistant.components.velbus
velbus-aio==2025.1.1
velbus-aio==2025.3.1
# homeassistant.components.venstar
venstarcolortouch==0.19
@@ -2492,7 +2492,7 @@ xiaomi-ble==0.33.0
xknx==3.6.0
# homeassistant.components.knx
xknxproject==3.8.1
xknxproject==3.8.2
# homeassistant.components.fritz
# homeassistant.components.rest
@@ -2538,7 +2538,7 @@ zeroconf==0.145.1
zeversolar==0.3.2
# homeassistant.components.zha
zha==0.0.51
zha==0.0.53
# homeassistant.components.zwave_js
zwave-js-server-python==0.60.1

View File

@@ -47,7 +47,8 @@ from homeassistant.components.backup.manager import (
WrittenBackup,
)
from homeassistant.components.backup.util import password_to_key
from homeassistant.core import HomeAssistant
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import issue_registry as ir
@@ -3469,3 +3470,66 @@ async def test_restore_progress_after_restart_fail_to_remove(
"Unexpected error deleting backup restore result file: <class 'OSError'> Boom!"
in caplog.text
)
async def test_manager_blocked_until_home_assistant_started(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test backup manager's state is blocked until Home Assistant has started."""
hass.set_state(CoreState.not_running)
await setup_backup_integration(hass)
manager = hass.data[DATA_MANAGER]
assert manager.state == BackupManagerState.BLOCKED
assert manager.last_non_idle_event is None
# Fired when Home Assistant changes to starting state
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
await hass.async_block_till_done()
assert manager.state == BackupManagerState.BLOCKED
assert manager.last_non_idle_event is None
# Fired when Home Assistant changes to running state
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert manager.state == BackupManagerState.IDLE
assert manager.last_non_idle_event is None
async def test_manager_not_blocked_after_restore(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test restore backup progress after restart."""
restore_result = {"error": None, "error_type": None, "success": True}
hass.set_state(CoreState.not_running)
with patch(
"pathlib.Path.read_bytes", return_value=json.dumps(restore_result).encode()
):
await setup_backup_integration(hass)
ws_client = await hass_ws_client(hass)
await ws_client.send_json_auto_id({"type": "backup/info"})
result = await ws_client.receive_json()
assert result["success"] is True
assert result["result"] == {
"agent_errors": {},
"backups": [],
"last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None,
"last_non_idle_event": {
"manager_state": "restore_backup",
"reason": None,
"stage": None,
"state": "completed",
},
"next_automatic_backup": None,
"next_automatic_backup_additional": False,
"state": "idle",
}

View File

@@ -1,5 +1,6 @@
"""Test the Bluesound config flow."""
from ipaddress import IPv4Address, IPv6Address
from unittest.mock import AsyncMock
from pyblu.errors import PlayerUnreachableError
@@ -121,8 +122,8 @@ async def test_zeroconf_flow_success(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data=ZeroconfServiceInfo(
ip_address="1.1.1.1",
ip_addresses=["1.1.1.1"],
ip_address=IPv4Address("1.1.1.1"),
ip_addresses=[IPv4Address("1.1.1.1")],
port=11000,
hostname="player-name1111",
type="_musc._tcp.local.",
@@ -160,8 +161,8 @@ async def test_zeroconf_flow_cannot_connect(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data=ZeroconfServiceInfo(
ip_address="1.1.1.1",
ip_addresses=["1.1.1.1"],
ip_address=IPv4Address("1.1.1.1"),
ip_addresses=[IPv4Address("1.1.1.1")],
port=11000,
hostname="player-name1111",
type="_musc._tcp.local.",
@@ -187,8 +188,8 @@ async def test_zeroconf_flow_already_configured(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data=ZeroconfServiceInfo(
ip_address="1.1.1.2",
ip_addresses=["1.1.1.2"],
ip_address=IPv4Address("1.1.1.2"),
ip_addresses=[IPv4Address("1.1.1.2")],
port=11000,
hostname="player-name1112",
type="_musc._tcp.local.",
@@ -203,3 +204,23 @@ async def test_zeroconf_flow_already_configured(
assert config_entry.data[CONF_HOST] == "1.1.1.2"
player_mocks.player_data_for_already_configured.player.sync_status.assert_called_once()
async def test_zeroconf_flow_no_ipv4_address(hass: HomeAssistant) -> None:
"""Test abort flow when no ipv4 address is found in zeroconf data."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data=ZeroconfServiceInfo(
ip_address=IPv6Address("2001:db8::1"),
ip_addresses=[IPv6Address("2001:db8::1")],
port=11000,
hostname="player-name1112",
type="_musc._tcp.local.",
name="player-name._musc._tcp.local.",
properties={},
),
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "no_ipv4_address"

View File

@@ -48,18 +48,18 @@ def location_status_fixture(install: str, loc_id: str | None = None) -> JsonObje
return load_json_object_fixture(f"{install}/status_{loc_id}.json", DOMAIN)
def dhw_schedule_fixture(install: str) -> JsonObjectType:
def dhw_schedule_fixture(install: str, dhw_id: str | None = None) -> JsonObjectType:
"""Load JSON for the schedule of a domesticHotWater zone."""
try:
return load_json_object_fixture(f"{install}/schedule_dhw.json", DOMAIN)
return load_json_object_fixture(f"{install}/schedule_{dhw_id}.json", DOMAIN)
except FileNotFoundError:
return load_json_object_fixture("default/schedule_dhw.json", DOMAIN)
def zone_schedule_fixture(install: str) -> JsonObjectType:
def zone_schedule_fixture(install: str, zon_id: str | None = None) -> JsonObjectType:
"""Load JSON for the schedule of a temperatureZone zone."""
try:
return load_json_object_fixture(f"{install}/schedule_zone.json", DOMAIN)
return load_json_object_fixture(f"{install}/schedule_{zon_id}.json", DOMAIN)
except FileNotFoundError:
return load_json_object_fixture("default/schedule_zone.json", DOMAIN)
@@ -120,9 +120,9 @@ def mock_make_request(install: str) -> Callable:
elif "schedule" in url:
if url.startswith("domesticHotWater"): # /v2/domesticHotWater/{id}/schedule
return dhw_schedule_fixture(install)
return dhw_schedule_fixture(install, url[16:23])
if url.startswith("temperatureZone"): # /v2/temperatureZone/{id}/schedule
return zone_schedule_fixture(install)
return zone_schedule_fixture(install, url[16:23])
pytest.fail(f"Unexpected request: {HTTPMethod.GET} {url}")

View File

@@ -15,8 +15,9 @@ TEST_INSTALLS: Final = (
"default", # evohome: multi-zone, with DHW
"h032585", # VisionProWifi: no preset modes for TCS, zoneId=systemId
"h099625", # RoundThermostat
"h139906", # zone with null schedule
"sys_004", # RoundModulation
)
# "botched", # as default: but with activeFaults, ghost zones & unknown types
TEST_INSTALLS_WITH_DHW: Final = ("default",)
TEST_INSTALLS_WITH_DHW: Final = ("default", "botched")

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