Compare commits

...

224 Commits

Author SHA1 Message Date
abmantis
be1ba31ff2 Set state() template round param default to True 2025-07-03 17:15:54 +01:00
J. Nick Koston
3c4ecffa1b Bump aioesphomeapi to 34.1.0 (#148048) 2025-07-03 17:33:44 +02:00
Joakim Sørensen
244e0f5ea8 Bump hass-nabucasa from 0.104.0 to 0.105.0 (#148040) 2025-07-03 14:24:51 +02:00
epenet
a656b6e26a Use HassKey in media_source (#148011) 2025-07-03 09:56:46 +02:00
epenet
691681a78a Move medcom_ble coordinator to separate module (#148009) 2025-07-03 09:32:57 +02:00
epenet
3bc00824e2 Use runtime_data in mystrom (#148020) 2025-07-03 09:27:38 +02:00
epenet
7d36a2e3a7 Move meteoclimatic coordinator to separate module (#148018) 2025-07-03 09:26:24 +02:00
Norbert Rittel
b1e3561ead Clarify description of autorelock setting in zwave_js (#148019) 2025-07-03 09:23:45 +02:00
epenet
bfc814c839 Use entry.async_on_unload in meteo_france (#148015) 2025-07-03 09:22:27 +02:00
epenet
5008151688 Use entry.async_on_unload in monoprice (#148016) 2025-07-03 09:20:50 +02:00
Franck Nijhof
d738c0d6b1 Merge branch 'master' into dev 2025-07-03 07:04:46 +00:00
epenet
e42235285d Use runtime_data in melcloud (#148012)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-03 08:57:22 +02:00
epenet
04e69479f4 Fix hass.data reference in lookin (#148008) 2025-07-03 08:54:20 +02:00
epenet
b973916032 Move met_eireann coordinator to separate module (#148014) 2025-07-03 08:53:22 +02:00
epenet
6f4757ef42 Use runtime_data in melnor (#148013) 2025-07-03 08:52:40 +02:00
epenet
a6962e9e1e Fix missing port in samsungtv (#147962)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-07-03 08:51:38 +02:00
Marcel van der Veldt
142c10cccc Fix state being incorrectly reported in some situations on Music Assistant players (#147997) 2025-07-03 08:50:41 +02:00
Matthias Alphart
c137c96cfd KNX: use async_load_json_object_fixture in tests (#147991) 2025-07-03 08:00:34 +02:00
Robert Svensson
f0e0c954e7 Bump aiounifi to v84 (#147987) 2025-07-02 23:10:21 +02:00
Norbert Rittel
681961d3a5 Use common config_flow strings in vegehub (#147984) 2025-07-02 22:14:55 +02:00
Matthias Alphart
53d2f6b0c6 KNX: Use a ConfigExtractor helper class for value retrieval (#147983) 2025-07-02 21:49:24 +02:00
G Johansson
78c39f8a06 Remove deprecated battery properties from demo vacuum (#147980) 2025-07-02 21:49:12 +02:00
Ludovic BOUÉ
a748525e03 Allow LevelControl Cluster for Matter Pump devices (#145004)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-07-02 21:48:15 +02:00
Manuel Rüger
8ca1fe83b7 Bump switchbot-api to v2.7.0 (#147978) 2025-07-02 21:36:06 +02:00
Matthias Alphart
8968cf704b Use send_json_auto_id in KNX tests (#147982) 2025-07-02 21:34:30 +02:00
puddly
ebe04466f4 Bump ZHA to 0.0.62 (#147966) 2025-07-02 21:19:32 +02:00
G Johansson
e31470ba5b Change breaking version for battery props in vacuum (#147956) 2025-07-02 19:06:56 +02:00
Franck Nijhof
4bc2951f44 2025.7.0 (#147533) 2025-07-02 18:01:06 +02:00
Franck Nijhof
8334a0398c Bump version to 2025.7.0 2025-07-02 15:12:16 +00:00
Ville Skyttä
80a1e0e4cd Improve huawei_lte config flow class naming (#147910) 2025-07-02 17:02:39 +02:00
Thomas55555
3778f537d5 Remove noisy debug logs in Husgvarna Automower (#147958) 2025-07-02 15:28:42 +01:00
Petro31
adec157d43 Allow trigger based numeric sensors to be set to unknown (#137047)
* Allow trigger based numeric sensors to be set to unknown

* resolve comments

* Do case insensitive check

* use _parse_result

---------

Co-authored-by: abmantis <amfcalt@gmail.com>
2025-07-02 15:35:47 +02:00
Franck Nijhof
8fc3fa51a8 Bump version to 2025.7.0b9 2025-07-02 13:30:51 +00:00
c0ffeeca7
4eb688b560 Z-Wave JS: rename controller to adapter according to term decision (#147955)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-02 13:30:31 +00:00
Simone Chemelli
9472ff5d36 Bump aioamazondevices to 3.2.2 (#147953) 2025-07-02 13:30:29 +00:00
Bram Kragten
12e8b81ec7 Update frontend to 20250702.0 (#147952) 2025-07-02 13:30:28 +00:00
Paulus Schoutsen
ec5e543c09 Ollama: Migrate pick model to subentry (#147944) 2025-07-02 13:30:27 +00:00
Paulus Schoutsen
116c745872 Split Ollama entity (#147769) 2025-07-02 13:30:26 +00:00
Robert Resch
1fdf152292 Bump deebot-client to 13.5.0 (#147938) 2025-07-02 13:27:47 +00:00
G Johansson
b816f1a408 Handle additional errors in Nord Pool (#147937) 2025-07-02 13:27:46 +00:00
John Hess
eb351e6505 Bump thermopro-ble to 0.13.1 (#147924) 2025-07-02 13:27:45 +00:00
Maciej Bieniek
2f27d55495 Open repair issue when outbound WebSocket is enabled for Shelly non-sleeping RPC device (#147901) 2025-07-02 13:26:03 +00:00
Space
fa1bed1849 Skip processing request body for HTTP HEAD requests (#147899)
* Skip processing request body for HTTP HEAD requests

* Use aiohttp's must_be_empty_body() to check whether ingress requests should be streamed

* Only call must_be_empty_body() once per request

* Fix incorrect use of walrus operator
2025-07-02 13:26:01 +00:00
Raphael Hehl
b8c19f23f3 UnifiProtect Change log level from debug to error for connection exceptions in ProtectFlowHandler (#147730) 2025-07-02 13:26:00 +00:00
Erwin Douna
b677ce6c90 SMA add DHCP strictness (#145753)
* Add DHCP strictness (needs beta check)

* Update to check on CONF_MAC

* Update to check on CONF_HOST

* Update hostname

* Polish it a bit

* Update to CONF_HOST, again

* Add split

* Add CONF_MAC add upon detection

* epenet feedback

* epenet round II
2025-07-02 13:25:59 +00:00
c0ffeeca7
d6da686ffe Z-Wave JS: rename controller to adapter according to term decision (#147955)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-02 15:23:08 +02:00
Paulus Schoutsen
f50ef79c72 Ollama: Migrate pick model to subentry (#147944) 2025-07-02 15:20:42 +02:00
Erik Montnemery
943fb9948b Adjust logic related to entity platform state (#147882)
* Adjust logic related to entity platform state

* Break up hard to read if-statement

* Add and improve tests
2025-07-02 14:57:53 +02:00
Raphael Hehl
7447cf329b UnifiProtect Change log level from debug to error for connection exceptions in ProtectFlowHandler (#147730) 2025-07-02 14:57:46 +02:00
Erwin Douna
3d27c0ce52 SMA add DHCP strictness (#145753)
* Add DHCP strictness (needs beta check)

* Update to check on CONF_MAC

* Update to check on CONF_HOST

* Update hostname

* Polish it a bit

* Update to CONF_HOST, again

* Add split

* Add CONF_MAC add upon detection

* epenet feedback

* epenet round II
2025-07-02 14:48:21 +02:00
Simone Chemelli
b7496be61f Bump aioamazondevices to 3.2.2 (#147953) 2025-07-02 14:27:51 +02:00
Bram Kragten
57a98240bd Update frontend to 20250702.0 (#147952) 2025-07-02 14:26:19 +02:00
Ville Skyttä
ff76017ba6 Simplify unnecessary re match.groups()[0] calls (#147909) 2025-07-02 14:12:26 +02:00
Maikel Punie
f10fcde6d8 Remove the deprecated interface paramater for velbus (#147868) 2025-07-02 14:07:47 +02:00
Marc Mueller
a7002e3a24 Update pytest to 8.4.1 (#147951) 2025-07-02 13:02:18 +01:00
tronikos
bbe03dcab7 Add missing Opower tests (#147934) 2025-07-02 13:46:40 +02:00
Andre Lengwenus
f77e6cc8fc Add missing exception translations to LCN (#147723) 2025-07-02 13:41:06 +02:00
Petro31
cb8e076703 Fix missing device_class and state_class on compensation entities (#146115)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-07-02 13:39:19 +02:00
G Johansson
73251fbb1c Handle additional errors in Nord Pool (#147937) 2025-07-02 13:26:47 +02:00
Maciej Bieniek
7ff90ca49d Open repair issue when outbound WebSocket is enabled for Shelly non-sleeping RPC device (#147901) 2025-07-02 13:06:27 +02:00
Manu
bab9ec9976 Add sensor for online status to PlayStation Network (#147842) 2025-07-02 11:47:41 +01:00
Marc Mueller
1051f85ac0 Update coverage to 7.9.1 (#147940) 2025-07-02 12:20:50 +02:00
Marc Mueller
6c7da57af2 Update pytest-cov to 6.2.1 (#147942) 2025-07-02 12:14:27 +02:00
Marc Mueller
73e505d48d Update pytest-xdist to 3.8.0 (#147943) 2025-07-02 12:11:09 +02:00
Marc Mueller
ec65066f5e Update mypy-dev to 1.17.0a4 (#147939) 2025-07-02 12:09:39 +02:00
Robert Resch
9c4951261c Bump deebot-client to 13.5.0 (#147938) 2025-07-02 12:00:48 +02:00
Space
00dfc04b86 Skip processing request body for HTTP HEAD requests (#147899)
* Skip processing request body for HTTP HEAD requests

* Use aiohttp's must_be_empty_body() to check whether ingress requests should be streamed

* Only call must_be_empty_body() once per request

* Fix incorrect use of walrus operator
2025-07-02 11:45:45 +02:00
Manu
bee07ad284 Fix Online ID string in PlayStation Network integration (#147915) 2025-07-02 10:45:07 +02:00
Paulus Schoutsen
b2108fdd40 Update Dockerfile.dev to only use uv for Python (#147926) 2025-07-02 10:40:16 +02:00
John Hess
3730a1a379 Bump thermopro-ble to 0.13.1 (#147924) 2025-07-02 10:11:49 +02:00
Sid
088c02d38a Complete tests for eheimdigital (#143337)
* Complete tests for eheimdigital

* Review

* Review

* Review

* Review

* Fix tests
2025-07-02 10:09:30 +02:00
Harry Heymann
afb247c907 Bump Python Matter server to 8.0.0 (#147783) 2025-07-02 08:12:47 +02:00
Franck Nijhof
0e6bbb30c1 Bump version to 2025.7.0b8 2025-07-02 06:04:14 +00:00
J. Nick Koston
fdba791f18 Bump bluetooth-data-tools to 1.28.2 (#147920) 2025-07-02 06:03:56 +00:00
Ivan Lopez Hernandez
d4dec6c7a9 Swap the Models label for the model name not it's display name, (#147918)
Swap display name for name.
2025-07-02 06:03:55 +00:00
Simone Chemelli
f838e85a79 Manager wrong country selection in Alexa Devices (#147914)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-07-02 06:03:54 +00:00
Simone Chemelli
04ae966544 Bump aioamazondevices to 3.2.1 (#147912) 2025-07-02 06:03:53 +00:00
Simone Chemelli
77dcba0984 Manager wrong country selection in Alexa Devices (#147914)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-07-02 08:02:53 +02:00
Simone Chemelli
48f9a12cca Bump aioamazondevices to 3.2.1 (#147912) 2025-07-02 07:36:41 +02:00
J. Nick Koston
bdd2ac9ae4 Bump bluetooth-data-tools to 1.28.2 (#147920) 2025-07-02 07:34:40 +02:00
Ivan Lopez Hernandez
2e7113d881 Swap the Models label for the model name not it's display name, (#147918)
Swap display name for name.
2025-07-01 21:12:58 -07:00
Sid
6842bfae4c Bump eheimdigital to 1.3.0 (#147908) 2025-07-01 23:00:25 +01:00
nadimz
392cde20d9 Add support for opening state in template lock (#147813)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-07-01 22:03:20 +01:00
cristianburrini
a6146fb5a9 Increase the number of irrigation zones up to 8 for Tuya enabled controllers. (#147793) 2025-07-01 22:40:36 +02:00
Franck Nijhof
b2c393db72 Bump version to 2025.7.0b7 2025-07-01 20:11:01 +00:00
Jesse Hills
6104731d53 Remove codeowner from ESPHome (#147850) 2025-07-01 22:09:23 +02:00
Marcel van der Veldt
3ed440a3af Bump Music Assistant Client to 1.2.3 (#147885) 2025-07-01 20:08:45 +00:00
Jamin
01e7efc7b4 Bump VoIP utils to 0.3.3 (#147880) 2025-07-01 20:08:44 +00:00
avee87
60a930554a Fix station name sensor for metoffice (#145500) 2025-07-01 20:08:43 +00:00
Erik Montnemery
66308a848a Set Entity._platform_state in google_assistant tests (#147892) 2025-07-01 21:46:36 +02:00
Erik Montnemery
c71dbd9d4d Set Entity._platform_state in universal tests (#147894) 2025-07-01 21:46:01 +02:00
Erik Montnemery
1195c2ec10 Set Entity._platform_state in core customize test (#147895) 2025-07-01 21:45:08 +02:00
Norbert Rittel
78a9cd9201 Use (new) common state "Empty" for water level in switchbot (#147836) 2025-07-01 21:43:21 +02:00
Erik Montnemery
639a749a0f Mock recorder in ista_ecotrend tests (#147893) 2025-07-01 20:09:48 +01:00
Simone Chemelli
058f3b8b6e Add reauth to Alexa Devices config flow (#147773) 2025-07-01 20:57:24 +02:00
Manu
926e9261ab Add switch to enable/disable boost in IronOS integration (#147831) 2025-07-01 20:53:13 +02:00
Erik Montnemery
d6fb860889 Use entity_registry_enabled_by_default fixture in dsmr_reader tests (#147891) 2025-07-01 20:50:38 +02:00
Marcel van der Veldt
5e03900e0a Bump Music Assistant Client to 1.2.3 (#147885) 2025-07-01 20:26:26 +02:00
Erik Montnemery
1e6e5ca1b6 Fix broadlink tests (#147890) 2025-07-01 18:32:58 +01:00
Erik Montnemery
60e3b38de1 Set Entity._platform_state in arcam_fmj tests (#147889) 2025-07-01 17:58:15 +02:00
epenet
852522219c Use correctly formatted MAC in bond tests (#147887) 2025-07-01 17:56:10 +02:00
epenet
23f1e8d1a3 Use correctly formatted MAC in elkm1 tests (#147888) 2025-07-01 17:55:46 +02:00
Franck Nijhof
c707bf6264 Bump version to 2025.7.0b6 2025-07-01 14:26:59 +00:00
avee87
655f009f07 Fix station name sensor for metoffice (#145500) 2025-07-01 16:18:13 +02:00
Paul Bottein
3548ab70fd Update frontend to 20250701.0 (#147879) 2025-07-01 14:10:30 +00:00
Erik Montnemery
e272ab1885 Initialize EsphomeEntity._has_state (#147877) 2025-07-01 14:10:29 +00:00
Erik Montnemery
d5d1b620d0 Correct openai conversation config entry migration (#147859) 2025-07-01 14:10:28 +00:00
Erik Montnemery
8b2f4f0f86 Correct ollama config entry migration (#147858) 2025-07-01 14:10:26 +00:00
Erik Montnemery
725269ecda Correct anthropic config entry migration (#147857) 2025-07-01 14:10:25 +00:00
Erik Montnemery
c42fc818bf Correct Google generative AI config entry migration (#147856) 2025-07-01 14:10:23 +00:00
Jesse Hills
5554e38171 Implement suggested_display_precision for ESPHome (#147849) 2025-07-01 14:10:22 +00:00
Jan Bouwhuis
b25acfe823 Fix invalid configuration of MQTT device QoS option in subentry flow (#147837) 2025-07-01 14:10:21 +00:00
micha91
ff25948e37 fix: Create new aiohttp session with DummyCookieJar (#147827) 2025-07-01 14:10:19 +00:00
Maciej Bieniek
f85fc7173f Bump Nettigo Air Monitor backend library to version 5.0.0 (#147812) 2025-07-01 14:10:18 +00:00
Bob Laz
748cc6386d fix state_class for water used today sensor (#147787) 2025-07-01 14:10:17 +00:00
Manu
47b232db49 Add more mac address prefixes for discovery to PlayStation Network (#147739) 2025-07-01 14:10:15 +00:00
hanwg
c61935fc41 Include chat ID in Telegram bot subentry title (#147643) 2025-07-01 14:10:14 +00:00
Jan-Philipp Benecke
414318f3fb Catch access denied errors in webdav and display proper message (#147093) 2025-07-01 14:10:12 +00:00
Paul Bottein
08985d783f Fix Meteo france Ciel clair condition mapping (#146965)
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2025-07-01 14:10:11 +00:00
Thomas55555
e4bcde7d20 Fix wrong state in Husqvarna Automower (#146075) 2025-07-01 14:10:10 +00:00
Jamin
59bf39f4ed Bump VoIP utils to 0.3.3 (#147880) 2025-07-01 16:09:51 +02:00
Fredrik Mårtensson
510e3977df Add water_level sensor to Tuya pet fountain cwysj (#146602)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-07-01 14:57:17 +01:00
micha91
922720576a fix: Create new aiohttp session with DummyCookieJar (#147827) 2025-07-01 15:50:04 +02:00
Paul Bottein
e10b581d4b Fix Meteo france Ciel clair condition mapping (#146965)
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2025-07-01 15:43:34 +02:00
hanwg
e38eac9415 Include chat ID in Telegram bot subentry title (#147643) 2025-07-01 15:42:32 +02:00
Maciej Bieniek
11c9aa9280 Bump Nettigo Air Monitor backend library to version 5.0.0 (#147812) 2025-07-01 15:39:29 +02:00
Paul Bottein
52c86f8a6a Update frontend to 20250701.0 (#147879) 2025-07-01 15:38:04 +02:00
Marc Mueller
6364a9ad98 Update pillow to 11.3.0 (#147869) 2025-07-01 14:31:06 +01:00
Manu
651162b8e7 Fix error in last online sensor of PlayStation integration (#147844)
* Fix Last online sensor

* set unavailable

* available_fn
2025-07-01 15:17:10 +02:00
Denis Shulyaka
7deca35172 Add multiple LLM API support for MCP Server (#147785)
* Add multiple LLM API support for MCP Server

* Update homeassistant/components/mcp_server/config_flow.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* ruff

* Update tests/components/mcp_server/conftest.py

Co-authored-by: Allen Porter <allen.porter@gmail.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Allen Porter <allen.porter@gmail.com>
2025-07-01 06:14:03 -07:00
epenet
073a467fb2 Use correctly formatted MAC in bond tests (#147870) 2025-07-01 14:41:31 +02:00
epenet
3f9590b03b Use correctly formatted MAC in gogogate2 tests (#147872) 2025-07-01 14:41:20 +02:00
epenet
b47f989c77 Use correctly formatted MAC in wmspro tests (#147876) 2025-07-01 14:40:41 +02:00
epenet
4ebffa8d23 Use correctly formatted MAC in palazzetti tests (#147875) 2025-07-01 14:40:27 +02:00
epenet
c5873c6dd0 Use correctly formatted MAC in dlink tests (#147871) 2025-07-01 14:40:12 +02:00
Erik Montnemery
2cb80e083e Initialize EsphomeEntity._has_state (#147877) 2025-07-01 07:33:33 -05:00
epenet
871296dff6 Use correctly formatted MAC in lamarzocco tests (#147874) 2025-07-01 14:13:21 +02:00
Franck Nijhof
db04c77e62 Bump version to 2025.7.0b5 2025-06-30 19:39:34 +00:00
puddly
e8204e5f8e Await firmware installation task when flashing ZBT-1/Yellow firmware (#147824) 2025-06-30 19:39:03 +00:00
starkillerOG
66cf9c4ed5 Bump reolink_aio to 0.14.2 (#147797) 2025-06-30 19:39:02 +00:00
mkmer
1f6d28dcbf Honeywell: Don't use shared session (#147772) 2025-06-30 19:39:02 +00:00
Paulus Schoutsen
328e838351 Use media selector for Assist Satellite actions (#147767)
Co-authored-by: Michael Hansen <mike@rhasspy.org>
2025-06-30 19:39:01 +00:00
cdnninja
62a1c8af11 Fix Vesync set_percentage error (#147751) 2025-06-30 19:39:00 +00:00
tronikos
b50e599517 Move the async_reload on updates in async_setup_entry in Google Generative AI (#147748)
Move the async_reload on updates in async_setup_entry
2025-06-30 19:38:59 +00:00
Manu
3c7c9176d2 Fix sensor displaying unknown when getting readings from heat meters in ista EcoTrend (#147741) 2025-06-30 19:37:54 +00:00
J. Nick Koston
c771f5fe1e Preserve httpx boolean behavior in REST integration after aiohttp conversion (#147738) 2025-06-30 19:35:31 +00:00
hanwg
6dc464ad73 Fix Telegram bot proxy URL not initialized when creating a new bot (#147707) 2025-06-30 19:35:30 +00:00
Marc Hörsken
ae48e3716e Update pywmspro to 0.3.0 to wait for short-lived actions (#147679)
Replace action delays with detailed action responses.
2025-06-30 19:35:29 +00:00
Hessel
1543726095 Wallbox Integration, Reduce API impact by limiting the amount of API calls made (#147618) 2025-06-30 19:35:27 +00:00
Evan Severson
adbace95c3 Fixed pushbullet handling of fields longer than 255 characters (#146993) 2025-06-30 19:35:26 +00:00
Shay Levy
578b43cf61 Bump aioshelly to 13.7.1 (#146221)
* Bump aioshelly to 13.8.0

* Change version to 13.7.1
2025-06-30 19:35:25 +00:00
mvn23
a8b5d1511d Populate hvac_modes list in opentherm_gw (#142074) 2025-06-30 19:35:24 +00:00
Pete Sage
5a0a1bbbf4 Person ble_trackers for non-home zones not processed correctly (#138475)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-06-30 19:35:23 +00:00
Paulus Schoutsen
cf2e69ed74 Bump version to 2025.7.0b4 2025-06-28 20:27:42 +00:00
J. Nick Koston
c32b44b774 Improve rest error logging (#147736)
* Improve rest error logging

* Improve rest error logging

* Improve rest error logging

* Improve rest error logging

* Improve rest error logging

* top level
2025-06-28 20:27:20 +00:00
Florian von Garrel
2f69ed4a8a bump pypaperless to 4.1.1 (#147735) 2025-06-28 20:27:19 +00:00
Marc Hörsken
4b3449fe0c Fix error if cover position is not available or unknown (#147732) 2025-06-28 20:27:18 +00:00
starkillerOG
33e1c6de68 Reduce idle timeout of HLS stream to conserve camera battery life (#147728)
* Reduce IDLE timeout of HLS stream to conserve camera battery life

* adjust tests
2025-06-28 20:27:17 +00:00
Daniel Hjelseth Høyer
81e712ea49 Bump pytibber to 0.31.6 (#147703) 2025-06-28 20:27:16 +00:00
Shay Levy
d3c5684cd0 Fix Shelly Block entity removal (#147694) 2025-06-28 20:27:16 +00:00
Jan Bouwhuis
862b7460b5 Move MQTT device sw and hw version to collapsed section in subentry flow (#147685)
Move MQTT device sw and hw version to collapsed section
2025-06-28 20:27:15 +00:00
Samuel Xiao
a65eb57539 Add lock models to switchbot cloud (#147569) 2025-06-28 20:27:14 +00:00
Antoni Czaplicki
b537850f52 Bump vulcan-api to 2.4.2 (#146857) 2025-06-28 20:27:13 +00:00
Franck Nijhof
16c6bd08f8 Bump version to 2025.7.0b3 2025-06-27 17:55:31 +00:00
Simone Chemelli
18834849c2 Bump aioamazondevices to 3.1.22 (#147681) 2025-06-27 17:54:40 +00:00
hanwg
e4d820799f Add codeowner for Telegram bot (#147680) 2025-06-27 17:54:38 +00:00
mkmer
013a35176a Bump aiosomecomfort to 0.0.33 (#147673) 2025-06-27 17:54:37 +00:00
Norbert Rittel
8230557aef Fix sentence-casing and spacing of button in thermopro (#147671) 2025-06-27 17:54:36 +00:00
Paul Bottein
5451063714 Update frontend to 20250627.0 (#147668) 2025-06-27 17:54:35 +00:00
Shay Levy
8cdc7523a4 Fix Shelly entity removal (#147665) 2025-06-27 17:54:33 +00:00
Josef Zweck
77ccfbd3a9 Fix: Unhandled NoneType sessions in jellyfin (#147659) 2025-06-27 17:54:32 +00:00
Josef Zweck
4977ee4998 Bump jellyfin-apiclient-python to 1.11.0 (#147658) 2025-06-27 17:54:31 +00:00
Josef Zweck
5c0f2d37f0 Make jellyfin not single config entry (#147656) 2025-06-27 17:54:29 +00:00
Thomas55555
0b5d2ab8e4 Respect availability of parent class in Husqvarna Automower (#147649) 2025-06-27 17:54:28 +00:00
Brett Adams
47f3bf29dd Fix energy history in Teslemetry (#147646) 2025-06-27 17:54:26 +00:00
Manu
62f7cbb51e Remove dweet.io integration (#147645) 2025-06-27 17:54:25 +00:00
Bernardus Jansen
b9e2c5d34c Add previously missing state classes to dsmr sensors (#147633) 2025-06-27 17:54:24 +00:00
Petar Petrov
1829acd0e1 Z-WaveJS config flow: Change keys question (#147518)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-06-27 17:54:22 +00:00
Franck Nijhof
41b9a7a9a3 Bump version to 2025.7.0b2 2025-06-27 08:08:02 +00:00
Norbert Rittel
9782637ec8 Clarify descriptions of subaru.unlock_specific_door action (#147655) 2025-06-27 08:05:06 +00:00
Manu
6bd6fa65d2 Bump pynecil to v4.1.1 (#147648) 2025-06-27 08:05:05 +00:00
Joost Lekkerkerker
85343a9f53 Make sure Ollama integration migration is clean (#147630) 2025-06-27 08:05:04 +00:00
Joost Lekkerkerker
bc607dd013 Make sure Anthropic integration migration is clean (#147629) 2025-06-27 08:05:02 +00:00
Joost Lekkerkerker
c2c388e0cc Make sure OpenAI integration migration is clean (#147627) 2025-06-27 08:05:01 +00:00
Joost Lekkerkerker
3fc154e1d7 Make sure Google Generative AI integration migration is clean (#147625) 2025-06-27 08:05:00 +00:00
Jack Powell
efb29d024e Add Diagnostics to PlayStation Network (#147607)
* Add Diagnostics support to PlayStation_Network

* Remove unused constant

* minor cleanup

* Redact additional data

* Redact additional data
2025-06-27 08:04:58 +00:00
Michael
263823c92c Fix config schema to make credentials optional in NUT flows (#147593) 2025-06-27 08:04:57 +00:00
hanwg
e5e6ed601b Fix Telegram bot yaml import for webhooks containing None value for URL (#147586) 2025-06-27 08:04:56 +00:00
Petar Petrov
28dfc997f3 Do not factory reset old Z-Wave controller during migration (#147576)
* Do not factory reset old Z-Wave controller during migration

* PR comments

* remove obsolete test
2025-06-27 08:04:55 +00:00
puddly
f93ab8d519 Allow setup of Zigbee/Thread for ZBT-1 and Yellow without internet access (#147549)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-06-27 08:04:54 +00:00
Josef Zweck
cb359da79e Make entities unavailable when machine is physically off in lamarzocco (#147426) 2025-06-27 08:04:52 +00:00
Franck Nijhof
6a7385590a Bump version to 2025.7.0b1 2025-06-26 18:03:11 +00:00
Joost Lekkerkerker
c0ec987b07 Fix meaters not being added after a reload (#147614) 2025-06-26 18:02:49 +00:00
Joost Lekkerkerker
26521f8cc0 Hide Telegram bot proxy URL behind section (#147613)
Co-authored-by: Manu <4445816+tr4nt0r@users.noreply.github.com>
2025-06-26 18:02:48 +00:00
Manu
4df1f702bf Fix asset url in Habitica integration (#147612) 2025-06-26 18:02:46 +00:00
Joost Lekkerkerker
c8422c9fb8 Improve explanation on how to get API token in Telegram (#147605) 2025-06-26 18:02:45 +00:00
Luca Angemi
f8207a2e0e Remove default icon for wind direction sensor for Buienradar (#147603)
* Fix wind direction state class sensor

* Remove default icon for wind direction sensor
2025-06-26 18:02:44 +00:00
Bram Kragten
9cc75f3458 Update frontend to 20250626.0 (#147601) 2025-06-26 18:02:43 +00:00
Joost Lekkerkerker
a233b6b1e3 Add default title to migrated Ollama entry (#147599) 2025-06-26 18:02:42 +00:00
Joost Lekkerkerker
c7677b91da Add default title to migrated Claude entry (#147598) 2025-06-26 18:02:40 +00:00
Joost Lekkerkerker
1f57bba9cd Add default conversation name for OpenAI integration (#147597) 2025-06-26 18:02:39 +00:00
Joost Lekkerkerker
4cc10ca2e2 Set Google AI model as device model (#147582)
* Set Google AI model as device model

* fix
2025-06-26 18:02:38 +00:00
Marcel van der Veldt
153e1e43e8 Do not make the favorite button unavailable when no content playing on a Music Assistant player (#147579) 2025-06-26 18:02:36 +00:00
Joost Lekkerkerker
398dd3ae46 Set right model in OpenAI conversation (#147575) 2025-06-26 18:02:35 +00:00
Petar Petrov
17fd850fa6 Hide unnamed paths when selecting a USB Z-Wave adapter (#147571)
* Hide unnamed paths when selecting a USB Z-Wave adapter

* remove pointless sorting
2025-06-26 18:02:34 +00:00
Petar Petrov
ae062b230c Remove obsolete routing info when migrating a Z-Wave network (#147568) 2025-06-26 18:02:33 +00:00
Marcel van der Veldt
d523f85404 Fix sending commands to Matter vacuum (#147567) 2025-06-26 18:02:31 +00:00
tronikos
f28d6582c6 Refactor in Google AI TTS in preparation for STT (#147562) 2025-06-26 18:02:30 +00:00
Petar Petrov
1e81e5990e Bump zwave-js-server-python to 0.65.0 (#147561)
* Bump zwave-js-server-python to 0.65.0

* update tests
2025-06-26 18:02:29 +00:00
tronikos
5fe2e4b6ed Include subentries in Google Generative AI diagnostics (#147558) 2025-06-26 18:02:28 +00:00
tronikos
914bb3aa76 Use default title for migrated Google Generative AI entries (#147551) 2025-06-26 18:02:26 +00:00
Simone Chemelli
cfa6746115 Fix unload for Alexa Devices (#147548) 2025-06-26 18:02:25 +00:00
Simone Chemelli
03f9caf3eb Add action exceptions to Alexa Devices (#147546) 2025-06-26 18:02:24 +00:00
Joost Lekkerkerker
6b2aaf3fdb Show current Lametric version if there is no newer version (#147538) 2025-06-26 18:02:23 +00:00
Luca Angemi
2c4ea0d584 Fix wind direction state class sensor for AEMET (#147535) 2025-06-26 18:02:21 +00:00
Anders Peter Fugmann
e627811f7a Bump dependency on pyW215 for DLink integration to 0.8.0 (#147534) 2025-06-26 18:02:20 +00:00
Simone Chemelli
150f41641b Improve config flow strings for Alexa Devices (#147523) 2025-06-26 18:02:19 +00:00
Erik Montnemery
b9a7371996 Set end date for when allowing unique id collisions in config entries (#147516)
* Set end date for when allowing unique id collisions in config entries

* Update test
2025-06-26 18:02:17 +00:00
tronikos
7d0e99da43 Fixes in Google AI TTS (#147501)
* Fix Google AI not using correct config options after subentries migration

* Fixes in Google AI TTS

* Fix tests by @IvanLH

* Change type name.

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2025-06-26 18:02:16 +00:00
hanwg
71f281cc14 Fix Telegram bot default target when sending messages (#147470)
* handle targets

* updated error message

* validate chat id for single target

* add validation for chat id

* handle empty target

* handle empty target
2025-06-26 18:02:15 +00:00
Renat Sibgatulin
aec812a475 Create a new client session for air-Q to fix cookie polution (#147027) 2025-06-26 18:00:50 +00:00
Robin Lintermann
d4b548b169 Fixed issue when tests (should) fail in Smarla (#146102)
* Fixed issue when tests (should) fail

* Use usefixture decorator

* Throw ConfigEntryError instead of AuthFailed
2025-06-26 18:00:48 +00:00
Fabio Natanael Kepler
a296324c30 Fix playing TTS and local media source over DLNA (#134903)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-06-26 18:00:47 +00:00
Franck Nijhof
cff3d3d6ac Bump version to 2025.7.0b0 2025-06-25 18:51:19 +00:00
218 changed files with 4033 additions and 1810 deletions

4
CODEOWNERS generated
View File

@@ -452,8 +452,8 @@ build.json @home-assistant/supervisor
/tests/components/eq3btsmart/ @eulemitkeule @dbuezas
/homeassistant/components/escea/ @lazdavila
/tests/components/escea/ @lazdavila
/homeassistant/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco
/tests/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco
/homeassistant/components/esphome/ @jesserockz @kbx81 @bdraco
/tests/components/esphome/ @jesserockz @kbx81 @bdraco
/homeassistant/components/eufylife_ble/ @bdr99
/tests/components/eufylife_ble/ @bdr99
/homeassistant/components/event/ @home-assistant/core

View File

@@ -1,15 +1,7 @@
FROM mcr.microsoft.com/devcontainers/python:1-3.13
FROM mcr.microsoft.com/vscode/devcontainers/base:debian
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# Uninstall pre-installed formatting and linting tools
# They would conflict with our pinned versions
RUN \
pipx uninstall pydocstyle \
&& pipx uninstall pycodestyle \
&& pipx uninstall mypy \
&& pipx uninstall pylint
RUN \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
&& apt-get update \
@@ -32,21 +24,18 @@ RUN \
libxml2 \
git \
cmake \
autoconf \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Add go2rtc binary
COPY --from=ghcr.io/alexxit/go2rtc:latest /usr/local/bin/go2rtc /bin/go2rtc
# Install uv
RUN pip3 install uv
WORKDIR /usr/src
# Setup hass-release
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
&& uv pip install --system -e hass-release/ \
&& chown -R vscode /usr/src/hass-release/data
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
RUN uv python install 3.13.2
USER vscode
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
@@ -55,6 +44,10 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
WORKDIR /tmp
# Setup hass-release
RUN git clone --depth 1 https://github.com/home-assistant/hass-release ~/hass-release \
&& uv pip install -e ~/hass-release/
# Install Python dependencies from requirements
COPY requirements.txt ./
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
@@ -65,4 +58,4 @@ RUN uv pip install -r requirements_test.txt
WORKDIR /workspaces
# Set the default shell to bash instead of sh
ENV SHELL /bin/bash
ENV SHELL=/bin/bash

View File

@@ -2,19 +2,45 @@
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from aioamazondevices.api import AmazonEchoApi
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect, WrongCountry
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.selector import CountrySelector
from .const import CONF_LOGIN_DATA, DOMAIN
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_CODE): cv.string,
}
)
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
"""Validate the user input allows us to connect."""
api = AmazonEchoApi(
data[CONF_COUNTRY],
data[CONF_USERNAME],
data[CONF_PASSWORD],
)
try:
data = await api.login_mode_interactive(data[CONF_CODE])
finally:
await api.close()
return data
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Alexa Devices."""
@@ -25,17 +51,14 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the initial step."""
errors = {}
if user_input:
client = AmazonEchoApi(
user_input[CONF_COUNTRY],
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
)
try:
data = await client.login_mode_interactive(user_input[CONF_CODE])
data = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except CannotAuthenticate:
errors["base"] = "invalid_auth"
except WrongCountry:
errors["base"] = "wrong_country"
else:
await self.async_set_unique_id(data["customer_info"]["user_id"])
self._abort_if_unique_id_configured()
@@ -44,8 +67,6 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
title=user_input[CONF_USERNAME],
data=user_input | {CONF_LOGIN_DATA: data},
)
finally:
await client.close()
return self.async_show_form(
step_id="user",
@@ -61,3 +82,43 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
}
),
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauth flow."""
self.context["title_placeholders"] = {CONF_USERNAME: entry_data[CONF_USERNAME]}
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reauth confirm."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
entry_data = reauth_entry.data
if user_input is not None:
try:
await validate_input(self.hass, {**reauth_entry.data, **user_input})
except CannotConnect:
errors["base"] = "cannot_connect"
except CannotAuthenticate:
errors["base"] = "invalid_auth"
else:
return self.async_update_reload_and_abort(
reauth_entry,
data={
CONF_USERNAME: entry_data[CONF_USERNAME],
CONF_PASSWORD: entry_data[CONF_PASSWORD],
CONF_CODE: user_input[CONF_CODE],
},
)
return self.async_show_form(
step_id="reauth_confirm",
description_placeholders={CONF_USERNAME: entry_data[CONF_USERNAME]},
data_schema=STEP_REAUTH_DATA_SCHEMA,
errors=errors,
)

View File

@@ -12,10 +12,10 @@ from aioamazondevices.exceptions import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import _LOGGER, CONF_LOGIN_DATA
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
SCAN_INTERVAL = 30
@@ -55,4 +55,8 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
except (CannotConnect, CannotRetrieveData) as err:
raise UpdateFailed(f"Error occurred while updating {self.name}") from err
except CannotAuthenticate as err:
raise ConfigEntryError("Could not authenticate") from err
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
translation_placeholders={"error": repr(err)},
) from err

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "bronze",
"requirements": ["aioamazondevices==3.1.22"]
"requirements": ["aioamazondevices==3.2.2"]
}

View File

@@ -34,7 +34,7 @@ rules:
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
reauthentication-flow: done
test-coverage:
status: todo
comment: all tests missing

View File

@@ -22,17 +22,29 @@
"password": "[%key:component::alexa_devices::common::data_description_password%]",
"code": "[%key:component::alexa_devices::common::data_description_code%]"
}
},
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"code": "[%key:component::alexa_devices::common::data_code%]"
},
"data_description": {
"password": "[%key:component::alexa_devices::common::data_description_password%]",
"code": "[%key:component::alexa_devices::common::data_description_code%]"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"wrong_country": "Wrong country selected. Please select the country where your Amazon account is registered.",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},

View File

@@ -19,7 +19,7 @@
"bleak-retry-connector==3.9.0",
"bluetooth-adapters==0.21.4",
"bluetooth-auto-recovery==1.5.2",
"bluetooth-data-tools==1.28.1",
"bluetooth-data-tools==1.28.2",
"dbus-fast==2.43.0",
"habluetooth==3.49.0"
]

View File

@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==0.104.0"],
"requirements": ["hass-nabucasa==0.105.0"],
"single_config_entry": true
}

View File

@@ -6,11 +6,18 @@ from operator import itemgetter
import numpy as np
import voluptuous as vol
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
DOMAIN as SENSOR_DOMAIN,
STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
)
from homeassistant.const import (
CONF_ATTRIBUTE,
CONF_DEVICE_CLASS,
CONF_MAXIMUM,
CONF_MINIMUM,
CONF_NAME,
CONF_SOURCE,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
@@ -50,20 +57,23 @@ def datapoints_greater_than_degree(value: dict) -> dict:
COMPENSATION_SCHEMA = vol.Schema(
{
vol.Required(CONF_SOURCE): cv.entity_id,
vol.Optional(CONF_ATTRIBUTE): cv.string,
vol.Required(CONF_DATAPOINTS): [
vol.ExactSequence([vol.Coerce(float), vol.Coerce(float)])
],
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_ATTRIBUTE): cv.string,
vol.Optional(CONF_UPPER_LIMIT, default=False): cv.boolean,
vol.Optional(CONF_LOWER_LIMIT, default=False): cv.boolean,
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int,
vol.Optional(CONF_DEGREE, default=DEFAULT_DEGREE): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=7),
),
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_LOWER_LIMIT, default=False): cv.boolean,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int,
vol.Required(CONF_SOURCE): cv.entity_id,
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_UPPER_LIMIT, default=False): cv.boolean,
}
)

View File

@@ -7,15 +7,23 @@ from typing import Any
import numpy as np
from homeassistant.components.sensor import SensorEntity
from homeassistant.components.sensor import (
ATTR_STATE_CLASS,
CONF_STATE_CLASS,
SensorEntity,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
CONF_ATTRIBUTE,
CONF_DEVICE_CLASS,
CONF_MAXIMUM,
CONF_MINIMUM,
CONF_NAME,
CONF_SOURCE,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import (
@@ -59,24 +67,13 @@ async def async_setup_platform(
source: str = conf[CONF_SOURCE]
attribute: str | None = conf.get(CONF_ATTRIBUTE)
name = f"{DEFAULT_NAME} {source}"
if attribute is not None:
name = f"{name} {attribute}"
if not (name := conf.get(CONF_NAME)):
name = f"{DEFAULT_NAME} {source}"
if attribute is not None:
name = f"{name} {attribute}"
async_add_entities(
[
CompensationSensor(
conf.get(CONF_UNIQUE_ID),
name,
source,
attribute,
conf[CONF_PRECISION],
conf[CONF_POLYNOMIAL],
conf.get(CONF_UNIT_OF_MEASUREMENT),
conf[CONF_MINIMUM],
conf[CONF_MAXIMUM],
)
]
[CompensationSensor(conf.get(CONF_UNIQUE_ID), name, source, attribute, conf)]
)
@@ -91,23 +88,27 @@ class CompensationSensor(SensorEntity):
name: str,
source: str,
attribute: str | None,
precision: int,
polynomial: np.poly1d,
unit_of_measurement: str | None,
minimum: tuple[float, float] | None,
maximum: tuple[float, float] | None,
config: dict[str, Any],
) -> None:
"""Initialize the Compensation sensor."""
self._attr_name = name
self._source_entity_id = source
self._precision = precision
self._source_attribute = attribute
self._attr_native_unit_of_measurement = unit_of_measurement
self._precision = config[CONF_PRECISION]
self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
polynomial: np.poly1d = config[CONF_POLYNOMIAL]
self._poly = polynomial
self._coefficients = polynomial.coefficients.tolist()
self._attr_unique_id = unique_id
self._attr_name = name
self._minimum = minimum
self._maximum = maximum
self._minimum = config[CONF_MINIMUM]
self._maximum = config[CONF_MAXIMUM]
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
self._attr_state_class = config.get(CONF_STATE_CLASS)
async def async_added_to_hass(self) -> None:
"""Handle added to Hass."""
@@ -137,13 +138,40 @@ class CompensationSensor(SensorEntity):
"""Handle sensor state changes."""
new_state: State | None
if (new_state := event.data["new_state"]) is None:
_LOGGER.warning(
"While updating compensation %s, the new_state is None", self.name
)
self._attr_native_value = None
self.async_write_ha_state()
return
if new_state.state == STATE_UNKNOWN:
self._attr_native_value = None
self.async_write_ha_state()
return
if new_state.state == STATE_UNAVAILABLE:
self._attr_available = False
self.async_write_ha_state()
return
self._attr_available = True
if self.native_unit_of_measurement is None and self._source_attribute is None:
self._attr_native_unit_of_measurement = new_state.attributes.get(
ATTR_UNIT_OF_MEASUREMENT
)
if self._attr_device_class is None and (
device_class := new_state.attributes.get(ATTR_DEVICE_CLASS)
):
self._attr_device_class = device_class
if self._attr_state_class is None and (
state_class := new_state.attributes.get(ATTR_STATE_CLASS)
):
self._attr_state_class = state_class
if self._source_attribute:
value = new_state.attributes.get(self._source_attribute)
else:

View File

@@ -19,10 +19,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
SUPPORT_MINIMAL_SERVICES = VacuumEntityFeature.TURN_ON | VacuumEntityFeature.TURN_OFF
SUPPORT_BASIC_SERVICES = (
VacuumEntityFeature.STATE
| VacuumEntityFeature.START
| VacuumEntityFeature.STOP
| VacuumEntityFeature.BATTERY
VacuumEntityFeature.STATE | VacuumEntityFeature.START | VacuumEntityFeature.STOP
)
SUPPORT_MOST_SERVICES = (
@@ -31,7 +28,6 @@ SUPPORT_MOST_SERVICES = (
| VacuumEntityFeature.STOP
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.BATTERY
| VacuumEntityFeature.FAN_SPEED
)
@@ -46,7 +42,6 @@ SUPPORT_ALL_SERVICES = (
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.STATUS
| VacuumEntityFeature.BATTERY
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.MAP
| VacuumEntityFeature.CLEAN_SPOT
@@ -90,12 +85,6 @@ class StateDemoVacuum(StateVacuumEntity):
self._attr_activity = VacuumActivity.DOCKED
self._fan_speed = FAN_SPEEDS[1]
self._cleaned_area: float = 0
self._battery_level = 100
@property
def battery_level(self) -> int:
"""Return the current battery level of the vacuum."""
return max(0, min(100, self._battery_level))
@property
def fan_speed(self) -> str:
@@ -117,7 +106,6 @@ class StateDemoVacuum(StateVacuumEntity):
if self._attr_activity != VacuumActivity.CLEANING:
self._attr_activity = VacuumActivity.CLEANING
self._cleaned_area += 1.32
self._battery_level -= 1
self.schedule_update_ha_state()
def pause(self) -> None:
@@ -142,7 +130,6 @@ class StateDemoVacuum(StateVacuumEntity):
"""Perform a spot clean-up."""
self._attr_activity = VacuumActivity.CLEANING
self._cleaned_area += 1.32
self._battery_level -= 1
self.schedule_update_ha_state()
def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:

View File

@@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pydoods"],
"quality_scale": "legacy",
"requirements": ["pydoods==1.0.2", "Pillow==11.2.1"]
"requirements": ["pydoods==1.0.2", "Pillow==11.3.0"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==13.4.0"]
"requirements": ["py-sucks==0.9.11", "deebot-client==13.5.0"]
}

View File

@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["eheimdigital"],
"quality_scale": "bronze",
"requirements": ["eheimdigital==1.2.0"],
"requirements": ["eheimdigital==1.3.0"],
"zeroconf": [
{ "type": "_http._tcp.local.", "name": "eheimdigital._http._tcp.local." }
]

View File

@@ -281,7 +281,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
_static_info: _InfoT
_state: _StateT
_has_state: bool
_has_state: bool = False
unique_id: str
def __init__(

View File

@@ -2,7 +2,7 @@
"domain": "esphome",
"name": "ESPHome",
"after_dependencies": ["hassio", "zeroconf", "tag"],
"codeowners": ["@OttoWinter", "@jesserockz", "@kbx81", "@bdraco"],
"codeowners": ["@jesserockz", "@kbx81", "@bdraco"],
"config_flow": true,
"dependencies": ["assist_pipeline", "bluetooth", "intent", "ffmpeg", "http"],
"dhcp": [
@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==33.1.1",
"aioesphomeapi==34.1.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==2.16.0"
],

View File

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

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/generic",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["av==13.1.0", "Pillow==11.2.1"]
"requirements": ["av==13.1.0", "Pillow==11.3.0"]
}

View File

@@ -330,13 +330,14 @@ async def google_generative_ai_config_option_schema(
api_models = [api_model async for api_model in api_models_pager]
models = [
SelectOptionDict(
label=api_model.display_name,
label=api_model.name.lstrip("models/"),
value=api_model.name,
)
for api_model in sorted(api_models, key=lambda x: x.display_name or "")
for api_model in sorted(
api_models, key=lambda x: x.name.lstrip("models/") or ""
)
if (
api_model.display_name
and api_model.name
api_model.name
and ("tts" in api_model.name) == (subentry_type == "tts")
and "vision" not in api_model.name
and api_model.supported_actions

View File

@@ -11,6 +11,7 @@ from urllib.parse import quote
import aiohttp
from aiohttp import ClientTimeout, ClientWebSocketResponse, hdrs, web
from aiohttp.helpers import must_be_empty_body
from aiohttp.web_exceptions import HTTPBadGateway, HTTPBadRequest
from multidict import CIMultiDict
from yarl import URL
@@ -184,13 +185,16 @@ class HassIOIngress(HomeAssistantView):
content_type = "application/octet-stream"
# Simple request
if result.status in (204, 304) or (
if (empty_body := must_be_empty_body(result.method, result.status)) or (
content_length is not UNDEFINED
and (content_length_int := int(content_length))
<= MAX_SIMPLE_RESPONSE_SIZE
):
# Return Response
body = await result.read()
if empty_body:
body = None
else:
body = await result.read()
simple_response = web.Response(
headers=headers,
status=result.status,

View File

@@ -63,8 +63,8 @@ from .utils import get_device_macs, non_verifying_requests_session
_LOGGER = logging.getLogger(__name__)
class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle Huawei LTE config flow."""
class HuaweiLteConfigFlow(ConfigFlow, domain=DOMAIN):
"""Huawei LTE config flow."""
VERSION = 3
@@ -75,9 +75,9 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
) -> HuaweiLteOptionsFlow:
"""Get options flow."""
return OptionsFlowHandler()
return HuaweiLteOptionsFlow()
async def _async_show_user_form(
self,
@@ -354,7 +354,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
return self.async_update_reload_and_abort(entry, data=new_data)
class OptionsFlowHandler(OptionsFlow):
class HuaweiLteOptionsFlow(OptionsFlow):
"""Huawei LTE options flow."""
async def async_step_init(

View File

@@ -73,7 +73,6 @@ class AutomowerCalendarEntity(AutomowerBaseEntity, CalendarEntity):
schedule = self.mower_attributes.calendar
cursor = schedule.timeline.active_after(dt_util.now())
program_event = next(cursor, None)
_LOGGER.debug("program_event %s", program_event)
if not program_event:
return None
work_area_name = None

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/image_upload",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["Pillow==11.2.1"]
"requirements": ["Pillow==11.3.0"]
}

View File

@@ -209,6 +209,12 @@
"state": {
"off": "mdi:card-bulleted-off-outline"
}
},
"boost": {
"default": "mdi:thermometer-high",
"state": {
"off": "mdi:thermometer-off"
}
}
}
}

View File

@@ -464,6 +464,16 @@ class IronOSTemperatureNumberEntity(IronOSNumberEntity):
else super().native_max_value
)
@property
def available(self) -> bool:
"""Return True if entity is available."""
if (
self.entity_description.key is PinecilNumber.BOOST_TEMP
and self.native_value == 0
):
return False
return super().available
class IronOSSetpointNumberEntity(IronOSTemperatureNumberEntity):
"""IronOS setpoint temperature entity."""

View File

@@ -278,6 +278,9 @@
},
"calibrate_cjc": {
"name": "Calibrate CJC"
},
"boost": {
"name": "Boost"
}
}
},

View File

@@ -7,7 +7,7 @@ from dataclasses import dataclass
from enum import StrEnum
from typing import Any
from pynecil import CharSetting, SettingsDataResponse
from pynecil import CharSetting, SettingsDataResponse, TempUnit
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import EntityCategory
@@ -15,6 +15,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import IronOSConfigEntry
from .const import MIN_BOOST_TEMP, MIN_BOOST_TEMP_F
from .coordinator import IronOSCoordinators
from .entity import IronOSBaseEntity
@@ -39,6 +40,7 @@ class IronOSSwitch(StrEnum):
INVERT_BUTTONS = "invert_buttons"
DISPLAY_INVERT = "display_invert"
CALIBRATE_CJC = "calibrate_cjc"
BOOST = "boost"
SWITCH_DESCRIPTIONS: tuple[IronOSSwitchEntityDescription, ...] = (
@@ -94,6 +96,13 @@ SWITCH_DESCRIPTIONS: tuple[IronOSSwitchEntityDescription, ...] = (
entity_registry_enabled_default=False,
entity_category=EntityCategory.CONFIG,
),
IronOSSwitchEntityDescription(
key=IronOSSwitch.BOOST,
translation_key=IronOSSwitch.BOOST,
characteristic=CharSetting.BOOST_TEMP,
is_on_fn=lambda x: bool(x.get("boost_temp")),
entity_category=EntityCategory.CONFIG,
),
)
@@ -136,7 +145,15 @@ class IronOSSwitchEntity(IronOSBaseEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
await self.settings.write(self.entity_description.characteristic, True)
if self.entity_description.key is IronOSSwitch.BOOST:
await self.settings.write(
self.entity_description.characteristic,
MIN_BOOST_TEMP_F
if self.settings.data.get("temp_unit") is TempUnit.FAHRENHEIT
else MIN_BOOST_TEMP,
)
else:
await self.settings.write(self.entity_description.characteristic, True)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity on."""

View File

@@ -39,7 +39,8 @@ from .const import (
KNX_MODULE_KEY,
)
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
from .storage.const import CONF_ENTITY, CONF_GA_PASSIVE, CONF_GA_SENSOR, CONF_GA_STATE
from .storage.const import CONF_ENTITY, CONF_GA_SENSOR
from .storage.util import ConfigExtractor
async def async_setup_entry(
@@ -146,17 +147,17 @@ class KnxUiBinarySensor(_KnxBinarySensor, KnxUiEntity):
unique_id=unique_id,
entity_config=config[CONF_ENTITY],
)
knx_conf = ConfigExtractor(config[DOMAIN])
self._device = XknxBinarySensor(
xknx=knx_module.xknx,
name=config[CONF_ENTITY][CONF_NAME],
group_address_state=[
config[DOMAIN][CONF_GA_SENSOR][CONF_GA_STATE],
*config[DOMAIN][CONF_GA_SENSOR][CONF_GA_PASSIVE],
],
sync_state=config[DOMAIN][CONF_SYNC_STATE],
invert=config[DOMAIN].get(CONF_INVERT, False),
ignore_internal_state=config[DOMAIN].get(CONF_IGNORE_INTERNAL_STATE, False),
context_timeout=config[DOMAIN].get(CONF_CONTEXT_TIMEOUT),
reset_after=config[DOMAIN].get(CONF_RESET_AFTER),
group_address_state=knx_conf.get_state_and_passive(CONF_GA_SENSOR),
sync_state=knx_conf.get(CONF_SYNC_STATE),
invert=knx_conf.get(CONF_INVERT, default=False),
ignore_internal_state=knx_conf.get(
CONF_IGNORE_INTERNAL_STATE, default=False
),
context_timeout=knx_conf.get(CONF_CONTEXT_TIMEOUT),
reset_after=knx_conf.get(CONF_RESET_AFTER),
)
self._attr_force_update = self._device.ignore_internal_state

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from typing import Any, Literal
from typing import Any
from xknx import XKNX
from xknx.devices import Cover as XknxCover
@@ -35,15 +35,13 @@ from .schema import CoverSchema
from .storage.const import (
CONF_ENTITY,
CONF_GA_ANGLE,
CONF_GA_PASSIVE,
CONF_GA_POSITION_SET,
CONF_GA_POSITION_STATE,
CONF_GA_STATE,
CONF_GA_STEP,
CONF_GA_STOP,
CONF_GA_UP_DOWN,
CONF_GA_WRITE,
)
from .storage.util import ConfigExtractor
async def async_setup_entry(
@@ -230,38 +228,24 @@ class KnxYamlCover(_KnxCover, KnxYamlEntity):
def _create_ui_cover(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxCover:
"""Return a KNX Light device to be used within XKNX."""
def get_address(
key: str, address_type: Literal["write", "state"] = CONF_GA_WRITE
) -> str | None:
"""Get a single group address for given key."""
return knx_config[key][address_type] if key in knx_config else None
def get_addresses(
key: str, address_type: Literal["write", "state"] = CONF_GA_STATE
) -> list[Any] | None:
"""Get group address including passive addresses as list."""
return (
[knx_config[key][address_type], *knx_config[key][CONF_GA_PASSIVE]]
if key in knx_config
else None
)
conf = ConfigExtractor(knx_config)
return XknxCover(
xknx=xknx,
name=name,
group_address_long=get_addresses(CONF_GA_UP_DOWN, CONF_GA_WRITE),
group_address_short=get_addresses(CONF_GA_STEP, CONF_GA_WRITE),
group_address_stop=get_addresses(CONF_GA_STOP, CONF_GA_WRITE),
group_address_position=get_addresses(CONF_GA_POSITION_SET, CONF_GA_WRITE),
group_address_position_state=get_addresses(CONF_GA_POSITION_STATE),
group_address_angle=get_address(CONF_GA_ANGLE),
group_address_angle_state=get_addresses(CONF_GA_ANGLE),
travel_time_down=knx_config[CoverConf.TRAVELLING_TIME_DOWN],
travel_time_up=knx_config[CoverConf.TRAVELLING_TIME_UP],
invert_updown=knx_config.get(CoverConf.INVERT_UPDOWN, False),
invert_position=knx_config.get(CoverConf.INVERT_POSITION, False),
invert_angle=knx_config.get(CoverConf.INVERT_ANGLE, False),
sync_state=knx_config[CONF_SYNC_STATE],
group_address_long=conf.get_write_and_passive(CONF_GA_UP_DOWN),
group_address_short=conf.get_write_and_passive(CONF_GA_STEP),
group_address_stop=conf.get_write_and_passive(CONF_GA_STOP),
group_address_position=conf.get_write_and_passive(CONF_GA_POSITION_SET),
group_address_position_state=conf.get_state_and_passive(CONF_GA_POSITION_STATE),
group_address_angle=conf.get_write(CONF_GA_ANGLE),
group_address_angle_state=conf.get_state_and_passive(CONF_GA_ANGLE),
travel_time_down=conf.get(CoverConf.TRAVELLING_TIME_DOWN),
travel_time_up=conf.get(CoverConf.TRAVELLING_TIME_UP),
invert_updown=conf.get(CoverConf.INVERT_UPDOWN, default=False),
invert_position=conf.get(CoverConf.INVERT_POSITION, default=False),
invert_angle=conf.get(CoverConf.INVERT_ANGLE, default=False),
sync_state=conf.get(CONF_SYNC_STATE),
)

View File

@@ -35,7 +35,6 @@ from .schema import LightSchema
from .storage.const import (
CONF_COLOR_TEMP_MAX,
CONF_COLOR_TEMP_MIN,
CONF_DPT,
CONF_ENTITY,
CONF_GA_BLUE_BRIGHTNESS,
CONF_GA_BLUE_SWITCH,
@@ -45,17 +44,15 @@ from .storage.const import (
CONF_GA_GREEN_BRIGHTNESS,
CONF_GA_GREEN_SWITCH,
CONF_GA_HUE,
CONF_GA_PASSIVE,
CONF_GA_RED_BRIGHTNESS,
CONF_GA_RED_SWITCH,
CONF_GA_SATURATION,
CONF_GA_STATE,
CONF_GA_SWITCH,
CONF_GA_WHITE_BRIGHTNESS,
CONF_GA_WHITE_SWITCH,
CONF_GA_WRITE,
)
from .storage.entity_store_schema import LightColorMode
from .storage.util import ConfigExtractor
async def async_setup_entry(
@@ -203,94 +200,92 @@ def _create_yaml_light(xknx: XKNX, config: ConfigType) -> XknxLight:
def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight:
"""Return a KNX Light device to be used within XKNX."""
def get_write(key: str) -> str | None:
"""Get the write group address."""
return knx_config[key][CONF_GA_WRITE] if key in knx_config else None
def get_state(key: str) -> list[Any] | None:
"""Get the state group address."""
return (
[knx_config[key][CONF_GA_STATE], *knx_config[key][CONF_GA_PASSIVE]]
if key in knx_config
else None
)
def get_dpt(key: str) -> str | None:
"""Get the DPT."""
return knx_config[key].get(CONF_DPT) if key in knx_config else None
conf = ConfigExtractor(knx_config)
group_address_tunable_white = None
group_address_tunable_white_state = None
group_address_color_temp = None
group_address_color_temp_state = None
color_temperature_type = ColorTemperatureType.UINT_2_BYTE
if ga_color_temp := knx_config.get(CONF_GA_COLOR_TEMP):
if ga_color_temp[CONF_DPT] == ColorTempModes.RELATIVE.value:
group_address_tunable_white = ga_color_temp[CONF_GA_WRITE]
group_address_tunable_white_state = [
ga_color_temp[CONF_GA_STATE],
*ga_color_temp[CONF_GA_PASSIVE],
]
if _color_temp_dpt := conf.get_dpt(CONF_GA_COLOR_TEMP):
if _color_temp_dpt == ColorTempModes.RELATIVE.value:
group_address_tunable_white = conf.get_write(CONF_GA_COLOR_TEMP)
group_address_tunable_white_state = conf.get_state_and_passive(
CONF_GA_COLOR_TEMP
)
else:
# absolute uint or float
group_address_color_temp = ga_color_temp[CONF_GA_WRITE]
group_address_color_temp_state = [
ga_color_temp[CONF_GA_STATE],
*ga_color_temp[CONF_GA_PASSIVE],
]
if ga_color_temp[CONF_DPT] == ColorTempModes.ABSOLUTE_FLOAT.value:
group_address_color_temp = conf.get_write(CONF_GA_COLOR_TEMP)
group_address_color_temp_state = conf.get_state_and_passive(
CONF_GA_COLOR_TEMP
)
if _color_temp_dpt == ColorTempModes.ABSOLUTE_FLOAT.value:
color_temperature_type = ColorTemperatureType.FLOAT_2_BYTE
_color_dpt = get_dpt(CONF_GA_COLOR)
color_dpt = conf.get_dpt(CONF_GA_COLOR)
return XknxLight(
xknx,
name=name,
group_address_switch=get_write(CONF_GA_SWITCH),
group_address_switch_state=get_state(CONF_GA_SWITCH),
group_address_brightness=get_write(CONF_GA_BRIGHTNESS),
group_address_brightness_state=get_state(CONF_GA_BRIGHTNESS),
group_address_color=get_write(CONF_GA_COLOR)
if _color_dpt == LightColorMode.RGB
group_address_switch=conf.get_write(CONF_GA_SWITCH),
group_address_switch_state=conf.get_state_and_passive(CONF_GA_SWITCH),
group_address_brightness=conf.get_write(CONF_GA_BRIGHTNESS),
group_address_brightness_state=conf.get_state_and_passive(CONF_GA_BRIGHTNESS),
group_address_color=conf.get_write(CONF_GA_COLOR)
if color_dpt == LightColorMode.RGB
else None,
group_address_color_state=get_state(CONF_GA_COLOR)
if _color_dpt == LightColorMode.RGB
group_address_color_state=conf.get_state_and_passive(CONF_GA_COLOR)
if color_dpt == LightColorMode.RGB
else None,
group_address_rgbw=get_write(CONF_GA_COLOR)
if _color_dpt == LightColorMode.RGBW
group_address_rgbw=conf.get_write(CONF_GA_COLOR)
if color_dpt == LightColorMode.RGBW
else None,
group_address_rgbw_state=get_state(CONF_GA_COLOR)
if _color_dpt == LightColorMode.RGBW
group_address_rgbw_state=conf.get_state_and_passive(CONF_GA_COLOR)
if color_dpt == LightColorMode.RGBW
else None,
group_address_hue=get_write(CONF_GA_HUE),
group_address_hue_state=get_state(CONF_GA_HUE),
group_address_saturation=get_write(CONF_GA_SATURATION),
group_address_saturation_state=get_state(CONF_GA_SATURATION),
group_address_xyy_color=get_write(CONF_GA_COLOR)
if _color_dpt == LightColorMode.XYY
group_address_hue=conf.get_write(CONF_GA_HUE),
group_address_hue_state=conf.get_state_and_passive(CONF_GA_HUE),
group_address_saturation=conf.get_write(CONF_GA_SATURATION),
group_address_saturation_state=conf.get_state_and_passive(CONF_GA_SATURATION),
group_address_xyy_color=conf.get_write(CONF_GA_COLOR)
if color_dpt == LightColorMode.XYY
else None,
group_address_xyy_color_state=get_write(CONF_GA_COLOR)
if _color_dpt == LightColorMode.XYY
group_address_xyy_color_state=conf.get_write(CONF_GA_COLOR)
if color_dpt == LightColorMode.XYY
else None,
group_address_tunable_white=group_address_tunable_white,
group_address_tunable_white_state=group_address_tunable_white_state,
group_address_color_temperature=group_address_color_temp,
group_address_color_temperature_state=group_address_color_temp_state,
group_address_switch_red=get_write(CONF_GA_RED_SWITCH),
group_address_switch_red_state=get_state(CONF_GA_RED_SWITCH),
group_address_brightness_red=get_write(CONF_GA_RED_BRIGHTNESS),
group_address_brightness_red_state=get_state(CONF_GA_RED_BRIGHTNESS),
group_address_switch_green=get_write(CONF_GA_GREEN_SWITCH),
group_address_switch_green_state=get_state(CONF_GA_GREEN_SWITCH),
group_address_brightness_green=get_write(CONF_GA_GREEN_BRIGHTNESS),
group_address_brightness_green_state=get_state(CONF_GA_GREEN_BRIGHTNESS),
group_address_switch_blue=get_write(CONF_GA_BLUE_SWITCH),
group_address_switch_blue_state=get_state(CONF_GA_BLUE_SWITCH),
group_address_brightness_blue=get_write(CONF_GA_BLUE_BRIGHTNESS),
group_address_brightness_blue_state=get_state(CONF_GA_BLUE_BRIGHTNESS),
group_address_switch_white=get_write(CONF_GA_WHITE_SWITCH),
group_address_switch_white_state=get_state(CONF_GA_WHITE_SWITCH),
group_address_brightness_white=get_write(CONF_GA_WHITE_BRIGHTNESS),
group_address_brightness_white_state=get_state(CONF_GA_WHITE_BRIGHTNESS),
group_address_switch_red=conf.get_write(CONF_GA_RED_SWITCH),
group_address_switch_red_state=conf.get_state_and_passive(CONF_GA_RED_SWITCH),
group_address_brightness_red=conf.get_write(CONF_GA_RED_BRIGHTNESS),
group_address_brightness_red_state=conf.get_state_and_passive(
CONF_GA_RED_BRIGHTNESS
),
group_address_switch_green=conf.get_write(CONF_GA_GREEN_SWITCH),
group_address_switch_green_state=conf.get_state_and_passive(
CONF_GA_GREEN_SWITCH
),
group_address_brightness_green=conf.get_write(CONF_GA_GREEN_BRIGHTNESS),
group_address_brightness_green_state=conf.get_state_and_passive(
CONF_GA_GREEN_BRIGHTNESS
),
group_address_switch_blue=conf.get_write(CONF_GA_BLUE_SWITCH),
group_address_switch_blue_state=conf.get_state_and_passive(CONF_GA_BLUE_SWITCH),
group_address_brightness_blue=conf.get_write(CONF_GA_BLUE_BRIGHTNESS),
group_address_brightness_blue_state=conf.get_state_and_passive(
CONF_GA_BLUE_BRIGHTNESS
),
group_address_switch_white=conf.get_write(CONF_GA_WHITE_SWITCH),
group_address_switch_white_state=conf.get_state_and_passive(
CONF_GA_WHITE_SWITCH
),
group_address_brightness_white=conf.get_write(CONF_GA_WHITE_BRIGHTNESS),
group_address_brightness_white_state=conf.get_state_and_passive(
CONF_GA_WHITE_BRIGHTNESS
),
color_temperature_type=color_temperature_type,
min_kelvin=knx_config[CONF_COLOR_TEMP_MIN],
max_kelvin=knx_config[CONF_COLOR_TEMP_MAX],

View File

@@ -0,0 +1,51 @@
"""Utility functions for the KNX integration."""
from functools import partial
from typing import Any
from homeassistant.helpers.typing import ConfigType
from .const import CONF_DPT, CONF_GA_PASSIVE, CONF_GA_STATE, CONF_GA_WRITE
def nested_get(dic: ConfigType, *keys: str, default: Any | None = None) -> Any:
"""Get the value from a nested dictionary."""
for key in keys:
if key not in dic:
return default
dic = dic[key]
return dic
class ConfigExtractor:
"""Helper class for extracting values from a knx config store dictionary."""
__slots__ = ("get",)
def __init__(self, config: ConfigType) -> None:
"""Initialize the extractor."""
self.get = partial(nested_get, config)
def get_write(self, *path: str) -> str | None:
"""Get the write group address."""
return self.get(*path, CONF_GA_WRITE) # type: ignore[no-any-return]
def get_state(self, *path: str) -> str | None:
"""Get the state group address."""
return self.get(*path, CONF_GA_STATE) # type: ignore[no-any-return]
def get_write_and_passive(self, *path: str) -> list[Any | None]:
"""Get the group addresses of write and passive."""
write = self.get(*path, CONF_GA_WRITE)
passive = self.get(*path, CONF_GA_PASSIVE)
return [write, *passive] if passive else [write]
def get_state_and_passive(self, *path: str) -> list[Any | None]:
"""Get the group addresses of state and passive."""
state = self.get(*path, CONF_GA_STATE)
passive = self.get(*path, CONF_GA_PASSIVE)
return [state, *passive] if passive else [state]
def get_dpt(self, *path: str) -> str | None:
"""Get the data point type of a group address config key."""
return self.get(*path, CONF_DPT) # type: ignore[no-any-return]

View File

@@ -36,13 +36,8 @@ from .const import (
)
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
from .schema import SwitchSchema
from .storage.const import (
CONF_ENTITY,
CONF_GA_PASSIVE,
CONF_GA_STATE,
CONF_GA_SWITCH,
CONF_GA_WRITE,
)
from .storage.const import CONF_ENTITY, CONF_GA_SWITCH
from .storage.util import ConfigExtractor
async def async_setup_entry(
@@ -142,15 +137,13 @@ class KnxUiSwitch(_KnxSwitch, KnxUiEntity):
unique_id=unique_id,
entity_config=config[CONF_ENTITY],
)
knx_conf = ConfigExtractor(config[DOMAIN])
self._device = XknxSwitch(
knx_module.xknx,
name=config[CONF_ENTITY][CONF_NAME],
group_address=config[DOMAIN][CONF_GA_SWITCH][CONF_GA_WRITE],
group_address_state=[
config[DOMAIN][CONF_GA_SWITCH][CONF_GA_STATE],
*config[DOMAIN][CONF_GA_SWITCH][CONF_GA_PASSIVE],
],
respond_to_read=config[DOMAIN][CONF_RESPOND_TO_READ],
sync_state=config[DOMAIN][CONF_SYNC_STATE],
invert=config[DOMAIN][CONF_INVERT],
group_address=knx_conf.get_write(CONF_GA_SWITCH),
group_address_state=knx_conf.get_state_and_passive(CONF_GA_SWITCH),
respond_to_read=knx_conf.get(CONF_RESPOND_TO_READ),
sync_state=knx_conf.get(CONF_SYNC_STATE),
invert=knx_conf.get(CONF_INVERT),
)

View File

@@ -104,7 +104,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: LcnConfigEntry) -
) as ex:
await lcn_connection.async_close()
raise ConfigEntryNotReady(
f"Unable to connect to {config_entry.title}: {ex}"
translation_domain=DOMAIN,
translation_key="cannot_connect",
translation_placeholders={
"config_entry_title": config_entry.title,
},
) from ex
_LOGGER.info('LCN connected to "%s"', config_entry.title)

View File

@@ -26,6 +26,7 @@ from homeassistant.const import (
CONF_SWITCHES,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.typing import ConfigType
@@ -100,7 +101,11 @@ def get_resource(domain_name: str, domain_data: ConfigType) -> str:
return cast(str, domain_data["setpoint"])
if domain_name == "scene":
return f"{domain_data['register']}{domain_data['scene']}"
raise ValueError("Unknown domain")
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="invalid_domain",
translation_placeholders={CONF_DOMAIN: domain_name},
)
def generate_unique_id(
@@ -304,6 +309,8 @@ def get_device_config(
def is_states_string(states_string: str) -> list[str]:
"""Validate the given states string and return states list."""
if len(states_string) != 8:
raise ValueError("Invalid length of states string")
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="invalid_length_of_states_string"
)
states = {"1": "ON", "0": "OFF", "T": "TOGGLE", "-": "NOCHANGE"}
return [states[state_string] for state_string in states_string]

View File

@@ -19,7 +19,7 @@ rules:
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions: todo
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters:
status: exempt

View File

@@ -330,8 +330,9 @@ class SendKeys(LcnServiceCall):
if (delay_time := service.data[CONF_TIME]) != 0:
hit = pypck.lcn_defs.SendKeyCommand.HIT
if pypck.lcn_defs.SendKeyCommand[service.data[CONF_STATE]] != hit:
raise ValueError(
"Only hit command is allowed when sending deferred keys."
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_send_keys_action",
)
delay_unit = pypck.lcn_defs.TimeUnit.parse(service.data[CONF_TIME_UNIT])
await device_connection.send_keys_hit_deferred(keys, delay_time, delay_unit)
@@ -368,8 +369,9 @@ class LockKeys(LcnServiceCall):
if (delay_time := service.data[CONF_TIME]) != 0:
if table_id != 0:
raise ValueError(
"Only table A is allowed when locking keys for a specific time."
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_lock_keys_table",
)
delay_unit = pypck.lcn_defs.TimeUnit.parse(service.data[CONF_TIME_UNIT])
await device_connection.lock_keys_tab_a_temporary(

View File

@@ -414,11 +414,23 @@
}
},
"exceptions": {
"invalid_address": {
"message": "LCN device for given address has not been configured."
"cannot_connect": {
"message": "Unable to connect to {config_entry_title}."
},
"invalid_device_id": {
"message": "LCN device for given device ID has not been configured."
"message": "LCN device for given device ID {device_id} has not been configured."
},
"invalid_domain": {
"message": "Invalid domain {domain}."
},
"invalid_send_keys_action": {
"message": "Invalid state for sending keys. Only 'hit' allowed for deferred sending."
},
"invalid_lock_keys_table": {
"message": "Invalid table for locking keys. Only table A allowed when locking for a specific time."
},
"invalid_length_of_states_string": {
"message": "Invalid length of states string. Expected 8 characters."
}
}
}

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/ld2410_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["bluetooth-data-tools==1.28.1", "ld2410-ble==0.1.1"]
"requirements": ["bluetooth-data-tools==1.28.2", "ld2410-ble==0.1.1"]
}

View File

@@ -35,5 +35,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/led_ble",
"iot_class": "local_polling",
"requirements": ["bluetooth-data-tools==1.28.1", "led-ble==1.1.7"]
"requirements": ["bluetooth-data-tools==1.28.2", "led-ble==1.1.7"]
}

View File

@@ -200,7 +200,7 @@ async def async_remove_config_entry_device(
hass: HomeAssistant, entry: LookinConfigEntry, device_entry: dr.DeviceEntry
) -> bool:
"""Remove lookin config entry from a device."""
data: LookinData = hass.data[DOMAIN][entry.entry_id]
data = entry.runtime_data
all_identifiers: set[tuple[str, str]] = {
(DOMAIN, data.lookin_device.id),
*((DOMAIN, remote["UUID"]) for remote in data.devices),

View File

@@ -6,5 +6,5 @@
"iot_class": "cloud_push",
"loggers": ["matrix_client"],
"quality_scale": "legacy",
"requirements": ["matrix-nio==0.25.2", "Pillow==11.2.1"]
"requirements": ["matrix-nio==0.25.2", "Pillow==11.3.0"]
}

View File

@@ -7,6 +7,6 @@
"dependencies": ["websocket_api"],
"documentation": "https://www.home-assistant.io/integrations/matter",
"iot_class": "local_push",
"requirements": ["python-matter-server==7.0.0"],
"requirements": ["python-matter-server==8.0.0"],
"zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."]
}

View File

@@ -8,6 +8,7 @@ from typing import Any, cast
from chip.clusters import Objects as clusters
from chip.clusters.ClusterObjects import ClusterAttributeDescriptor, ClusterCommand
from matter_server.client.models import device_types
from matter_server.common import custom_clusters
from homeassistant.components.number import (
@@ -18,6 +19,7 @@ from homeassistant.components.number import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
PERCENTAGE,
EntityCategory,
Platform,
UnitOfLength,
@@ -123,6 +125,31 @@ class MatterRangeNumber(MatterEntity, NumberEntity):
)
class MatterLevelControlNumber(MatterEntity, NumberEntity):
"""Representation of a Matter Attribute as a Number entity."""
entity_description: MatterNumberEntityDescription
async def async_set_native_value(self, value: float) -> None:
"""Set level value."""
send_value = int(value)
if value_convert := self.entity_description.ha_to_native_value:
send_value = value_convert(value)
await self.send_device_command(
clusters.LevelControl.Commands.MoveToLevel(
level=send_value,
)
)
@callback
def _update_from_device(self) -> None:
"""Update from device."""
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
if value_convert := self.entity_description.measurement_to_ha:
value = value_convert(value)
self._attr_native_value = value
# Discovery schema(s) to map Matter Attributes to HA entities
DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
@@ -239,6 +266,26 @@ DISCOVERY_SCHEMAS = [
),
vendor_id=(4874,),
),
MatterDiscoverySchema(
platform=Platform.NUMBER,
entity_description=MatterNumberEntityDescription(
key="pump_setpoint",
native_unit_of_measurement=PERCENTAGE,
translation_key="pump_setpoint",
native_max_value=100,
native_min_value=0.5,
native_step=0.5,
measurement_to_ha=(
lambda x: None if x is None else x / 2 # Matter range (1-200)
),
ha_to_native_value=lambda x: round(x * 2), # HA range 0.5100.0%
mode=NumberMode.SLIDER,
),
entity_class=MatterLevelControlNumber,
required_attributes=(clusters.LevelControl.Attributes.CurrentLevel,),
device_type=(device_types.Pump,),
allow_multi=True,
),
MatterDiscoverySchema(
platform=Platform.NUMBER,
entity_description=MatterNumberEntityDescription(

View File

@@ -180,6 +180,9 @@
"altitude": {
"name": "Altitude above sea level"
},
"pump_setpoint": {
"name": "Setpoint"
},
"temperature_offset": {
"name": "Temperature offset"
},

View File

@@ -32,11 +32,18 @@ class ModelContextServerProtocolConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
llm_apis = {api.id: api.name for api in llm.async_get_apis(self.hass)}
if user_input is not None:
return self.async_create_entry(
title=llm_apis[user_input[CONF_LLM_HASS_API]], data=user_input
)
if not user_input[CONF_LLM_HASS_API]:
errors[CONF_LLM_HASS_API] = "llm_api_required"
else:
return self.async_create_entry(
title=", ".join(
llm_apis[api_id] for api_id in user_input[CONF_LLM_HASS_API]
),
data=user_input,
)
return self.async_show_form(
step_id="user",
@@ -44,7 +51,7 @@ class ModelContextServerProtocolConfigFlow(ConfigFlow, domain=DOMAIN):
{
vol.Optional(
CONF_LLM_HASS_API,
default=llm.LLM_API_ASSIST,
default=[llm.LLM_API_ASSIST],
): SelectSelector(
SelectSelectorConfig(
options=[
@@ -53,10 +60,12 @@ class ModelContextServerProtocolConfigFlow(ConfigFlow, domain=DOMAIN):
value=llm_api_id,
)
for llm_api_id, name in llm_apis.items()
]
],
multiple=True,
)
),
}
),
description_placeholders={"more_info_url": MORE_INFO_URL},
errors=errors,
)

View File

@@ -42,7 +42,7 @@ def _format_tool(
async def create_server(
hass: HomeAssistant, llm_api_id: str, llm_context: llm.LLMContext
hass: HomeAssistant, llm_api_id: str | list[str], llm_context: llm.LLMContext
) -> Server:
"""Create a new Model Context Protocol Server.

View File

@@ -11,6 +11,9 @@
}
}
},
"error": {
"llm_api_required": "At least one LLM API must be configured."
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}

View File

@@ -2,34 +2,23 @@
from __future__ import annotations
from datetime import timedelta
import logging
from bleak import BleakError
from medcom_ble import MedcomBleDeviceData
from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.unit_system import METRIC_SYSTEM
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
from .const import DOMAIN
from .coordinator import MedcomBleUpdateCoordinator
# Supported platforms
PLATFORMS: list[Platform] = [Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Medcom BLE radiation monitor from a config entry."""
address = entry.unique_id
elevation = hass.config.elevation
is_metric = hass.config.units is METRIC_SYSTEM
assert address is not None
ble_device = bluetooth.async_ble_device_from_address(hass, address)
@@ -38,26 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
f"Could not find Medcom BLE device with address {address}"
)
async def _async_update_method():
"""Get data from Medcom BLE radiation monitor."""
ble_device = bluetooth.async_ble_device_from_address(hass, address)
inspector = MedcomBleDeviceData(_LOGGER, elevation, is_metric)
try:
data = await inspector.update_device(ble_device)
except BleakError as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err
return data
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_method=_async_update_method,
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
)
coordinator = MedcomBleUpdateCoordinator(hass, entry, address)
await coordinator.async_config_entry_first_refresh()

View File

@@ -0,0 +1,50 @@
"""The Medcom BLE integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from bleak import BleakError
from medcom_ble import MedcomBleDevice, MedcomBleDeviceData
from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.unit_system import METRIC_SYSTEM
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
_LOGGER = logging.getLogger(__name__)
class MedcomBleUpdateCoordinator(DataUpdateCoordinator[MedcomBleDevice]):
"""Coordinator for Medcom BLE radiation monitor data."""
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant, entry: ConfigEntry, address: str) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
)
self._address = address
self._elevation = hass.config.elevation
self._is_metric = hass.config.units is METRIC_SYSTEM
async def _async_update_data(self) -> MedcomBleDevice:
"""Get data from Medcom BLE radiation monitor."""
ble_device = bluetooth.async_ble_device_from_address(self.hass, self._address)
inspector = MedcomBleDeviceData(_LOGGER, self._elevation, self._is_metric)
try:
data = await inspector.update_device(ble_device)
except BleakError as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err
return data

View File

@@ -4,8 +4,6 @@ from __future__ import annotations
import logging
from medcom_ble import MedcomBleDevice
from homeassistant import config_entries
from homeassistant.components.sensor import (
SensorEntity,
@@ -15,12 +13,10 @@ from homeassistant.components.sensor import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, UNIT_CPM
from .coordinator import MedcomBleUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -41,9 +37,7 @@ async def async_setup_entry(
) -> None:
"""Set up Medcom BLE radiation monitor sensors."""
coordinator: DataUpdateCoordinator[MedcomBleDevice] = hass.data[DOMAIN][
entry.entry_id
]
coordinator: MedcomBleUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
entities = []
_LOGGER.debug("got sensors: %s", coordinator.data.sensors)
@@ -62,16 +56,14 @@ async def async_setup_entry(
async_add_entities(entities)
class MedcomSensor(
CoordinatorEntity[DataUpdateCoordinator[MedcomBleDevice]], SensorEntity
):
class MedcomSensor(CoordinatorEntity[MedcomBleUpdateCoordinator], SensorEntity):
"""Medcom BLE radiation monitor sensors for the device."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: DataUpdateCoordinator[MedcomBleDevice],
coordinator: MedcomBleUpdateCoordinator,
entity_description: SensorEntityDescription,
) -> None:
"""Populate the medcom entity with relevant data."""

View File

@@ -30,6 +30,7 @@ from .const import (
DOMAIN,
MEDIA_CLASS_MAP,
MEDIA_MIME_TYPES,
MEDIA_SOURCE_DATA,
URI_SCHEME,
URI_SCHEME_REGEX,
)
@@ -78,7 +79,7 @@ def generate_media_source_id(domain: str, identifier: str) -> str:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the media_source component."""
hass.data[DOMAIN] = {}
hass.data[MEDIA_SOURCE_DATA] = {}
websocket_api.async_register_command(hass, websocket_browse_media)
websocket_api.async_register_command(hass, websocket_resolve_media)
frontend.async_register_built_in_panel(
@@ -97,7 +98,7 @@ async def _process_media_source_platform(
platform: MediaSourceProtocol,
) -> None:
"""Process a media source platform."""
hass.data[DOMAIN][domain] = await platform.async_get_media_source(hass)
hass.data[MEDIA_SOURCE_DATA][domain] = await platform.async_get_media_source(hass)
@callback
@@ -109,10 +110,10 @@ def _get_media_item(
item = MediaSourceItem.from_uri(hass, media_content_id, target_media_player)
else:
# We default to our own domain if its only one registered
domain = None if len(hass.data[DOMAIN]) > 1 else DOMAIN
domain = None if len(hass.data[MEDIA_SOURCE_DATA]) > 1 else DOMAIN
return MediaSourceItem(hass, domain, "", target_media_player)
if item.domain is not None and item.domain not in hass.data[DOMAIN]:
if item.domain is not None and item.domain not in hass.data[MEDIA_SOURCE_DATA]:
raise UnknownMediaSource(
translation_domain=DOMAIN,
translation_key="unknown_media_source",

View File

@@ -1,10 +1,18 @@
"""Constants for the media_source integration."""
from __future__ import annotations
import re
from typing import TYPE_CHECKING
from homeassistant.components.media_player import MediaClass
from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from .models import MediaSource
DOMAIN = "media_source"
MEDIA_SOURCE_DATA: HassKey[dict[str, MediaSource]] = HassKey(DOMAIN)
MEDIA_MIME_TYPES = ("audio", "video", "image")
MEDIA_CLASS_MAP = {
"audio": MediaClass.MUSIC,

View File

@@ -6,7 +6,7 @@ import logging
import mimetypes
from pathlib import Path
import shutil
from typing import Any
from typing import Any, cast
from aiohttp import web
from aiohttp.web_request import FileField
@@ -18,7 +18,7 @@ from homeassistant.components.media_player import BrowseError, MediaClass
from homeassistant.core import HomeAssistant, callback
from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES
from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES, MEDIA_SOURCE_DATA
from .error import Unresolvable
from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia
@@ -30,7 +30,7 @@ LOGGER = logging.getLogger(__name__)
def async_setup(hass: HomeAssistant) -> None:
"""Set up local media source."""
source = LocalSource(hass)
hass.data[DOMAIN][DOMAIN] = source
hass.data[MEDIA_SOURCE_DATA][DOMAIN] = source
hass.http.register_view(LocalMediaView(hass, source))
hass.http.register_view(UploadMediaView(hass, source))
websocket_api.async_register_command(hass, websocket_remove_media)
@@ -352,7 +352,7 @@ async def websocket_remove_media(
connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err))
return
source: LocalSource = hass.data[DOMAIN][DOMAIN]
source = cast(LocalSource, hass.data[MEDIA_SOURCE_DATA][DOMAIN])
try:
source_dir_id, location = source.async_parse_identifier(item)

View File

@@ -3,12 +3,12 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, cast
from typing import TYPE_CHECKING, Any
from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType
from homeassistant.core import HomeAssistant, callback
from .const import DOMAIN, URI_SCHEME, URI_SCHEME_REGEX
from .const import MEDIA_SOURCE_DATA, URI_SCHEME, URI_SCHEME_REGEX
@dataclass(slots=True)
@@ -70,7 +70,7 @@ class MediaSourceItem:
can_play=False,
can_expand=True,
)
for source in self.hass.data[DOMAIN].values()
for source in self.hass.data[MEDIA_SOURCE_DATA].values()
),
key=lambda item: item.title,
)
@@ -85,7 +85,9 @@ class MediaSourceItem:
@callback
def async_media_source(self) -> MediaSource:
"""Return media source that owns this item."""
return cast(MediaSource, self.hass.data[DOMAIN][self.domain])
if TYPE_CHECKING:
assert self.domain is not None
return self.hass.data[MEDIA_SOURCE_DATA][self.domain]
@classmethod
def from_uri(

View File

@@ -27,9 +27,11 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER]
type MelCloudConfigEntry = ConfigEntry[dict[str, list[MelCloudDevice]]]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Establish connection with MELClooud."""
async def async_setup_entry(hass: HomeAssistant, entry: MelCloudConfigEntry) -> bool:
"""Establish connection with MELCloud."""
conf = entry.data
try:
mel_devices = await mel_devices_setup(hass, conf[CONF_TOKEN])
@@ -40,20 +42,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except (TimeoutError, ClientConnectionError) as ex:
raise ConfigEntryNotReady from ex
hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: mel_devices})
entry.runtime_data = mel_devices
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
)
hass.data[DOMAIN].pop(config_entry.entry_id)
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN)
return unload_ok
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
class MelCloudDevice:

View File

@@ -24,13 +24,12 @@ from homeassistant.components.climate import (
HVACAction,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import MelCloudDevice
from . import MelCloudConfigEntry, MelCloudDevice
from .const import (
ATTR_STATUS,
ATTR_VANE_HORIZONTAL,
@@ -38,7 +37,6 @@ from .const import (
ATTR_VANE_VERTICAL,
ATTR_VANE_VERTICAL_POSITIONS,
CONF_POSITION,
DOMAIN,
SERVICE_SET_VANE_HORIZONTAL,
SERVICE_SET_VANE_VERTICAL,
)
@@ -77,11 +75,11 @@ ATW_ZONE_HVAC_ACTION_LOOKUP = {
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: MelCloudConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up MelCloud device climate based on config_entry."""
mel_devices = hass.data[DOMAIN][entry.entry_id]
mel_devices = entry.runtime_data
entities: list[AtaDeviceClimate | AtwDeviceZoneClimate] = [
AtaDeviceClimate(mel_device, mel_device.device)
for mel_device in mel_devices[DEVICE_TYPE_ATA]

View File

@@ -5,11 +5,12 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TOKEN, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import MelCloudConfigEntry
TO_REDACT = {
CONF_USERNAME,
CONF_TOKEN,
@@ -17,7 +18,7 @@ TO_REDACT = {
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, entry: MelCloudConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for the config entry."""
ent_reg = er.async_get(hass)

View File

@@ -15,13 +15,11 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfEnergy, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import MelCloudDevice
from .const import DOMAIN
from . import MelCloudConfigEntry, MelCloudDevice
@dataclasses.dataclass(frozen=True, kw_only=True)
@@ -105,11 +103,11 @@ ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: MelCloudConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up MELCloud device sensors based on config_entry."""
mel_devices = hass.data[DOMAIN].get(entry.entry_id)
mel_devices = entry.runtime_data
entities: list[MelDeviceSensor] = [
MelDeviceSensor(mel_device, description)

View File

@@ -17,22 +17,21 @@ from homeassistant.components.water_heater import (
WaterHeaterEntity,
WaterHeaterEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN, MelCloudDevice
from . import MelCloudConfigEntry, MelCloudDevice
from .const import ATTR_STATUS
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: MelCloudConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up MelCloud device climate based on config_entry."""
mel_devices = hass.data[DOMAIN][entry.entry_id]
mel_devices = entry.runtime_data
async_add_entities(
[
AtwWaterHeater(mel_device, mel_device.device)

View File

@@ -6,13 +6,11 @@ from melnor_bluetooth.device import Device
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth.match import BluetoothCallbackMatcher
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN
from .coordinator import MelnorDataUpdateCoordinator
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
PLATFORMS: list[Platform] = [
Platform.NUMBER,
@@ -22,11 +20,8 @@ PLATFORMS: list[Platform] = [
]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: MelnorConfigEntry) -> bool:
"""Set up melnor from a config entry."""
hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {})
ble_device = bluetooth.async_ble_device_from_address(hass, entry.data[CONF_ADDRESS])
if not ble_device:
@@ -60,20 +55,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = MelnorDataUpdateCoordinator(hass, entry, device)
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = coordinator
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: MelnorConfigEntry) -> bool:
"""Unload a config entry."""
await entry.runtime_data.data.disconnect()
device: Device = hass.data[DOMAIN][entry.entry_id].data
await device.disconnect()
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -11,15 +11,17 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
type MelnorConfigEntry = ConfigEntry[MelnorDataUpdateCoordinator]
class MelnorDataUpdateCoordinator(DataUpdateCoordinator[Device]):
"""Melnor data update coordinator."""
config_entry: ConfigEntry
config_entry: MelnorConfigEntry
_device: Device
def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, device: Device
self, hass: HomeAssistant, config_entry: MelnorConfigEntry, device: Device
) -> None:
"""Initialize my coordinator."""
super().__init__(

View File

@@ -13,13 +13,11 @@ from homeassistant.components.number import (
NumberEntityDescription,
NumberMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import MelnorDataUpdateCoordinator
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
from .entity import MelnorZoneEntity, get_entities_for_valves
@@ -67,12 +65,12 @@ ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneNumberEntityDescription] = [
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: MelnorConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the number platform."""
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
async_add_entities(
get_entities_for_valves(

View File

@@ -15,7 +15,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
@@ -26,8 +25,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util import dt as dt_util
from .const import DOMAIN
from .coordinator import MelnorDataUpdateCoordinator
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
from .entity import MelnorBluetoothEntity, MelnorZoneEntity, get_entities_for_valves
@@ -104,12 +102,12 @@ ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneSensorEntityDescription] = [
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: MelnorConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the sensor platform."""
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
# Device-level sensors
async_add_entities(

View File

@@ -13,12 +13,10 @@ from homeassistant.components.switch import (
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import MelnorDataUpdateCoordinator
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
from .entity import MelnorZoneEntity, get_entities_for_valves
@@ -51,12 +49,12 @@ ZONE_ENTITY_DESCRIPTIONS = [
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: MelnorConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the switch platform."""
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
async_add_entities(
get_entities_for_valves(

View File

@@ -10,13 +10,11 @@ from typing import Any
from melnor_bluetooth.device import Valve
from homeassistant.components.time import TimeEntity, TimeEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import MelnorDataUpdateCoordinator
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
from .entity import MelnorZoneEntity, get_entities_for_valves
@@ -41,12 +39,12 @@ ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneTimeEntityDescription] = [
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: MelnorConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the number platform."""
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
async_add_entities(
get_entities_for_valves(

View File

@@ -1,59 +1,21 @@
"""The met_eireann component."""
from collections.abc import Mapping
from datetime import timedelta
import logging
from typing import Any, Self
import meteireann
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, Platform
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
UPDATE_INTERVAL = timedelta(minutes=60)
from .coordinator import MetEireannUpdateCoordinator
PLATFORMS = [Platform.WEATHER]
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up Met Éireann as config entry."""
hass.data.setdefault(DOMAIN, {})
raw_weather_data = meteireann.WeatherData(
async_get_clientsession(hass),
latitude=config_entry.data[CONF_LATITUDE],
longitude=config_entry.data[CONF_LONGITUDE],
altitude=config_entry.data[CONF_ELEVATION],
)
weather_data = MetEireannWeatherData(config_entry.data, raw_weather_data)
async def _async_update_data() -> MetEireannWeatherData:
"""Fetch data from Met Éireann."""
try:
return await weather_data.fetch_data()
except Exception as err:
raise UpdateFailed(f"Update failed: {err}") from err
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_method=_async_update_data,
update_interval=UPDATE_INTERVAL,
)
coordinator = MetEireannUpdateCoordinator(hass, config_entry=config_entry)
await coordinator.async_refresh()
hass.data[DOMAIN][config_entry.entry_id] = coordinator
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
@@ -68,26 +30,3 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload_ok
class MetEireannWeatherData:
"""Keep data for Met Éireann weather entities."""
def __init__(
self, config: Mapping[str, Any], weather_data: meteireann.WeatherData
) -> None:
"""Initialise the weather entity data."""
self._config = config
self._weather_data = weather_data
self.current_weather_data: dict[str, Any] = {}
self.daily_forecast: list[dict[str, Any]] = []
self.hourly_forecast: list[dict[str, Any]] = []
async def fetch_data(self) -> Self:
"""Fetch data from API - (current weather and forecast)."""
await self._weather_data.fetching_data()
self.current_weather_data = self._weather_data.get_current_weather()
time_zone = dt_util.get_default_time_zone()
self.daily_forecast = self._weather_data.get_forecast(time_zone, False)
self.hourly_forecast = self._weather_data.get_forecast(time_zone, True)
return self

View File

@@ -0,0 +1,76 @@
"""The met_eireann component."""
from __future__ import annotations
from collections.abc import Mapping
from datetime import timedelta
import logging
from typing import Any, Self
import meteireann
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
UPDATE_INTERVAL = timedelta(minutes=60)
class MetEireannWeatherData:
"""Keep data for Met Éireann weather entities."""
def __init__(
self, config: Mapping[str, Any], weather_data: meteireann.WeatherData
) -> None:
"""Initialise the weather entity data."""
self._config = config
self._weather_data = weather_data
self.current_weather_data: dict[str, Any] = {}
self.daily_forecast: list[dict[str, Any]] = []
self.hourly_forecast: list[dict[str, Any]] = []
async def fetch_data(self) -> Self:
"""Fetch data from API - (current weather and forecast)."""
await self._weather_data.fetching_data()
self.current_weather_data = self._weather_data.get_current_weather()
time_zone = dt_util.get_default_time_zone()
self.daily_forecast = self._weather_data.get_forecast(time_zone, False)
self.hourly_forecast = self._weather_data.get_forecast(time_zone, True)
return self
class MetEireannUpdateCoordinator(DataUpdateCoordinator[MetEireannWeatherData]):
"""Coordinator for Met Éireann weather data."""
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_interval=UPDATE_INTERVAL,
)
raw_weather_data = meteireann.WeatherData(
async_get_clientsession(hass),
latitude=config_entry.data[CONF_LATITUDE],
longitude=config_entry.data[CONF_LONGITUDE],
altitude=config_entry.data[CONF_ELEVATION],
)
self._weather_data = MetEireannWeatherData(config_entry.data, raw_weather_data)
async def _async_update_data(self) -> MetEireannWeatherData:
"""Fetch data from Met Éireann."""
try:
return await self._weather_data.fetch_data()
except Exception as err:
raise UpdateFailed(f"Update failed: {err}") from err

View File

@@ -1,7 +1,6 @@
"""Support for Met Éireann weather service."""
from collections.abc import Mapping
import logging
from typing import Any, cast
from homeassistant.components.weather import (
@@ -29,10 +28,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import dt as dt_util
from . import MetEireannWeatherData
from .const import CONDITION_MAP, DEFAULT_NAME, DOMAIN, FORECAST_MAP
_LOGGER = logging.getLogger(__name__)
from .coordinator import MetEireannWeatherData
def format_condition(condition: str | None) -> str | None:

View File

@@ -23,7 +23,6 @@ from .const import (
COORDINATOR_RAIN,
DOMAIN,
PLATFORMS,
UNDO_UPDATE_LISTENER,
)
_LOGGER = logging.getLogger(__name__)
@@ -130,10 +129,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.title,
)
undo_listener = entry.add_update_listener(_async_update_listener)
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
hass.data[DOMAIN][entry.entry_id] = {
UNDO_UPDATE_LISTENER: undo_listener,
COORDINATOR_FORECAST: coordinator_forecast,
}
if coordinator_rain and coordinator_rain.last_update_success:
@@ -163,7 +161,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
hass.data[DOMAIN].pop(entry.entry_id)
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN)

View File

@@ -26,7 +26,6 @@ PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
COORDINATOR_FORECAST = "coordinator_forecast"
COORDINATOR_RAIN = "coordinator_rain"
COORDINATOR_ALERT = "coordinator_alert"
UNDO_UPDATE_LISTENER = "undo_update_listener"
ATTRIBUTION = "Data provided by Météo-France"
MODEL = "Météo-France mobile API"
MANUFACTURER = "Météo-France"

View File

@@ -6,6 +6,8 @@ import time
from meteofrance_api.model.forecast import Forecast as MeteoFranceForecast
from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_SUNNY,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_HUMIDITY,
ATTR_FORECAST_NATIVE_PRECIPITATION,
@@ -49,9 +51,13 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
def format_condition(condition: str):
def format_condition(condition: str, force_day: bool = False) -> str:
"""Return condition from dict CONDITION_MAP."""
return CONDITION_MAP.get(condition, condition)
mapped_condition = CONDITION_MAP.get(condition, condition)
if force_day and mapped_condition == ATTR_CONDITION_CLEAR_NIGHT:
# Meteo-France can return clear night condition instead of sunny for daily weather, so we map it to sunny
return ATTR_CONDITION_SUNNY
return mapped_condition
async def async_setup_entry(
@@ -212,7 +218,7 @@ class MeteoFranceWeather(
forecast["dt"]
).isoformat(),
ATTR_FORECAST_CONDITION: format_condition(
forecast["weather12H"]["desc"]
forecast["weather12H"]["desc"], force_day=True
),
ATTR_FORECAST_HUMIDITY: forecast["humidity"]["max"],
ATTR_FORECAST_NATIVE_TEMP: forecast["T"]["max"],

View File

@@ -1,43 +1,15 @@
"""Support for Meteoclimatic weather data."""
import logging
from meteoclimatic import MeteoclimaticClient
from meteoclimatic.exceptions import MeteoclimaticError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_STATION_CODE, DOMAIN, PLATFORMS, SCAN_INTERVAL
_LOGGER = logging.getLogger(__name__)
from .const import DOMAIN, PLATFORMS
from .coordinator import MeteoclimaticUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a Meteoclimatic entry."""
station_code = entry.data[CONF_STATION_CODE]
meteoclimatic_client = MeteoclimaticClient()
async def async_update_data():
"""Obtain the latest data from Meteoclimatic."""
try:
data = await hass.async_add_executor_job(
meteoclimatic_client.weather_at_station, station_code
)
except MeteoclimaticError as err:
raise UpdateFailed(f"Error while retrieving data: {err}") from err
return data.__dict__
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=f"Meteoclimatic weather for {entry.title} ({station_code})",
update_method=async_update_data,
update_interval=SCAN_INTERVAL,
)
coordinator = MeteoclimaticUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})

View File

@@ -0,0 +1,43 @@
"""Support for Meteoclimatic weather data."""
import logging
from typing import Any
from meteoclimatic import MeteoclimaticClient
from meteoclimatic.exceptions import MeteoclimaticError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_STATION_CODE, SCAN_INTERVAL
_LOGGER = logging.getLogger(__name__)
class MeteoclimaticUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Coordinator for Meteoclimatic weather data."""
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize the coordinator."""
self._station_code = entry.data[CONF_STATION_CODE]
super().__init__(
hass,
_LOGGER,
config_entry=entry,
name=f"Meteoclimatic weather for {entry.title} ({self._station_code})",
update_interval=SCAN_INTERVAL,
)
self._meteoclimatic_client = MeteoclimaticClient()
async def _async_update_data(self) -> dict[str, Any]:
"""Obtain the latest data from Meteoclimatic."""
try:
data = await self.hass.async_add_executor_job(
self._meteoclimatic_client.weather_at_station, self._station_code
)
except MeteoclimaticError as err:
raise UpdateFailed(f"Error while retrieving data: {err}") from err
return data.__dict__

View File

@@ -18,12 +18,10 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ATTRIBUTION, DOMAIN, MANUFACTURER, MODEL
from .coordinator import MeteoclimaticUpdateCoordinator
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
@@ -119,7 +117,7 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Meteoclimatic sensor platform."""
coordinator = hass.data[DOMAIN][entry.entry_id]
coordinator: MeteoclimaticUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
[MeteoclimaticSensor(coordinator, description) for description in SENSOR_TYPES],
@@ -127,13 +125,17 @@ async def async_setup_entry(
)
class MeteoclimaticSensor(CoordinatorEntity, SensorEntity):
class MeteoclimaticSensor(
CoordinatorEntity[MeteoclimaticUpdateCoordinator], SensorEntity
):
"""Representation of a Meteoclimatic sensor."""
_attr_attribution = ATTRIBUTION
def __init__(
self, coordinator: DataUpdateCoordinator, description: SensorEntityDescription
self,
coordinator: MeteoclimaticUpdateCoordinator,
description: SensorEntityDescription,
) -> None:
"""Initialize the Meteoclimatic sensor."""
super().__init__(coordinator)

View File

@@ -8,12 +8,10 @@ from homeassistant.const import UnitOfPressure, UnitOfSpeed, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ATTRIBUTION, CONDITION_MAP, DOMAIN, MANUFACTURER, MODEL
from .coordinator import MeteoclimaticUpdateCoordinator
def format_condition(condition):
@@ -31,12 +29,14 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Meteoclimatic weather platform."""
coordinator = hass.data[DOMAIN][entry.entry_id]
coordinator: MeteoclimaticUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([MeteoclimaticWeather(coordinator)], False)
class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity):
class MeteoclimaticWeather(
CoordinatorEntity[MeteoclimaticUpdateCoordinator], WeatherEntity
):
"""Representation of a weather condition."""
_attr_attribution = ATTRIBUTION
@@ -44,7 +44,7 @@ class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity):
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
def __init__(self, coordinator: DataUpdateCoordinator) -> None:
def __init__(self, coordinator: MeteoclimaticUpdateCoordinator) -> None:
"""Initialise the weather platform."""
super().__init__(coordinator)
self._unique_id = self.coordinator.data["station"].code

View File

@@ -9,6 +9,7 @@ from datapoint.Forecast import Forecast
from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN,
EntityCategory,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
@@ -59,6 +60,7 @@ SENSOR_TYPES: tuple[MetOfficeSensorEntityDescription, ...] = (
native_attr_name="name",
name="Station name",
icon="mdi:label-outline",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
MetOfficeSensorEntityDescription(
@@ -235,14 +237,13 @@ class MetOfficeCurrentSensor(
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
value = get_attribute(
self.coordinator.data.now(), self.entity_description.native_attr_name
)
native_attr = self.entity_description.native_attr_name
if (
self.entity_description.native_attr_name == "significantWeatherCode"
and value is not None
):
if native_attr == "name":
return str(self.coordinator.data.name)
value = get_attribute(self.coordinator.data.now(), native_attr)
if native_attr == "significantWeatherCode" and value is not None:
value = CONDITION_MAP.get(value)
return value

View File

@@ -10,13 +10,7 @@ from homeassistant.const import CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import (
CONF_NOT_FIRST_RUN,
DOMAIN,
FIRST_RUN,
MONOPRICE_OBJECT,
UNDO_UPDATE_LISTENER,
)
from .const import CONF_NOT_FIRST_RUN, DOMAIN, FIRST_RUN, MONOPRICE_OBJECT
PLATFORMS = [Platform.MEDIA_PLAYER]
@@ -41,11 +35,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry, data={**entry.data, CONF_NOT_FIRST_RUN: True}
)
undo_listener = entry.add_update_listener(_update_listener)
entry.async_on_unload(entry.add_update_listener(_update_listener))
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
MONOPRICE_OBJECT: monoprice,
UNDO_UPDATE_LISTENER: undo_listener,
FIRST_RUN: first_run,
}
@@ -60,8 +53,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if not unload_ok:
return False
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
def _cleanup(monoprice) -> None:
"""Destroy the Monoprice object.

View File

@@ -18,4 +18,3 @@ SERVICE_RESTORE = "restore"
FIRST_RUN = "first_run"
MONOPRICE_OBJECT = "monoprice_object"
UNDO_UPDATE_LISTENER = "update_update_listener"

View File

@@ -34,7 +34,7 @@ class MusicAssistantEntity(Entity):
identifiers={(DOMAIN, player_id)},
manufacturer=self.player.device_info.manufacturer or provider.name,
model=self.player.device_info.model or self.player.name,
name=self.player.display_name,
name=self.player.name,
configuration_url=f"{mass.server_url}/#/settings/editplayer/{player_id}",
)

View File

@@ -7,6 +7,6 @@
"documentation": "https://www.home-assistant.io/integrations/music_assistant",
"iot_class": "local_push",
"loggers": ["music_assistant"],
"requirements": ["music-assistant-client==1.2.0"],
"requirements": ["music-assistant-client==1.2.4"],
"zeroconf": ["_mass._tcp.local."]
}

View File

@@ -6,11 +6,7 @@ import logging
from typing import TYPE_CHECKING, Any, cast
from music_assistant_models.enums import MediaType as MASSMediaType
from music_assistant_models.media_items import (
BrowseFolder,
MediaItemType,
SearchResults,
)
from music_assistant_models.media_items import MediaItemType, SearchResults
from homeassistant.components import media_source
from homeassistant.components.media_player import (
@@ -549,8 +545,6 @@ def _process_search_results(
# Add available items to results
for item in items:
if TYPE_CHECKING:
assert not isinstance(item, BrowseFolder)
if not item.available:
continue

View File

@@ -248,10 +248,8 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
player = self.player
active_queue = self.active_queue
# update generic attributes
if player.powered and active_queue is not None:
self._attr_state = MediaPlayerState(active_queue.state.value)
if player.powered and player.state is not None:
self._attr_state = MediaPlayerState(player.state.value)
if player.powered and player.playback_state is not None:
self._attr_state = MediaPlayerState(player.playback_state.value)
else:
self._attr_state = MediaPlayerState(STATE_OFF)
# active source and source list (translate to HA source names)
@@ -270,12 +268,12 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
self._attr_source = active_source_name
group_members: list[str] = []
if player.group_childs:
group_members = player.group_childs
if player.group_members:
group_members = player.group_members
elif player.synced_to and (parent := self.mass.players.get(player.synced_to)):
group_members = parent.group_childs
group_members = parent.group_members
# translate MA group_childs to HA group_members as entity id's
# translate MA group_members to HA group_members as entity id's
entity_registry = er.async_get(self.hass)
group_members_entity_ids: list[str] = [
entity_id

View File

@@ -9,13 +9,11 @@ from pymystrom.bulb import MyStromBulb
from pymystrom.exceptions import MyStromConnectionError
from pymystrom.switch import MyStromSwitch
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN
from .models import MyStromData
from .models import MyStromConfigEntry, MyStromData
PLATFORMS_PLUGS = [Platform.SENSOR, Platform.SWITCH]
PLATFORMS_BULB = [Platform.LIGHT]
@@ -41,7 +39,7 @@ def _get_mystrom_switch(host: str) -> MyStromSwitch:
return MyStromSwitch(host)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: MyStromConfigEntry) -> bool:
"""Set up myStrom from a config entry."""
host = entry.data[CONF_HOST]
try:
@@ -73,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.error("Unsupported myStrom device type: %s", device_type)
return False
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = MyStromData(
entry.runtime_data = MyStromData(
device=device,
info=info,
)
@@ -82,15 +80,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: MyStromConfigEntry) -> bool:
"""Unload a config entry."""
device_type = hass.data[DOMAIN][entry.entry_id].info["type"]
device_type = entry.runtime_data.info["type"]
platforms = []
if device_type in [101, 106, 107, 120]:
platforms.extend(PLATFORMS_PLUGS)
elif device_type in [102, 105]:
platforms.extend(PLATFORMS_BULB)
if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, platforms)

View File

@@ -15,12 +15,12 @@ from homeassistant.components.light import (
LightEntity,
LightEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, MANUFACTURER
from .models import MyStromConfigEntry
_LOGGER = logging.getLogger(__name__)
@@ -32,12 +32,12 @@ EFFECT_SUNRISE = "sunrise"
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: MyStromConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the myStrom entities."""
info = hass.data[DOMAIN][entry.entry_id].info
device = hass.data[DOMAIN][entry.entry_id].device
info = entry.runtime_data.info
device = entry.runtime_data.device
async_add_entities([MyStromLight(device, entry.title, info["mac"])])

View File

@@ -6,6 +6,10 @@ from typing import Any
from pymystrom.bulb import MyStromBulb
from pymystrom.switch import MyStromSwitch
from homeassistant.config_entries import ConfigEntry
type MyStromConfigEntry = ConfigEntry[MyStromData]
@dataclass
class MyStromData:

View File

@@ -13,13 +13,13 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfPower, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, MANUFACTURER
from .models import MyStromConfigEntry
@dataclass(frozen=True)
@@ -56,11 +56,11 @@ SENSOR_TYPES: tuple[MyStromSwitchSensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: MyStromConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the myStrom entities."""
device: MyStromSwitch = hass.data[DOMAIN][entry.entry_id].device
device: MyStromSwitch = entry.runtime_data.device
async_add_entities(
MyStromSwitchSensor(device, entry.title, description)

View File

@@ -8,12 +8,12 @@ from typing import Any
from pymystrom.exceptions import MyStromConnectionError
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo, format_mac
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, MANUFACTURER
from .models import MyStromConfigEntry
DEFAULT_NAME = "myStrom Switch"
@@ -22,11 +22,11 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: MyStromConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the myStrom entities."""
device = hass.data[DOMAIN][entry.entry_id].device
device = entry.runtime_data.device
async_add_entities([MyStromSwitch(device, entry.title)])

View File

@@ -44,15 +44,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: NAMConfigEntry) -> bool:
translation_key="device_communication_error",
translation_placeholders={"device": entry.title},
) from err
try:
await nam.async_check_credentials()
except (ApiError, ClientError) as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="device_communication_error",
translation_placeholders={"device": entry.title},
) from err
except AuthFailedError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
import logging
from typing import Any
@@ -26,15 +25,6 @@ from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import DOMAIN
@dataclass
class NamConfig:
"""NAM device configuration class."""
mac_address: str
auth_enabled: bool
_LOGGER = logging.getLogger(__name__)
AUTH_SCHEMA = vol.Schema(
@@ -42,29 +32,14 @@ AUTH_SCHEMA = vol.Schema(
)
async def async_get_config(hass: HomeAssistant, host: str) -> NamConfig:
"""Get device MAC address and auth_enabled property."""
websession = async_get_clientsession(hass)
options = ConnectionOptions(host)
nam = await NettigoAirMonitor.create(websession, options)
mac = await nam.async_get_mac_address()
return NamConfig(mac, nam.auth_enabled)
async def async_check_credentials(
async def async_get_nam(
hass: HomeAssistant, host: str, data: dict[str, Any]
) -> None:
"""Check if credentials are valid."""
) -> NettigoAirMonitor:
"""Get NAM client."""
websession = async_get_clientsession(hass)
options = ConnectionOptions(host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD))
nam = await NettigoAirMonitor.create(websession, options)
await nam.async_check_credentials()
return await NettigoAirMonitor.create(websession, options)
class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
@@ -72,8 +47,8 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
_config: NamConfig
host: str
auth_enabled: bool = False
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -85,21 +60,20 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
self.host = user_input[CONF_HOST]
try:
config = await async_get_config(self.hass, self.host)
nam = await async_get_nam(self.hass, self.host, {})
except (ApiError, ClientConnectorError, TimeoutError):
errors["base"] = "cannot_connect"
except CannotGetMacError:
return self.async_abort(reason="device_unsupported")
except AuthFailedError:
return await self.async_step_credentials()
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(format_mac(config.mac_address))
await self.async_set_unique_id(format_mac(nam.mac))
self._abort_if_unique_id_configured({CONF_HOST: self.host})
if config.auth_enabled is True:
return await self.async_step_credentials()
return self.async_create_entry(
title=self.host,
data=user_input,
@@ -119,7 +93,7 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
if user_input is not None:
try:
await async_check_credentials(self.hass, self.host, user_input)
nam = await async_get_nam(self.hass, self.host, user_input)
except AuthFailedError:
errors["base"] = "invalid_auth"
except (ApiError, ClientConnectorError, TimeoutError):
@@ -128,6 +102,9 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(format_mac(nam.mac))
self._abort_if_unique_id_configured({CONF_HOST: self.host})
return self.async_create_entry(
title=self.host,
data={**user_input, CONF_HOST: self.host},
@@ -148,14 +125,16 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
self._async_abort_entries_match({CONF_HOST: self.host})
try:
self._config = await async_get_config(self.hass, self.host)
nam = await async_get_nam(self.hass, self.host, {})
except (ApiError, ClientConnectorError, TimeoutError):
return self.async_abort(reason="cannot_connect")
except CannotGetMacError:
return self.async_abort(reason="device_unsupported")
except AuthFailedError:
self.auth_enabled = True
return await self.async_step_confirm_discovery()
await self.async_set_unique_id(format_mac(self._config.mac_address))
self._abort_if_unique_id_configured({CONF_HOST: self.host})
await self.async_set_unique_id(format_mac(nam.mac))
return await self.async_step_confirm_discovery()
@@ -171,7 +150,7 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
data={CONF_HOST: self.host},
)
if self._config.auth_enabled is True:
if self.auth_enabled is True:
return await self.async_step_credentials()
self._set_confirm_only()
@@ -198,7 +177,7 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
if user_input is not None:
try:
await async_check_credentials(self.hass, self.host, user_input)
await async_get_nam(self.hass, self.host, user_input)
except (
ApiError,
AuthFailedError,
@@ -228,11 +207,11 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
if user_input is not None:
try:
config = await async_get_config(self.hass, user_input[CONF_HOST])
nam = await async_get_nam(self.hass, user_input[CONF_HOST], {})
except (ApiError, ClientConnectorError, TimeoutError):
errors["base"] = "cannot_connect"
else:
await self.async_set_unique_id(format_mac(config.mac_address))
await self.async_set_unique_id(format_mac(nam.mac))
self._abort_if_unique_id_mismatch(reason="another_device")
return self.async_update_reload_and_abort(

View File

@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["nettigo_air_monitor"],
"requirements": ["nettigo-air-monitor==4.1.0"],
"requirements": ["nettigo-air-monitor==5.0.0"],
"zeroconf": [
{
"type": "_http._tcp.local.",

View File

@@ -6,6 +6,7 @@ from collections.abc import Callable
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
import aiohttp
from pynordpool import (
Currency,
DeliveryPeriodData,
@@ -91,6 +92,8 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodsData]):
except (
NordPoolResponseError,
NordPoolError,
TimeoutError,
aiohttp.ClientError,
) as error:
LOGGER.debug("Connection error: %s", error)
self.async_set_update_error(error)

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio
import logging
from types import MappingProxyType
import httpx
import ollama
@@ -100,8 +101,12 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
for entry in entries:
use_existing = False
# Create subentry with model from entry.data and options from entry.options
subentry_data = entry.options.copy()
subentry_data[CONF_MODEL] = entry.data[CONF_MODEL]
subentry = ConfigSubentry(
data=entry.options,
data=MappingProxyType(subentry_data),
subentry_type="conversation",
title=entry.title,
unique_id=None,
@@ -154,9 +159,11 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
hass.config_entries.async_update_entry(
entry,
title=DEFAULT_NAME,
# Update parent entry to only keep URL, remove model
data={CONF_URL: entry.data[CONF_URL]},
options={},
version=2,
minor_version=2,
version=3,
minor_version=1,
)
@@ -164,7 +171,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: OllamaConfigEntry) ->
"""Migrate entry."""
_LOGGER.debug("Migrating from version %s:%s", entry.version, entry.minor_version)
if entry.version > 2:
if entry.version > 3:
# This means the user has downgraded from a future version
return False
@@ -182,6 +189,25 @@ async def async_migrate_entry(hass: HomeAssistant, entry: OllamaConfigEntry) ->
hass.config_entries.async_update_entry(entry, minor_version=2)
if entry.version == 2 and entry.minor_version == 2:
# Update subentries to include the model
for subentry in entry.subentries.values():
if subentry.subentry_type == "conversation":
updated_data = dict(subentry.data)
updated_data[CONF_MODEL] = entry.data[CONF_MODEL]
hass.config_entries.async_update_subentry(
entry, subentry, data=MappingProxyType(updated_data)
)
# Update main entry to remove model and bump version
hass.config_entries.async_update_entry(
entry,
data={CONF_URL: entry.data[CONF_URL]},
version=3,
minor_version=1,
)
_LOGGER.debug(
"Migration to version %s:%s successful", entry.version, entry.minor_version
)

View File

@@ -22,7 +22,7 @@ from homeassistant.config_entries import (
)
from homeassistant.const import CONF_LLM_HASS_API, CONF_NAME, CONF_URL
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import llm
from homeassistant.helpers import config_validation as cv, llm
from homeassistant.helpers.selector import (
BooleanSelector,
NumberSelector,
@@ -38,6 +38,7 @@ from homeassistant.helpers.selector import (
)
from homeassistant.util.ssl import get_default_context
from . import OllamaConfigEntry
from .const import (
CONF_KEEP_ALIVE,
CONF_MAX_HISTORY,
@@ -72,43 +73,43 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
class OllamaConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Ollama."""
VERSION = 2
MINOR_VERSION = 2
VERSION = 3
MINOR_VERSION = 1
def __init__(self) -> None:
"""Initialize config flow."""
self.url: str | None = None
self.model: str | None = None
self.client: ollama.AsyncClient | None = None
self.download_task: asyncio.Task | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
user_input = user_input or {}
self.url = user_input.get(CONF_URL, self.url)
self.model = user_input.get(CONF_MODEL, self.model)
if self.url is None:
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, last_step=False
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
)
errors = {}
url = user_input[CONF_URL]
self._async_abort_entries_match({CONF_URL: self.url})
self._async_abort_entries_match({CONF_URL: url})
try:
self.client = ollama.AsyncClient(
host=self.url, verify=get_default_context()
url = cv.url(url)
except vol.Invalid:
errors["base"] = "invalid_url"
return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(
STEP_USER_DATA_SCHEMA, user_input
),
errors=errors,
)
async with asyncio.timeout(DEFAULT_TIMEOUT):
response = await self.client.list()
downloaded_models: set[str] = {
model_info["model"] for model_info in response.get("models", [])
}
try:
client = ollama.AsyncClient(host=url, verify=get_default_context())
async with asyncio.timeout(DEFAULT_TIMEOUT):
await client.list()
except (TimeoutError, httpx.ConnectError):
errors["base"] = "cannot_connect"
except Exception:
@@ -117,10 +118,69 @@ class OllamaConfigFlow(ConfigFlow, domain=DOMAIN):
if errors:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
step_id="user",
data_schema=self.add_suggested_values_to_schema(
STEP_USER_DATA_SCHEMA, user_input
),
errors=errors,
)
if self.model is None:
return self.async_create_entry(
title=url,
data={CONF_URL: url},
)
@classmethod
@callback
def async_get_supported_subentry_types(
cls, config_entry: ConfigEntry
) -> dict[str, type[ConfigSubentryFlow]]:
"""Return subentries supported by this integration."""
return {"conversation": ConversationSubentryFlowHandler}
class ConversationSubentryFlowHandler(ConfigSubentryFlow):
"""Flow for managing conversation subentries."""
def __init__(self) -> None:
"""Initialize the subentry flow."""
super().__init__()
self._name: str | None = None
self._model: str | None = None
self.download_task: asyncio.Task | None = None
self._config_data: dict[str, Any] | None = None
@property
def _is_new(self) -> bool:
"""Return if this is a new subentry."""
return self.source == "user"
@property
def _client(self) -> ollama.AsyncClient:
"""Return the Ollama client."""
entry: OllamaConfigEntry = self._get_entry()
return entry.runtime_data
async def async_step_set_options(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""Handle model selection and configuration step."""
if self._get_entry().state != ConfigEntryState.LOADED:
return self.async_abort(reason="entry_not_loaded")
if user_input is None:
# Get available models from Ollama server
try:
async with asyncio.timeout(DEFAULT_TIMEOUT):
response = await self._client.list()
downloaded_models: set[str] = {
model_info["model"] for model_info in response.get("models", [])
}
except (TimeoutError, httpx.ConnectError, httpx.HTTPError):
_LOGGER.exception("Failed to get models from Ollama server")
return self.async_abort(reason="cannot_connect")
# Show models that have been downloaded first, followed by all known
# models (only latest tags).
models_to_list = [
@@ -131,52 +191,69 @@ class OllamaConfigFlow(ConfigFlow, domain=DOMAIN):
for m in sorted(MODEL_NAMES)
if m not in downloaded_models
]
model_step_schema = vol.Schema(
{
vol.Required(
CONF_MODEL, description={"suggested_value": DEFAULT_MODEL}
): SelectSelector(
SelectSelectorConfig(options=models_to_list, custom_value=True)
),
}
)
if self._is_new:
options = {}
else:
options = self._get_reconfigure_subentry().data.copy()
return self.async_show_form(
step_id="user",
data_schema=model_step_schema,
step_id="set_options",
data_schema=vol.Schema(
ollama_config_option_schema(
self.hass, self._is_new, options, models_to_list
)
),
)
if self.model not in downloaded_models:
# Ollama server needs to download model first
return await self.async_step_download()
self._model = user_input[CONF_MODEL]
if self._is_new:
self._name = user_input.pop(CONF_NAME)
return self.async_create_entry(
title=self.url,
data={CONF_URL: self.url, CONF_MODEL: self.model},
subentries=[
{
"subentry_type": "conversation",
"data": {},
"title": _get_title(self.model),
"unique_id": None,
}
],
# Check if model needs to be downloaded
try:
async with asyncio.timeout(DEFAULT_TIMEOUT):
response = await self._client.list()
currently_downloaded_models: set[str] = {
model_info["model"] for model_info in response.get("models", [])
}
if self._model not in currently_downloaded_models:
# Store the user input to use after download
self._config_data = user_input
# Ollama server needs to download model first
return await self.async_step_download()
except Exception:
_LOGGER.exception("Failed to check model availability")
return self.async_abort(reason="cannot_connect")
# Model is already downloaded, create/update the entry
if self._is_new:
return self.async_create_entry(
title=self._name,
data=user_input,
)
return self.async_update_and_abort(
self._get_entry(),
self._get_reconfigure_subentry(),
data=user_input,
)
async def async_step_download(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
) -> SubentryFlowResult:
"""Step to wait for Ollama server to download a model."""
assert self.model is not None
assert self.client is not None
assert self._model is not None
if self.download_task is None:
# Tell Ollama server to pull the model.
# The task will block until the model and metadata are fully
# downloaded.
self.download_task = self.hass.async_create_background_task(
self.client.pull(self.model),
f"Downloading {self.model}",
self._client.pull(self._model),
f"Downloading {self._model}",
)
if self.download_task.done():
@@ -192,80 +269,28 @@ class OllamaConfigFlow(ConfigFlow, domain=DOMAIN):
progress_task=self.download_task,
)
async def async_step_finish(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Step after model downloading has succeeded."""
assert self.url is not None
assert self.model is not None
return self.async_create_entry(
title=_get_title(self.model),
data={CONF_URL: self.url, CONF_MODEL: self.model},
subentries=[
{
"subentry_type": "conversation",
"data": {},
"title": _get_title(self.model),
"unique_id": None,
}
],
)
async def async_step_failed(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
) -> SubentryFlowResult:
"""Step after model downloading has failed."""
return self.async_abort(reason="download_failed")
@classmethod
@callback
def async_get_supported_subentry_types(
cls, config_entry: ConfigEntry
) -> dict[str, type[ConfigSubentryFlow]]:
"""Return subentries supported by this integration."""
return {"conversation": ConversationSubentryFlowHandler}
class ConversationSubentryFlowHandler(ConfigSubentryFlow):
"""Flow for managing conversation subentries."""
@property
def _is_new(self) -> bool:
"""Return if this is a new subentry."""
return self.source == "user"
async def async_step_set_options(
async def async_step_finish(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""Set conversation options."""
# abort if entry is not loaded
if self._get_entry().state != ConfigEntryState.LOADED:
return self.async_abort(reason="entry_not_loaded")
"""Step after model downloading has succeeded."""
assert self._config_data is not None
errors: dict[str, str] = {}
if user_input is None:
if self._is_new:
options = {}
else:
options = self._get_reconfigure_subentry().data.copy()
elif self._is_new:
# Model download completed, create/update the entry with stored config
if self._is_new:
return self.async_create_entry(
title=user_input.pop(CONF_NAME),
data=user_input,
title=self._name,
data=self._config_data,
)
else:
return self.async_update_and_abort(
self._get_entry(),
self._get_reconfigure_subentry(),
data=user_input,
)
schema = ollama_config_option_schema(self.hass, self._is_new, options)
return self.async_show_form(
step_id="set_options", data_schema=vol.Schema(schema), errors=errors
return self.async_update_and_abort(
self._get_entry(),
self._get_reconfigure_subentry(),
data=self._config_data,
)
async_step_user = async_step_set_options
@@ -273,19 +298,14 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
def ollama_config_option_schema(
hass: HomeAssistant, is_new: bool, options: Mapping[str, Any]
hass: HomeAssistant,
is_new: bool,
options: Mapping[str, Any],
models_to_list: list[SelectOptionDict],
) -> dict:
"""Ollama options schema."""
hass_apis: list[SelectOptionDict] = [
SelectOptionDict(
label=api.name,
value=api.id,
)
for api in llm.async_get_apis(hass)
]
if is_new:
schema: dict[vol.Required | vol.Optional, Any] = {
schema: dict = {
vol.Required(CONF_NAME, default="Ollama Conversation"): str,
}
else:
@@ -293,6 +313,12 @@ def ollama_config_option_schema(
schema.update(
{
vol.Required(
CONF_MODEL,
description={"suggested_value": options.get(CONF_MODEL, DEFAULT_MODEL)},
): SelectSelector(
SelectSelectorConfig(options=models_to_list, custom_value=True)
),
vol.Optional(
CONF_PROMPT,
description={
@@ -304,7 +330,18 @@ def ollama_config_option_schema(
vol.Optional(
CONF_LLM_HASS_API,
description={"suggested_value": options.get(CONF_LLM_HASS_API)},
): SelectSelector(SelectSelectorConfig(options=hass_apis, multiple=True)),
): SelectSelector(
SelectSelectorConfig(
options=[
SelectOptionDict(
label=api.name,
value=api.id,
)
for api in llm.async_get_apis(hass)
],
multiple=True,
)
),
vol.Optional(
CONF_NUM_CTX,
description={
@@ -350,11 +387,3 @@ def ollama_config_option_schema(
)
return schema
def _get_title(model: str) -> str:
"""Get title for config entry."""
if model.endswith(":latest"):
model = model.split(":", maxsplit=1)[0]
return model

View File

@@ -166,11 +166,14 @@ class OllamaBaseLLMEntity(Entity):
self.subentry = subentry
self._attr_name = subentry.title
self._attr_unique_id = subentry.subentry_id
model, _, version = subentry.data[CONF_MODEL].partition(":")
self._attr_device_info = dr.DeviceInfo(
identifiers={(DOMAIN, subentry.subentry_id)},
name=subentry.title,
manufacturer="Ollama",
model=entry.data[CONF_MODEL],
model=model,
sw_version=version or "latest",
entry_type=dr.DeviceEntryType.SERVICE,
)

View File

@@ -3,24 +3,17 @@
"step": {
"user": {
"data": {
"url": "[%key:common::config_flow::data::url%]",
"model": "Model"
"url": "[%key:common::config_flow::data::url%]"
}
},
"download": {
"title": "Downloading model"
}
},
"abort": {
"download_failed": "Model downloading failed",
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
},
"error": {
"invalid_url": "[%key:common::config_flow::error::invalid_host%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"progress": {
"download": "Please wait while the model is downloaded, which may take a very long time. Check your Ollama server logs for more details."
}
},
"config_subentries": {
@@ -33,6 +26,7 @@
"step": {
"set_options": {
"data": {
"model": "Model",
"name": "[%key:common::config_flow::data::name%]",
"prompt": "Instructions",
"llm_hass_api": "[%key:common::config_flow::data::llm_hass_api%]",
@@ -47,11 +41,19 @@
"num_ctx": "Maximum number of text tokens the model can process. Lower to reduce Ollama RAM, or increase for a large number of exposed entities.",
"think": "If enabled, the LLM will think before responding. This can improve response quality but may increase latency."
}
},
"download": {
"title": "Downloading model"
}
},
"abort": {
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"entry_not_loaded": "Cannot add things while the configuration is disabled."
"entry_not_loaded": "Failed to add agent. The configuration is disabled.",
"download_failed": "Model downloading failed",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"progress": {
"download": "Please wait while the model is downloaded, which may take a very long time. Check your Ollama server logs for more details."
}
}
}

View File

@@ -38,7 +38,7 @@ class PlaystationNetworkData:
presence: dict[str, Any] = field(default_factory=dict)
username: str = ""
account_id: str = ""
available: bool = False
availability: str = "unavailable"
active_sessions: dict[PlatformType, SessionData] = field(default_factory=dict)
registered_platforms: set[PlatformType] = field(default_factory=set)
trophy_summary: TrophySummary | None = None
@@ -92,10 +92,7 @@ class PlaystationNetwork:
data.username = self.user.online_id
data.account_id = self.user.account_id
data.available = (
data.presence.get("basicPresence", {}).get("availability")
== "availableToPlay"
)
data.availability = data.presence["basicPresence"]["availability"]
session = SessionData()
session.platform = PlatformType(
@@ -127,8 +124,6 @@ class PlaystationNetwork:
if (game_title_info := presence[0] if presence else {}) and game_title_info[
"onlineStatus"
] == "online":
data.available = True
platform = PlatformType(game_title_info["platform"])
if platform is PlatformType.PS4:

View File

@@ -29,6 +29,14 @@
},
"last_online": {
"default": "mdi:account-clock"
},
"online_status": {
"default": "mdi:account-badge",
"state": {
"busy": "mdi:account-cancel",
"availabletocommunicate": "mdi:cellphone",
"offline": "mdi:account-off-outline"
}
}
}
}

View File

@@ -107,7 +107,7 @@ class PsnMediaPlayerEntity(
"""Media Player state getter."""
session = self.coordinator.data.active_sessions.get(self.key)
if session and session.status == "online":
if self.coordinator.data.available and session.title_id is not None:
if session.title_id is not None:
return MediaPlayerState.PLAYING
return MediaPlayerState.ON
return MediaPlayerState.OFF

View File

@@ -37,6 +37,7 @@ class PlaystationNetworkSensorEntityDescription(SensorEntityDescription):
value_fn: Callable[[PlaystationNetworkData], StateType | datetime]
entity_picture: str | None = None
available_fn: Callable[[PlaystationNetworkData], bool] = lambda _: True
class PlaystationNetworkSensor(StrEnum):
@@ -50,6 +51,7 @@ class PlaystationNetworkSensor(StrEnum):
EARNED_TROPHIES_BRONZE = "earned_trophies_bronze"
ONLINE_ID = "online_id"
LAST_ONLINE = "last_online"
ONLINE_STATUS = "online_status"
SENSOR_DESCRIPTIONS: tuple[PlaystationNetworkSensorEntityDescription, ...] = (
@@ -117,8 +119,16 @@ SENSOR_DESCRIPTIONS: tuple[PlaystationNetworkSensorEntityDescription, ...] = (
psn.presence["basicPresence"]["lastAvailableDate"]
)
),
available_fn=lambda psn: "lastAvailableDate" in psn.presence["basicPresence"],
device_class=SensorDeviceClass.TIMESTAMP,
),
PlaystationNetworkSensorEntityDescription(
key=PlaystationNetworkSensor.ONLINE_STATUS,
translation_key=PlaystationNetworkSensor.ONLINE_STATUS,
value_fn=lambda psn: psn.availability.lower().replace("unavailable", "offline"),
device_class=SensorDeviceClass.ENUM,
options=["offline", "availabletoplay", "availabletocommunicate", "busy"],
),
)
@@ -183,3 +193,12 @@ class PlaystationNetworkSensorEntity(
)
return super().entity_picture
@property
def available(self) -> bool:
"""Return True if entity is available."""
return (
self.entity_description.available_fn(self.coordinator.data)
and super().available
)

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