Compare commits

...

167 Commits

Author SHA1 Message Date
farmio
7aea90c301 Move KNXModule class to separate module 2025-06-03 09:38:52 +02:00
epenet
5df05fb6dd Move async_register_services to async_setup (#146092) 2025-06-03 08:38:02 +02:00
Simone Chemelli
f295ca27af Bump aioamazondevices to 3.0.5 (#146073) 2025-06-03 01:18:49 +03:00
Marc Mueller
8f75cc6a33 Update pyatmo to 9.2.1 (#146077) 2025-06-02 23:47:50 +02:00
Marc Mueller
19c71f0f49 Update python-homewizard-energy to 8.3.3 (#146076) 2025-06-02 23:34:50 +02:00
Marc Mueller
22c2028c00 Update typing-extensions to 4.14.0 (#146054) 2025-06-02 23:15:53 +02:00
Ian
39f687e3a3 Bump ollama to 0.5.1 (#146063)
* Bump ollama to 0.5.1
* Add ollama to license exceptions
2025-06-02 22:43:00 +02:00
Shay Levy
6692b9b71f Fix Shelly BLU TRV calibrate button (#146066) 2025-06-02 22:38:17 +03:00
J. Nick Koston
2f5787e7be Bump aiohttp to 3.12.7 (#146028) 2025-06-02 21:27:08 +02:00
Simone Chemelli
bbda1761bf Avoid services unload for Isy994 (#146069)
* Avoid services unload for Isy994

* cleanup
2025-06-02 21:19:10 +02:00
Robert Resch
ecc10e9793 Bump go2rtc-client to 0.2.1 (#146019)
* Bump go2rtc-client to 0.2.0

* Bump go2rtc-client to 0.2.1

* Clean up hassfest exception

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-06-02 20:48:40 +02:00
Simone Chemelli
9e1e889fd7 Rename mispelled services python files (#146049) 2025-06-02 20:41:31 +02:00
Michael
eefe1e6f0f Don't use multi-line conditionals in immich (#146062) 2025-06-02 19:58:54 +02:00
Marc Mueller
397ed87f2d Update aiohomekit to 3.2.15 (#146059)
* Update aiohomekit to 3.2.15

* Remove Python version exception for homekit_controller
2025-06-02 18:23:04 +01:00
Marc Mueller
15830f383e Update pyoverkiz to 1.17.2 (#146056) 2025-06-02 18:21:26 +01:00
epenet
87395efc6e Add awesomeversion to dependency version checks (#146047) 2025-06-02 17:28:13 +02:00
Marc Mueller
27d79bb10a Update yamllint to 1.37.1 (#146038) 2025-06-02 16:35:31 +02:00
Simone Chemelli
7427db70aa Move async_setup_services to async_setup (#146048)
* Moved async_setup_services to async_setup

* fix schema missing
2025-06-02 16:23:20 +02:00
Marc Mueller
77d5bffa85 Update pytest warnings filter (#146024) 2025-06-02 16:01:23 +02:00
Marc Mueller
ab7c7b8d89 Update ruff to 0.11.12 (#146037)
* Update ruff to 0.11.12
* Replace ruff legacy alias with ruff-check
2025-06-02 16:01:10 +02:00
Simon Lamon
93b8cc38d8 Small nmbs sensor attributes refactoring (#145956)
Attributes refactoring
2025-06-02 15:13:23 +02:00
Pete Sage
e5f95b3aff Add diagnostics tests for Sonos (#146040)
* fix: add tests for diagnostics

* fix: add new files

* fix: add new files
2025-06-02 15:12:34 +02:00
starkillerOG
613728ad3b Improve debug logging Reolink (#146033)
Add debug logging
2025-06-02 15:12:13 +02:00
starkillerOG
cb1bfe6ebe Bump reolink-aio to 0.13.5 (#145974)
* Add debug logging

* Bump reolink-aio to 0.13.5

* Revert "Add debug logging"

This reverts commit f96030a6c8dccca7888b6d1274d5ed3a251ac03c.
2025-06-02 15:11:56 +02:00
Joost Lekkerkerker
434179ab3f Remove NMBS YAML import (#145733)
* Remove NMBS YAML import

* Remove NMBS YAML import
2025-06-02 15:10:46 +02:00
TimL
eb53277fcc Bump pysmlight to 0.2.6 (#146039)
Co-authored-by: Tim Lunn <tim@feathertop.org>
2025-06-02 15:04:34 +02:00
J. Nick Koston
850ddb3667 Bump grpcio to 1.72.1 (#146029) 2025-06-02 15:04:02 +02:00
epenet
5a727a4fa3 Avoid constant alias for integration DOMAIN (#145788)
* Avoid constant alias for integration DOMAIN

* Tweak

* Improve

* Three more

---------

Co-authored-by: Shay Levy <levyshay1@gmail.com>
2025-06-02 10:37:29 +02:00
karwosts
33fc700952 Make sun solar_rising a binary_sensor (#140956)
* Make sun solar_rising a binary_sensor.

* Add a state translation

* code review

* fix test

* move PLATFORMS

* Update strings.json
2025-06-02 10:32:48 +02:00
Joakim Sørensen
ad493e077e Submit legacy integrations for analytics (#145787)
* Submit legacy integrations for analytics

* adjustments
2025-06-02 10:29:17 +02:00
Marc Mueller
a2b2f6f20a Update pre-commit to 4.2.0 (#145986) 2025-06-02 09:56:20 +02:00
Marc Mueller
ee57fd413a Update freezegun to 1.5.2 (#145982) 2025-06-02 09:53:12 +02:00
Martin Hjelmare
f5d585e0f0 Fix removal of devices during Z-Wave migration (#145867) 2025-06-02 09:52:02 +02:00
Simone Chemelli
1899388f35 Add diagnostics to Amazon devices (#145964) 2025-06-02 09:48:42 +02:00
Allen Porter
4d833e9b1c Bump ical to 10.0.0 (#145954) 2025-06-02 09:47:05 +02:00
Robert Resch
6d827cd412 Deprecate hddtemp (#145850) 2025-06-02 09:45:14 +02:00
epenet
ebfbea39ff Use async_load_fixture in twitch tests (#146016) 2025-06-02 09:27:53 +02:00
dependabot[bot]
89a40f1c48 Bump dawidd6/action-download-artifact from 9 to 10 (#146015) 2025-06-02 09:21:26 +02:00
epenet
664eb7af10 Use async_load_fixture in moehlenhoff_alpha2 tests (#146012) 2025-06-02 08:59:19 +02:00
epenet
33b99b6627 Use async_load_fixture in netatmo tests (#146013) 2025-06-02 08:59:11 +02:00
epenet
0cf2ee0bcb Remove unnecessary DOMAIN alias in tests (l-r) (#146009)
* Remove unnecessary DOMAIN alias in tests (l-r)

* Keep late import in lirc
2025-06-02 08:54:55 +02:00
hanwg
85a86c3f11 Add config flow for telegram bot integration (#144617)
* added config flow for telegram integration

* added chat id in config entry title and added config flow tests

* fix import issue when there are no notifiers in configuration.yaml

* Revert "fix import issue when there are no notifiers in configuration.yaml"

This reverts commit b5b83e2a9a5d8cd1572f3e8c36e360b0de80b58b.

* Revert "added chat id in config entry title and added config flow tests"

This reverts commit 30c2bb4ae4d850dae931a5f7e1525cf19e3be5d8.

* Revert "added config flow for telegram integration"

This reverts commit 1f44afcd45e3a017b8c5f681dc39a160617018ce.

* added config and subentry flows

* added options flow to configure webhooks

* refactor module setup so it only load once

* moved service registration from async_setup_entry to async_setup

* Apply suggestions from code review

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

* import only last yaml config

* import only last yaml config

* reduced scope of try-block

* create issue when importing from yaml

* Apply suggestions from code review

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

* handle options update by reloading telegram bot

* handle import errors for create issue

* include bot's platform when creating issues

* handle options reload without needing HA restart

* moved url and trusted_networks inputs from options to new config flow step

* Apply suggestions from code review

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

* minor fixes

* refactor config flow

* moved constants to const.py

* Apply suggestions from code review

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

* Update homeassistant/components/telegram_bot/config_flow.py

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

* Update homeassistant/components/telegram_bot/config_flow.py

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

* Update homeassistant/components/telegram_bot/config_flow.py

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

* added options flow tests

* Update homeassistant/components/telegram_bot/__init__.py

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

* Update homeassistant/components/telegram_bot/__init__.py

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

* Update homeassistant/components/telegram_bot/__init__.py

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

* Update homeassistant/components/telegram_bot/config_flow.py

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

* Update homeassistant/components/telegram_bot/config_flow.py

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

* added reconfigure flow

* added reauth flow

* added tests for reconfigure flow

* added tests for reauth

* added tests for subentry flow

* added tests for user and webhooks flow with error scenarios

* added import flow tests

* handle webhook deregister exception

* added config entry id to all services

* fix leave chat bug

* Update homeassistant/components/telegram_bot/__init__.py

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

* removed leave chat bug fixes

* Update homeassistant/components/telegram_bot/strings.json

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

* handle other error types for import

* reuse translations

* added test for duplicated config entry for user step

* added tests

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-06-02 08:52:31 +02:00
epenet
de4a5fa30b Remove unnecessary DOMAIN alias in tests (s-z) (#146010) 2025-06-02 08:48:37 +02:00
Marc Mueller
43ac550ca0 Update pydantic to 2.11.5 (#145985)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-06-02 08:48:22 +02:00
Marc Mueller
c3c4d224b2 Update PyTurboJPEG to 1.8.0 (#145984)
Co-authored-by: Allen Porter <allen.porter@gmail.com>
2025-06-02 08:40:10 +02:00
Marc Mueller
6f865beacd Update attrs to 25.3.0 (#145977) 2025-06-02 07:58:35 +02:00
Marc Mueller
de25195383 Update bcrypt to 4.3.0 (#145978) 2025-06-02 07:56:51 +02:00
Marc Mueller
0139d2cabf Update cryptography to 45.0.3 (#145979) 2025-06-02 07:53:58 +02:00
Marc Mueller
17542614b5 Update aiohttp-cors to 0.8.1 (#145976)
* Update aiohttp-cors to 0.8.1

* Fix mypy
2025-06-02 07:52:23 +02:00
Marc Mueller
885367e690 Update coverage to 7.8.2 (#145983) 2025-06-02 07:47:56 +02:00
Marc Mueller
f8c44aad25 Update pytest-cov to 6.1.1 (#145989) 2025-06-02 07:34:11 +02:00
Marc Mueller
2323cc2869 Update numpy to 2.2.6 (#145981) 2025-06-01 21:23:30 -07:00
Marc Mueller
7f0249bbf7 Update pytest-timeout to 2.4.0 (#145990) 2025-06-02 06:17:39 +02:00
Marc Mueller
7a23b778a4 Update pytest-xdist to 3.7.0 (#145991) 2025-06-02 06:16:17 +02:00
Marc Mueller
d910924032 Update syrupy to 4.9.1 (#145992) 2025-06-02 06:14:52 +02:00
Marc Mueller
0b93a8c2f2 Update types packages (#145993) 2025-06-02 06:13:08 +02:00
Marc Mueller
5e377b89fc Update pytest-asyncio to 1.0.0 (#145988)
* Update pytest-asyncio to 1.0.0

* Remove event_loop fixture uses
2025-06-02 06:12:22 +02:00
Marc Mueller
dd85a1e5f0 Update mypy-dev to 1.17.0a2 (#146002)
* Update mypy-dev to 1.17.0a2

* Fix
2025-06-02 06:06:38 +02:00
Simone Chemelli
b96a7aebcd Bump aioamazondevices to 3.0.4 (#145971) 2025-06-01 21:15:18 +02:00
Michael
3cfcf382da Bump aioimmich to 0.8.0 (#145908) 2025-06-01 21:14:19 +02:00
epenet
ed9fd2c643 Use async_load_fixture in async test functions (b-i) (#145714)
* Use async_load_fixture in async test functions (b-i)

* Adjust
2025-06-01 06:31:37 -07:00
epenet
a007e8dc26 Use async_load_fixture in async test functions (l-z) (#145717)
* Use async_load_fixture in async test functions (l-z)

* Adjust
2025-06-01 06:29:17 -07:00
TimL
b318644998 Bump pysmlight to v0.2.5 (#145949) 2025-06-01 03:14:08 +02:00
Ståle Storø Hauknes
0434eea3ab Add sound pressure to Airthings (#145946)
Add sound pressure
2025-06-01 02:05:19 +02:00
Josef Zweck
c19b984660 Increase update intervals in lamarzocco (#145939) 2025-05-31 20:25:57 +02:00
Josef Zweck
0d6bb8a325 Bump pylamarzocco to 2.0.8 (#145938) 2025-05-31 20:25:47 +02:00
Joost Lekkerkerker
094b969301 Add more Amazon Devices DHCP matches (#145776) 2025-05-31 20:25:24 +02:00
Brett Adams
ddef6fdb98 Add streaming to charge cable connected in Teslemetry (#145880) 2025-05-31 20:01:10 +02:00
Robert Resch
cabf7860b3 Deprecate snips integration (#145784) 2025-05-31 20:00:34 +02:00
Bram Kragten
0c0a2403e5 Update frontend to 20250531.0 (#145933) 2025-05-31 17:54:36 +02:00
tronikos
be6c3d8bbd Bump opower to 0.12.3 (#145918) 2025-05-31 11:22:49 +02:00
Josef Zweck
c01536ee58 Move server device creation to init in jellyfin (#145910)
* Move server device creation to init in jellyfin

* move device creation to after coordinator refresh
2025-05-31 11:19:32 +02:00
J. Nick Koston
a9f36a50e4 Bump aiohttp to 3.12.6 (#145919)
* Bump aiohttp to 3.12.5

changelog: https://github.com/aio-libs/aiohttp/compare/v3.12.4...v3.12.5

* .6

* fix mock
2025-05-31 11:12:00 +02:00
Samuel Xiao
6d11c0395f Bump switchbot-api to 2.4.0 (#145786)
* update switchbot-api version to 2.4.0

* debug for test code
2025-05-30 20:22:40 +02:00
Brett Adams
66bb638dd0 Bump tesla-fleet-api to 1.1.1. (#145869)
bump
2025-05-30 20:21:51 +02:00
Iskra kranj
0d72bfef70 Bump pyiskra to 0.1.19 (#145889) 2025-05-30 20:21:14 +02:00
markhannon
6e44552d41 Minor cleanup of Zimi Integration (#144293) 2025-05-30 19:53:33 +02:00
Simon Lamon
9ec02633b3 Bump python-linkplay to v0.2.9 (#145892) 2025-05-30 19:35:08 +02:00
Jordan Harvey
5d340332bf Bump pyprobeplus to 1.0.1 (#145897) 2025-05-30 19:33:03 +02:00
J. Diego Rodríguez Royo
1e973c1d74 Bump aiohomeconnect to 0.17.1 (#145873) 2025-05-30 01:40:11 +02:00
starkillerOG
618ada64f8 Ensure Reolink host device is setup first (#145843) 2025-05-29 19:32:21 +02:00
Robert Resch
2d6802e06a Deprecate tensorflow (#145806)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-05-29 14:35:35 +01:00
starkillerOG
9687a34a70 Reolink fallback to download command for playback (#145842) 2025-05-29 15:31:50 +02:00
Michael
5ba0ceb6c2 Bump aioimmich to 0.7.0 (#145845) 2025-05-29 15:30:02 +02:00
G Johansson
d8e3e88c63 Fix language selections in workday (#145813) 2025-05-29 15:28:54 +02:00
Robert Resch
d1d1bca29d Deprecate sms integration (#145847) 2025-05-29 14:12:51 +02:00
Michael
80189495c5 Use mime type provided by Immich (#145830)
use mime type from immich instead of guessing it
2025-05-29 10:28:02 +02:00
Josef Zweck
cad6c72cfa Bump aiotedee to 0.2.23 (#145822)
* Bump aiotedee to 0.2.23

* update snapshot
2025-05-29 10:35:05 +03:00
J. Nick Koston
23ac22e213 Remove default args to ESPHome test fixture calls (#145840) 2025-05-29 01:45:37 -05:00
J. Nick Koston
55e664fc0d Bump aiohttp to 3.12.4 (#145838) 2025-05-28 21:08:01 -05:00
Brett Adams
881ce45afa Fix Tessie volume max and step (#145835)
* Use fixed volume max and step

* Update snapshot
2025-05-29 03:58:29 +02:00
André Lersveen
b80195df81 Set correct nobo_hub max temperature (#145751)
Max temperature 30°C is implemented upstream in pynobo and the Nobø Energy Hub app also stops at 30°C.
2025-05-29 03:52:05 +02:00
Matthew FitzGerald-Chamberlain
e57ce0a9df Bump pyaprilaire to 0.9.1 (#145836) 2025-05-29 03:43:28 +02:00
J. Nick Koston
ff66ad7705 Bump aiohttp to 3.12.3 (#145837) 2025-05-28 19:38:06 -05:00
Robert Resch
33e98ebffa Remove decora-wifi from excluded requirements (#145832) 2025-05-29 00:14:38 +02:00
Robert Resch
8fd9e2046e Deprecate decora integration (#145807) 2025-05-28 23:54:48 +02:00
Bram Kragten
32c2f47ab5 Update frontend to 20250528.0 (#145828)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-05-28 23:17:14 +02:00
Ståle Storø Hauknes
e2fc2dce84 Move Airthings coordinator to separate module (#145827)
* Create coordinator

* Fix sensor.py
2025-05-28 22:38:33 +02:00
Michael
afa97f8ec1 Add level of collections in Immich media source tree (#145734)
* add layer for collections in media source tree

* re-arange tests, add test for collection layer

* fix
2025-05-28 20:51:27 +02:00
Michael
2708c1c94c Fix Immich media source browsing with multiple config entries (#145823)
fix media source browsing with multiple config entries
2025-05-28 20:49:20 +02:00
Michael Hansen
d76ed6a3c2 Bump intents to 2025.5.28 (#145816) 2025-05-28 21:14:13 +03:00
epenet
695f69bd90 Remove unnecessary DOMAIN alias in tests (e-k) (#145818) 2025-05-28 21:06:25 +03:00
epenet
7da8e24e21 Remove unnecessary DOMAIN alias in tests (a-d) (#145817) 2025-05-28 21:00:38 +03:00
David Bonnes
9d0fc0d513 Fix HOMEASSISTANT_STOP unsubscribe in data update coordinator (#145809)
* initial commit

* a better approach

* Add comment
2025-05-28 17:52:51 +01:00
Robert Resch
ca567aa7fc Deprecate lirc integration (#145797) 2025-05-28 17:28:37 +01:00
Robert Resch
27af2d8ec6 Deprecate keyboard integration (#145805) 2025-05-28 17:22:18 +02:00
Lennart Nederstigt
59ea6f375a Add hardwired chime toggle to Reolink Battery Doorbell (#145779)
Co-authored-by: starkillerOG <starkiller.og@gmail.com>
2025-05-28 17:10:38 +02:00
Marc Mueller
6c365c94ed Update sqlalchemy to 2.0.41 (#145790) 2025-05-28 16:39:10 +02:00
Marc Mueller
6693fc764f Update httpcore to 1.0.9 and h11 to 0.16.0 (#145789) 2025-05-28 16:35:11 +02:00
starkillerOG
e855b6c2bc Bump reolink-aio to 0.13.4 (#145799) 2025-05-28 16:33:20 +02:00
Abílio Costa
23a1dddc23 Add Shelly zwave virtual integration (#145749) 2025-05-28 14:56:47 +01:00
epenet
bd5fef1ddb Use async_load_fixture in async test functions (a) (#145718) 2025-05-28 15:51:49 +02:00
epenet
c3ade400fb Use Platform constant in tests (#145801)
* Use Platform constant in tests

* spelling

* Fix platform
2025-05-28 15:51:37 +02:00
epenet
1889f0ef66 Use Platform constant in hue tests (#145798) 2025-05-28 14:43:48 +02:00
epenet
6b28af8282 Remove unnecessary DOMAIN alias in components (#145791) 2025-05-28 14:04:35 +02:00
Robert Resch
f59001d45f Deprecate pandora integration (#145785) 2025-05-28 13:12:55 +02:00
Erik Montnemery
a857461059 Handle late abort when creating subentry (#145765)
* Handle late abort when creating subentry

* Move error handling to the base class

* Narrow down expected error in test
2025-05-28 12:26:28 +02:00
epenet
e4cc842584 Use async_load_json_(array/object)_fixture in async test functions (#145773) 2025-05-28 12:09:05 +02:00
Robert Resch
bb52058920 Deprecate GStreamer integration (#145768) 2025-05-28 11:16:08 +02:00
J. Diego Rodríguez Royo
c1676570da Add more information about possible hostnames at Home Connect (#145770) 2025-05-28 10:57:01 +02:00
G Johansson
4858b2171e Modernize tests for smhi (#139334)
* Modernize tests for smhi

* Fixes

* Mods

* Fix weather

* Coverage 100%

* Fix init test

* Fixes

* Fixes

* Remove waits
2025-05-28 10:56:07 +02:00
Jan Bouwhuis
192aa76cd7 Ensure mqtt sensor unit of measurement validation for state class measurement_angle (#145648) 2025-05-28 10:16:40 +02:00
Josef Zweck
ddf611bfdf Fix uom for prebrew numbers in lamarzocco (#145772) 2025-05-28 10:15:24 +02:00
Robert Resch
3164394982 Deprecate dlib image processing integrations (#145767) 2025-05-28 09:58:44 +02:00
Josef Zweck
b250a03ff5 Bump pylamarzocco to 2.0.7 (#145763) 2025-05-28 09:39:33 +02:00
dependabot[bot]
2dd7f035f6 Bump docker/build-push-action from 6.17.0 to 6.18.0 (#145764)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-28 09:10:37 +02:00
Joost Lekkerkerker
2c08b3f30c Add more Amazon Devices DHCP matches (#145754) 2025-05-28 08:43:59 +02:00
Josef Zweck
c3ec30ce3b Update otp description for amazon_devices (#145701)
* Update otp description from amazon_devices

* separate

* Update strings.json
2025-05-28 08:13:28 +02:00
Erik Montnemery
9d4375ca76 Make async_remove_stale_devices_links_keep_entity_device move entities (#145719)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-05-27 23:00:52 +02:00
Raphael Hehl
3870b87db9 Bump uiprotect to version 7.10.1 (#145737)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2025-05-27 22:58:46 +02:00
Joost Lekkerkerker
ff2fd7e9ef Add DHCP discovery to LG ThinQ (#145746) 2025-05-27 16:45:30 -04:00
G Johansson
719dd09eb3 Fix dns resolver error in dnsip config flow validation (#145735)
Fix dns resolver error in dnsip
2025-05-27 22:17:34 +02:00
Bram Kragten
2cf2613dbd Update frontend to 20250527.0 (#145741) 2025-05-27 22:12:07 +02:00
Jan Bouwhuis
181a3d142e Revert "squeezebox Better result for testing (#144622)" (#145739)
This reverts commit 987af8f7df.
2025-05-27 21:36:51 +02:00
Elias Wernicke
c20ad5fde1 Add complete intent function for shopping list component (#128565)
* add intent

* add tests

* raise IntentHandleError

* add check for non completed

* Prefer completing non complete items

* cleanup

* cleanup tests

* rename test

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>

* remove duplicated test

* update test

* complete all items

* fix event

* remove type def

* return speech slots

---------

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-05-27 14:35:14 -05:00
Erwin Douna
4fcebf18dc Tado update mobile devices interval (#145738)
Update the mobile devices interval to five minutes
2025-05-27 21:27:52 +02:00
Joost Lekkerkerker
a6e04be076 Remove niko_home_control YAML import (#145732) 2025-05-27 19:58:05 +02:00
Erwin Douna
330a8e197d MELCloud remove deprecated YAML import strings (#145731)
Remove deprecated YAML import strings
2025-05-27 19:50:31 +02:00
Joost Lekkerkerker
4300e846e6 Fix unbound local variable in Acmeda config flow (#145729) 2025-05-27 19:29:04 +02:00
Kevin Stillhammer
07fd1f99df Support addresses with comma in google_travel_time (#145663)
Support addresses with comma
2025-05-27 18:53:45 +02:00
Kevin Stillhammer
481639bcf9 Catch PermissionDenied(Route API disabled) in google_travel_time (#145722)
Catch PermissionDenied(Route API disabled)
2025-05-27 18:45:49 +02:00
Martin Hjelmare
376008940b Disable advanced window cover position Matter sensor by default (#145713)
* Disable advanced window cover position Matter sensor by default

* Enanble disabled sensors in snapshot test
2025-05-27 17:46:21 +02:00
epenet
b2c2db3394 Add check for transient packages restricting Python version (#145695) 2025-05-27 17:45:51 +02:00
Kevin Stillhammer
a636e38d24 Debug log the update response in google_travel_time (#145725)
Debug log the update response
2025-05-27 17:44:48 +02:00
Martin Hjelmare
ae1294830c Remove static pin code length Matter sensors (#145711)
* Remove static Matter sensors

* Clean up translation strings
2025-05-27 17:35:11 +02:00
Robin Lintermann
d87fdf028b Improve smarla base entity (#145710) 2025-05-27 15:58:19 +02:00
Petar Petrov
6f5d5d4cdb Change text of installing and starting Z-WaveJs add-on steps (#145702) 2025-05-27 14:51:22 +02:00
epenet
12fdd7034a Simplify boolean check in onewire (#145700) 2025-05-27 13:30:44 +02:00
Martin Hjelmare
f295d72cd9 Fix error stack trace for HomeAssistantError in websocket service call (#145699)
* Add test

* Fix error stack trace for HomeAssistantError in websocket service call
2025-05-27 12:54:57 +02:00
Petar Petrov
2605fda185 Remove confirm screen after Z-Wave usb discovery (#145682)
* Remove confirm screen after Z-Wave usb discovery

* Simplify async_step_usb
2025-05-27 12:53:30 +02:00
Joost Lekkerkerker
2189dc3e2a Use string type for amazon devices OTP code (#145698) 2025-05-27 12:33:02 +02:00
Franck Nijhof
8364d8a2e3 Bump version to 2025.7.0dev0 (#145647)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-05-27 10:59:34 +02:00
epenet
96c9636086 Add check for packages restricting Python version (#145690)
* Add check for packages restricting Python version

* Apply suggestions from code review

* until

* until
2025-05-27 10:44:00 +02:00
Petar Petrov
7b1dfc35d1 Change description on recommended/custom Z-Wave install step (#145688)
Change description on recommended/custom Z-WaveJS step
2025-05-27 10:04:29 +02:00
Norbert Rittel
2e94730491 Replace "Invalid API key" with common string in overseerr (#145689)
Replace "Invalid API key" with common string
2025-05-27 09:56:16 +02:00
Markus Adrario
11c6998bf2 Add homee siren platform (#145675)
* port siren.py from custom component

* Add Siren Tests

* last small nits
2025-05-27 09:48:59 +02:00
epenet
055a024d10 Add async-timeout to forbidden packages (#145679) 2025-05-27 08:57:35 +02:00
Joost Lekkerkerker
f73afd71fd Fix Amazon devices offline handling (#145656) 2025-05-27 08:49:25 +02:00
Jan Bouwhuis
ec64194ab9 Fix justnimbus CI test (#145681) 2025-05-27 08:48:06 +02:00
karwosts
d49a613c62 Add read_only entity_id to Trend options flow (#145657) 2025-05-27 08:42:08 +02:00
Artur Pragacz
6fc064fa6a Test that recorder is not promoted to earlier stage in bootstrap (#142695)
Test that recorder is not promoted to earlier stage
2025-05-27 08:23:39 +02:00
Artur Pragacz
b36b591ccf Improve error message for global timeout (#141563)
* Improve error message for global timeout

* Add test

* Message works with zone too
2025-05-27 07:49:18 +02:00
J. Nick Koston
d25ba79427 Bump aiohttp to 3.12.2 (#145671) 2025-05-26 21:58:46 -05:00
Joost Lekkerkerker
df35f30321 Handle Google Nest DHCP flows (#145658)
* Handle Google Nest DHCP flows

* Handle Google Nest DHCP flows
2025-05-26 15:01:35 -07:00
Jan Bouwhuis
1e3d06a993 Fix translation for sensor measurement angle state class (#145649) 2025-05-26 22:47:53 +01:00
Florian von Garrel
2ee6bf7340 Add update platform to paperless integration (#145638)
* Add uüdate platform to paperless integration

* Add tests to paperless

* Add translation

* Fixed update unavailable

* Fetch remote version in update platform

* changed diagnostics

* changed diagnostic data

* Code quality

* revert changes

* code quality
2025-05-26 23:24:53 +02:00
Joost Lekkerkerker
13a8e5e021 Fix Aquacell snapshot (#145651) 2025-05-26 23:08:07 +02:00
645 changed files with 10689 additions and 5350 deletions

View File

@@ -94,7 +94,7 @@ jobs:
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@v9
uses: dawidd6/action-download-artifact@v10
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/frontend
@@ -105,7 +105,7 @@ jobs:
- name: Download nightly wheels of intents
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@v9
uses: dawidd6/action-download-artifact@v10
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/intents-package
@@ -509,7 +509,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
@@ -522,7 +522,7 @@ jobs:
- name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile

View File

@@ -40,7 +40,7 @@ env:
CACHE_VERSION: 2
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.6"
HA_SHORT_VERSION: "2025.7"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13']"
# 10.3 is the oldest supported version
@@ -360,7 +360,7 @@ jobs:
- name: Run ruff
run: |
. venv/bin/activate
pre-commit run --hook-stage manual ruff --all-files --show-diff-on-failure
pre-commit run --hook-stage manual ruff-check --all-files --show-diff-on-failure
env:
RUFF_OUTPUT_FORMAT: github

View File

@@ -1,8 +1,8 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.0
rev: v0.11.12
hooks:
- id: ruff
- id: ruff-check
args:
- --fix
- id: ruff-format
@@ -30,7 +30,7 @@ repos:
- --branch=master
- --branch=rc
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.35.1
rev: v1.37.1
hooks:
- id: yamllint
- repo: https://github.com/pre-commit/mirrors-prettier

2
.vscode/tasks.json vendored
View File

@@ -45,7 +45,7 @@
{
"label": "Ruff",
"type": "shell",
"command": "pre-commit run ruff --all-files",
"command": "pre-commit run ruff-check --all-files",
"group": {
"kind": "test",
"isDefault": true

View File

@@ -171,8 +171,6 @@ FRONTEND_INTEGRATIONS = {
# Stage 0 is divided into substages. Each substage has a name, a set of integrations and a timeout.
# The substage containing recorder should have no timeout, as it could cancel a database migration.
# Recorder freezes "recorder" timeout during a migration, but it does not freeze other timeouts.
# The substages preceding it should also have no timeout, until we ensure that the recorder
# is not accidentally promoted as a dependency of any of the integrations in them.
# If we add timeouts to the frontend substages, we should make sure they don't apply in recovery mode.
STAGE_0_INTEGRATIONS = (
# Load logging and http deps as soon as possible
@@ -929,7 +927,11 @@ async def _async_set_up_integrations(
await _async_setup_multi_components(hass, stage_all_domains, config)
continue
try:
async with hass.timeout.async_timeout(timeout, cool_down=COOLDOWN_TIME):
async with hass.timeout.async_timeout(
timeout,
cool_down=COOLDOWN_TIME,
cancel_message=f"Bootstrap stage {name} timeout",
):
await _async_setup_multi_components(hass, stage_all_domains, config)
except TimeoutError:
_LOGGER.warning(
@@ -941,7 +943,11 @@ async def _async_set_up_integrations(
# Wrap up startup
_LOGGER.debug("Waiting for startup to wrap up")
try:
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME):
async with hass.timeout.async_timeout(
WRAP_UP_TIMEOUT,
cool_down=COOLDOWN_TIME,
cancel_message="Bootstrap startup wrap up timeout",
):
await hass.async_block_till_done()
except TimeoutError:
_LOGGER.warning(

View File

@@ -0,0 +1,6 @@
{
"domain": "shelly",
"name": "shelly",
"integrations": ["shelly"],
"iot_standards": ["zwave"]
}

View File

@@ -40,9 +40,10 @@ class AcmedaFlowHandler(ConfigFlow, domain=DOMAIN):
entry.unique_id for entry in self._async_current_entries()
}
hubs: list[aiopulse.Hub] = []
with suppress(TimeoutError):
async with timeout(5):
hubs: list[aiopulse.Hub] = [
hubs = [
hub
async for hub in aiopulse.Hub.discover()
if hub.id not in already_configured

View File

@@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import (
)
from . import AgentDVRConfigEntry
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN as AGENT_DOMAIN
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN
SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)
@@ -82,7 +82,7 @@ class AgentCamera(MjpegCamera):
still_image_url=f"{device.client._server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", # noqa: SLF001
)
self._attr_device_info = DeviceInfo(
identifiers={(AGENT_DOMAIN, self.unique_id)},
identifiers={(DOMAIN, self.unique_id)},
manufacturer="Agent",
model="Camera",
name=f"{device.client.name} {device.name}",

View File

@@ -5,23 +5,22 @@ from __future__ import annotations
from datetime import timedelta
import logging
from airthings import Airthings, AirthingsDevice, AirthingsError
from airthings import Airthings
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_SECRET, DOMAIN
from .const import CONF_SECRET
from .coordinator import AirthingsDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR]
SCAN_INTERVAL = timedelta(minutes=6)
type AirthingsDataCoordinatorType = DataUpdateCoordinator[dict[str, AirthingsDevice]]
type AirthingsConfigEntry = ConfigEntry[AirthingsDataCoordinatorType]
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
@@ -32,21 +31,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
async_get_clientsession(hass),
)
async def _update_method() -> dict[str, AirthingsDevice]:
"""Get the latest data from Airthings."""
try:
return await airthings.update_devices() # type: ignore[no-any-return]
except AirthingsError as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err
coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_method=_update_method,
update_interval=SCAN_INTERVAL,
)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator

View File

@@ -0,0 +1,36 @@
"""The Airthings integration."""
from datetime import timedelta
import logging
from airthings import Airthings, AirthingsDevice, AirthingsError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=6)
class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]):
"""Coordinator for Airthings data updates."""
def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_method=self._update_method,
update_interval=SCAN_INTERVAL,
)
self.airthings = airthings
async def _update_method(self) -> dict[str, AirthingsDevice]:
"""Get the latest data from Airthings."""
try:
return await self.airthings.update_devices() # type: ignore[no-any-return]
except AirthingsError as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err

View File

@@ -19,6 +19,7 @@ from homeassistant.const import (
SIGNAL_STRENGTH_DECIBELS,
EntityCategory,
UnitOfPressure,
UnitOfSoundPressure,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
@@ -27,8 +28,9 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AirthingsConfigEntry, AirthingsDataCoordinatorType
from . import AirthingsConfigEntry
from .const import DOMAIN
from .coordinator import AirthingsDataUpdateCoordinator
SENSORS: dict[str, SensorEntityDescription] = {
"radonShortTermAvg": SensorEntityDescription(
@@ -54,6 +56,12 @@ SENSORS: dict[str, SensorEntityDescription] = {
native_unit_of_measurement=UnitOfPressure.MBAR,
state_class=SensorStateClass.MEASUREMENT,
),
"sla": SensorEntityDescription(
key="sla",
device_class=SensorDeviceClass.SOUND_PRESSURE,
native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
state_class=SensorStateClass.MEASUREMENT,
),
"battery": SensorEntityDescription(
key="battery",
device_class=SensorDeviceClass.BATTERY,
@@ -140,7 +148,7 @@ async def async_setup_entry(
class AirthingsHeaterEnergySensor(
CoordinatorEntity[AirthingsDataCoordinatorType], SensorEntity
CoordinatorEntity[AirthingsDataUpdateCoordinator], SensorEntity
):
"""Representation of a Airthings Sensor device."""
@@ -149,7 +157,7 @@ class AirthingsHeaterEnergySensor(
def __init__(
self,
coordinator: AirthingsDataCoordinatorType,
coordinator: AirthingsDataUpdateCoordinator,
airthings_device: AirthingsDevice,
entity_description: SensorEntityDescription,
) -> None:

View File

@@ -57,7 +57,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
): CountrySelector(),
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_CODE): cv.positive_int,
vol.Required(CONF_CODE): cv.string,
}
),
)

View File

@@ -0,0 +1,66 @@
"""Diagnostics support for Amazon Devices integration."""
from __future__ import annotations
from typing import Any
from aioamazondevices.api import AmazonDevice
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntry
from .coordinator import AmazonConfigEntry
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME, CONF_NAME, "title"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: AmazonConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data
devices: list[dict[str, dict[str, Any]]] = [
build_device_data(device) for device in coordinator.data.values()
]
return {
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
"device_info": {
"last_update success": coordinator.last_update_success,
"last_exception": repr(coordinator.last_exception),
"devices": devices,
},
}
async def async_get_device_diagnostics(
hass: HomeAssistant, entry: AmazonConfigEntry, device_entry: DeviceEntry
) -> dict[str, Any]:
"""Return diagnostics for a device."""
coordinator = entry.runtime_data
assert device_entry.serial_number
return build_device_data(coordinator.data[device_entry.serial_number])
def build_device_data(device: AmazonDevice) -> dict[str, Any]:
"""Build device data for diagnostics."""
return {
"account name": device.account_name,
"capabilities": device.capabilities,
"device family": device.device_family,
"device type": device.device_type,
"device cluster members": device.device_cluster_members,
"online": device.online,
"serial number": device.serial_number,
"software version": device.software_version,
"do not disturb": device.do_not_disturb,
"response style": device.response_style,
"bluetooth state": device.bluetooth_state,
}

View File

@@ -50,4 +50,8 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available and self._serial_num in self.coordinator.data
return (
super().available
and self._serial_num in self.coordinator.data
and self.device.online
)

View File

@@ -4,30 +4,119 @@
"codeowners": ["@chemelli74"],
"config_flow": true,
"dhcp": [
{ "macaddress": "007147*" },
{ "macaddress": "00FC8B*" },
{ "macaddress": "0812A5*" },
{ "macaddress": "086AE5*" },
{ "macaddress": "08849D*" },
{ "macaddress": "089115*" },
{ "macaddress": "08A6BC*" },
{ "macaddress": "08C224*" },
{ "macaddress": "0CDC91*" },
{ "macaddress": "0CEE99*" },
{ "macaddress": "1009F9*" },
{ "macaddress": "109693*" },
{ "macaddress": "10BF67*" },
{ "macaddress": "10CE02*" },
{ "macaddress": "140AC5*" },
{ "macaddress": "149138*" },
{ "macaddress": "1848BE*" },
{ "macaddress": "1C12B0*" },
{ "macaddress": "1C4D66*" },
{ "macaddress": "1C93C4*" },
{ "macaddress": "1CFE2B*" },
{ "macaddress": "244CE3*" },
{ "macaddress": "24CE33*" },
{ "macaddress": "2873F6*" },
{ "macaddress": "2C71FF*" },
{ "macaddress": "34AFB3*" },
{ "macaddress": "34D270*" },
{ "macaddress": "38F73D*" },
{ "macaddress": "3C5CC4*" },
{ "macaddress": "3CE441*" },
{ "macaddress": "440049*" },
{ "macaddress": "40A2DB*" },
{ "macaddress": "40A9CF*" },
{ "macaddress": "40B4CD*" },
{ "macaddress": "443D54*" },
{ "macaddress": "44650D*" },
{ "macaddress": "485F2D*" },
{ "macaddress": "48785E*" },
{ "macaddress": "48B423*" },
{ "macaddress": "4C1744*" },
{ "macaddress": "4CEFC0*" },
{ "macaddress": "5007C3*" },
{ "macaddress": "50D45C*" },
{ "macaddress": "50DCE7*" },
{ "macaddress": "50F5DA*" },
{ "macaddress": "5C415A*" },
{ "macaddress": "6837E9*" },
{ "macaddress": "6854FD*" },
{ "macaddress": "689A87*" },
{ "macaddress": "68B691*" },
{ "macaddress": "68DBF5*" },
{ "macaddress": "68F63B*" },
{ "macaddress": "6C0C9A*" },
{ "macaddress": "6C5697*" },
{ "macaddress": "7458F3*" },
{ "macaddress": "74C246*" },
{ "macaddress": "74D637*" },
{ "macaddress": "74E20C*" },
{ "macaddress": "74ECB2*" },
{ "macaddress": "786C84*" },
{ "macaddress": "78A03F*" },
{ "macaddress": "7C6166*" },
{ "macaddress": "7C6305*" },
{ "macaddress": "7CD566*" },
{ "macaddress": "8871E5*" },
{ "macaddress": "901195*" },
{ "macaddress": "90235B*" },
{ "macaddress": "90A822*" },
{ "macaddress": "90F82E*" },
{ "macaddress": "943A91*" },
{ "macaddress": "98226E*" },
{ "macaddress": "98CCF3*" },
{ "macaddress": "9CC8E9*" },
{ "macaddress": "A002DC*" },
{ "macaddress": "A0D2B1*" },
{ "macaddress": "A40801*" },
{ "macaddress": "A8E621*" },
{ "macaddress": "AC416A*" },
{ "macaddress": "AC63BE*" },
{ "macaddress": "ACCCFC*" },
{ "macaddress": "B0739C*" },
{ "macaddress": "B0CFCB*" },
{ "macaddress": "B0F7C4*" },
{ "macaddress": "B85F98*" },
{ "macaddress": "C091B9*" },
{ "macaddress": "C095CF*" },
{ "macaddress": "C49500*" },
{ "macaddress": "C86C3D*" },
{ "macaddress": "CC9EA2*" },
{ "macaddress": "CCF735*" },
{ "macaddress": "DC54D7*" },
{ "macaddress": "D8BE65*" },
{ "macaddress": "EC2BEB*" }
{ "macaddress": "D8FBD6*" },
{ "macaddress": "DC91BF*" },
{ "macaddress": "DCA0D0*" },
{ "macaddress": "E0F728*" },
{ "macaddress": "EC2BEB*" },
{ "macaddress": "EC8AC4*" },
{ "macaddress": "ECA138*" },
{ "macaddress": "F02F9E*" },
{ "macaddress": "F0272D*" },
{ "macaddress": "F0F0A4*" },
{ "macaddress": "F4032A*" },
{ "macaddress": "F854B8*" },
{ "macaddress": "FC492D*" },
{ "macaddress": "FC65DE*" },
{ "macaddress": "FCA183*" },
{ "macaddress": "FCE9D8*" }
],
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "bronze",
"requirements": ["aioamazondevices==2.1.1"]
"requirements": ["aioamazondevices==3.0.5"]
}

View File

@@ -5,7 +5,7 @@
"data_description_country": "The country of your Amazon account.",
"data_description_username": "The email address of your Amazon account.",
"data_description_password": "The password of your Amazon account.",
"data_description_code": "The one-time password sent to your email address."
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported."
},
"config": {
"flow_title": "{username}",

View File

@@ -24,7 +24,7 @@ from homeassistant.components.recorder import (
get_instance as get_recorder_instance,
)
from homeassistant.config_entries import SOURCE_IGNORE
from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
from homeassistant.const import ATTR_DOMAIN, BASE_PLATFORMS, __version__ as HA_VERSION
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
@@ -225,7 +225,8 @@ class Analytics:
LOGGER.error(err)
return
configuration_set = set(yaml_configuration)
configuration_set = _domains_from_yaml_config(yaml_configuration)
er_platforms = {
entity.platform
for entity in ent_reg.entities.values()
@@ -370,3 +371,13 @@ class Analytics:
for entry in entries
if entry.source != SOURCE_IGNORE and entry.disabled_by is None
)
def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]:
"""Extract domains from the YAML configuration."""
domains = set(yaml_configuration)
for platforms in conf_util.extract_platform_integrations(
yaml_configuration, BASE_PLATFORMS
).values():
domains.update(platforms)
return domains

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pyaprilaire"],
"requirements": ["pyaprilaire==0.9.0"]
"requirements": ["pyaprilaire==0.9.1"]
}

View File

@@ -11,7 +11,7 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
from homeassistant.helpers.dispatcher import async_dispatcher_send
from ..const import ATTR_MANUFACTURER, DOMAIN as AXIS_DOMAIN
from ..const import ATTR_MANUFACTURER, DOMAIN
from .config import AxisConfig
from .entity_loader import AxisEntityLoader
from .event_source import AxisEventSource
@@ -79,7 +79,7 @@ class AxisHub:
config_entry_id=self.config.entry.entry_id,
configuration_url=self.api.config.url,
connections={(CONNECTION_NETWORK_MAC, self.unique_id)},
identifiers={(AXIS_DOMAIN, self.unique_id)},
identifiers={(DOMAIN, self.unique_id)},
manufacturer=ATTR_MANUFACTURER,
model=f"{self.config.model} {self.product_type}",
name=self.config.name,

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/camera",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["PyTurboJPEG==1.7.5"]
"requirements": ["PyTurboJPEG==1.8.0"]
}

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/compensation",
"iot_class": "calculated",
"quality_scale": "legacy",
"requirements": ["numpy==2.2.2"]
"requirements": ["numpy==2.2.6"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.7"]
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.28"]
}

View File

@@ -9,7 +9,7 @@ from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT, CONF_ID
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from .const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN
from .const import CONF_GESTURE, DOMAIN
from .deconz_event import CONF_DECONZ_ALARM_EVENT, CONF_DECONZ_EVENT
from .device_trigger import (
CONF_BOTH_BUTTONS,
@@ -200,6 +200,6 @@ def async_describe_events(
}
async_describe_event(
DECONZ_DOMAIN, CONF_DECONZ_ALARM_EVENT, async_describe_deconz_alarm_event
DOMAIN, CONF_DECONZ_ALARM_EVENT, async_describe_deconz_alarm_event
)
async_describe_event(DECONZ_DOMAIN, CONF_DECONZ_EVENT, async_describe_deconz_event)
async_describe_event(DOMAIN, CONF_DECONZ_EVENT, async_describe_deconz_event)

View File

@@ -1 +1,3 @@
"""The decora component."""
DOMAIN = "decora"

View File

@@ -21,7 +21,11 @@ from homeassistant.components.light import (
LightEntity,
)
from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from . import DOMAIN
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
@@ -90,6 +94,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up an Decora switch."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Leviton Decora",
},
)
lights = []
for address, device_config in config[CONF_DEVICES].items():
device = {}

View File

@@ -1 +1,3 @@
"""The dlib_face_detect component."""
DOMAIN = "dlib_face_detect"

View File

@@ -11,10 +11,17 @@ from homeassistant.components.image_processing import (
ImageProcessingFaceEntity,
)
from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN,
HomeAssistant,
split_entity_id,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA
@@ -25,6 +32,20 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Dlib Face detection platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Dlib Face Detect",
},
)
source: list[dict[str, str]] = config[CONF_SOURCE]
add_entities(
DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME))

View File

@@ -1 +1,4 @@
"""The dlib_face_identify component."""
CONF_FACES = "faces"
DOMAIN = "dlib_face_identify"

View File

@@ -15,14 +15,20 @@ from homeassistant.components.image_processing import (
ImageProcessingFaceEntity,
)
from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN,
HomeAssistant,
split_entity_id,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import CONF_FACES, DOMAIN
_LOGGER = logging.getLogger(__name__)
CONF_FACES = "faces"
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend(
{
@@ -39,6 +45,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Dlib Face detection platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Dlib Face Identify",
},
)
confidence: float = config[CONF_CONFIDENCE]
faces: dict[str, str] = config[CONF_FACES]
source: list[dict[str, str]] = config[CONF_SOURCE]

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
import contextlib
from typing import Any
from typing import Any, Literal
import aiodns
from aiodns.error import DNSError
@@ -62,16 +62,16 @@ async def async_validate_hostname(
"""Validate hostname."""
async def async_check(
hostname: str, resolver: str, qtype: str, port: int = 53
hostname: str, resolver: str, qtype: Literal["A", "AAAA"], port: int = 53
) -> bool:
"""Return if able to resolve hostname."""
result = False
result: bool = False
with contextlib.suppress(DNSError):
result = bool(
await aiodns.DNSResolver( # type: ignore[call-overload]
nameservers=[resolver], udp_port=port, tcp_port=port
).query(hostname, qtype)
_resolver = aiodns.DNSResolver(
nameservers=[resolver], udp_port=port, tcp_port=port
)
result = bool(await _resolver.query(hostname, qtype))
return result
result: dict[str, bool] = {}

View File

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

View File

@@ -8,6 +8,6 @@
"integration_type": "system",
"iot_class": "local_polling",
"quality_scale": "internal",
"requirements": ["go2rtc-client==0.1.3b0"],
"requirements": ["go2rtc-client==0.2.1"],
"single_config_entry": true
}

View File

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

View File

@@ -24,9 +24,11 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Google Mail platform."""
"""Set up the Google Mail integration."""
hass.data.setdefault(DOMAIN, {})[DATA_HASS_CONFIG] = config
await async_setup_services(hass)
return True
@@ -52,8 +54,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoogleMailConfigEntry) -
entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
)
await async_setup_services(hass)
return True

View File

@@ -7,17 +7,26 @@ from google_photos_library_api.api import GooglePhotosLibraryApi
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from . import api
from .const import DOMAIN
from .coordinator import GooglePhotosConfigEntry, GooglePhotosUpdateCoordinator
from .services import async_register_services
__all__ = [
"DOMAIN",
]
__all__ = ["DOMAIN"]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Google Photos integration."""
async_register_services(hass)
return True
async def async_setup_entry(
@@ -48,8 +57,6 @@ async def async_setup_entry(
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
async_register_services(hass)
return True

View File

@@ -152,11 +152,10 @@ def async_register_services(hass: HomeAssistant) -> None:
}
return None
if not hass.services.has_service(DOMAIN, UPLOAD_SERVICE):
hass.services.async_register(
DOMAIN,
UPLOAD_SERVICE,
async_handle_upload,
schema=UPLOAD_SERVICE_SCHEMA,
supports_response=SupportsResponse.OPTIONAL,
)
hass.services.async_register(
DOMAIN,
UPLOAD_SERVICE,
async_handle_upload,
schema=UPLOAD_SERVICE_SCHEMA,
supports_response=SupportsResponse.OPTIONAL,
)

View File

@@ -50,7 +50,12 @@ from .const import (
UNITS_IMPERIAL,
UNITS_METRIC,
)
from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry
from .helpers import (
InvalidApiKeyException,
PermissionDeniedException,
UnknownException,
validate_config_entry,
)
RECONFIGURE_SCHEMA = vol.Schema(
{
@@ -188,6 +193,8 @@ async def validate_input(
user_input[CONF_ORIGIN],
user_input[CONF_DESTINATION],
)
except PermissionDeniedException:
return {"base": "permission_denied"}
except InvalidApiKeyException:
return {"base": "invalid_auth"}
except TimeoutError:

View File

@@ -7,6 +7,7 @@ from google.api_core.exceptions import (
Forbidden,
GatewayTimeout,
GoogleAPIError,
PermissionDenied,
Unauthorized,
)
from google.maps.routing_v2 import (
@@ -19,10 +20,18 @@ from google.maps.routing_v2 import (
from google.type import latlng_pb2
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.helpers.location import find_coordinates
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -37,7 +46,7 @@ def convert_to_waypoint(hass: HomeAssistant, location: str) -> Waypoint | None:
try:
formatted_coordinates = coordinates.split(",")
vol.Schema(cv.gps(formatted_coordinates))
except (AttributeError, vol.ExactSequenceInvalid):
except (AttributeError, vol.Invalid):
return Waypoint(address=location)
return Waypoint(
location=Location(
@@ -67,6 +76,9 @@ async def validate_config_entry(
await client.compute_routes(
request, metadata=[("x-goog-fieldmask", field_mask)]
)
except PermissionDenied as permission_error:
_LOGGER.error("Permission denied: %s", permission_error.message)
raise PermissionDeniedException from permission_error
except (Unauthorized, Forbidden) as unauthorized_error:
_LOGGER.error("Request denied: %s", unauthorized_error.message)
raise InvalidApiKeyException from unauthorized_error
@@ -84,3 +96,30 @@ class InvalidApiKeyException(Exception):
class UnknownException(Exception):
"""Unknown API Error."""
class PermissionDeniedException(Exception):
"""Permission Denied Error."""
def create_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Create an issue for the Routes API being disabled."""
async_create_issue(
hass,
DOMAIN,
f"routes_api_disabled_{entry.entry_id}",
learn_more_url="https://www.home-assistant.io/integrations/google_travel_time#setup",
is_fixable=False,
severity=IssueSeverity.ERROR,
translation_key="routes_api_disabled",
translation_placeholders={
"entry_title": entry.title,
"enable_api_url": "https://cloud.google.com/endpoints/docs/openapi/enable-api",
"api_key_restrictions_url": "https://cloud.google.com/docs/authentication/api-keys#adding-api-restrictions",
},
)
def delete_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Delete the issue for the Routes API being disabled."""
async_delete_issue(hass, DOMAIN, f"routes_api_disabled_{entry.entry_id}")

View File

@@ -7,7 +7,7 @@ import logging
from typing import TYPE_CHECKING, Any
from google.api_core.client_options import ClientOptions
from google.api_core.exceptions import GoogleAPIError
from google.api_core.exceptions import GoogleAPIError, PermissionDenied
from google.maps.routing_v2 import (
ComputeRoutesRequest,
Route,
@@ -58,7 +58,11 @@ from .const import (
TRAVEL_MODES_TO_GOOGLE_SDK_ENUM,
UNITS_TO_GOOGLE_SDK_ENUM,
)
from .helpers import convert_to_waypoint
from .helpers import (
convert_to_waypoint,
create_routes_api_disabled_issue,
delete_routes_api_disabled_issue,
)
_LOGGER = logging.getLogger(__name__)
@@ -271,8 +275,14 @@ class GoogleTravelTimeSensor(SensorEntity):
response = await self._client.compute_routes(
request, metadata=[("x-goog-fieldmask", FIELD_MASK)]
)
_LOGGER.debug("Received response: %s", response)
if response is not None and len(response.routes) > 0:
self._route = response.routes[0]
delete_routes_api_disabled_issue(self.hass, self._config_entry)
except PermissionDenied:
_LOGGER.error("Routes API is disabled for this API key")
create_routes_api_disabled_issue(self.hass, self._config_entry)
self._route = None
except GoogleAPIError as ex:
_LOGGER.error("Error getting travel time: %s", ex)
self._route = None

View File

@@ -21,6 +21,7 @@
}
},
"error": {
"permission_denied": "The Routes API is not enabled for this API key. Please see the setup instructions for detailed information.",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
@@ -100,5 +101,11 @@
"fewer_transfers": "Fewer transfers"
}
}
},
"issues": {
"routes_api_disabled": {
"title": "The Routes API must be enabled",
"description": "Your Google Travel Time integration `{entry_title}` uses an API key which does not have the Routes API enabled.\n\n Please follow the instructions to [enable the API for your project]({enable_api_url}) and make sure your [API key restrictions]({api_key_restrictions_url}) allow access to the Routes API.\n\n After enabling the API this issue will be resolved automatically."
}
}
}

View File

@@ -55,7 +55,7 @@ from homeassistant.helpers.issue_registry import (
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from .const import CONF_IGNORE_NON_NUMERIC, DOMAIN as GROUP_DOMAIN
from .const import CONF_IGNORE_NON_NUMERIC, DOMAIN
from .entity import GroupEntity
DEFAULT_NAME = "Sensor Group"
@@ -509,7 +509,7 @@ class SensorGroup(GroupEntity, SensorEntity):
return state_classes[0]
async_create_issue(
self.hass,
GROUP_DOMAIN,
DOMAIN,
f"{self.entity_id}_state_classes_not_matching",
is_fixable=False,
is_persistent=False,
@@ -566,7 +566,7 @@ class SensorGroup(GroupEntity, SensorEntity):
return device_classes[0]
async_create_issue(
self.hass,
GROUP_DOMAIN,
DOMAIN,
f"{self.entity_id}_device_classes_not_matching",
is_fixable=False,
is_persistent=False,
@@ -654,7 +654,7 @@ class SensorGroup(GroupEntity, SensorEntity):
if device_class:
async_create_issue(
self.hass,
GROUP_DOMAIN,
DOMAIN,
f"{self.entity_id}_uoms_not_matching_device_class",
is_fixable=False,
is_persistent=False,
@@ -670,7 +670,7 @@ class SensorGroup(GroupEntity, SensorEntity):
else:
async_create_issue(
self.hass,
GROUP_DOMAIN,
DOMAIN,
f"{self.entity_id}_uoms_not_matching_no_device_class",
is_fixable=False,
is_persistent=False,

View File

@@ -1 +1,3 @@
"""The gstreamer component."""
DOMAIN = "gstreamer"

View File

@@ -19,16 +19,18 @@ from homeassistant.components.media_player import (
async_process_play_media_url,
)
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
_LOGGER = logging.getLogger(__name__)
CONF_PIPELINE = "pipeline"
DOMAIN = "gstreamer"
PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
{vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string}
@@ -48,6 +50,20 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Gstreamer platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "GStreamer",
},
)
name = config.get(CONF_NAME)
pipeline = config.get(CONF_PIPELINE)

View File

@@ -1 +1,3 @@
"""The hddtemp component."""
DOMAIN = "hddtemp"

View File

@@ -22,11 +22,14 @@ from homeassistant.const import (
CONF_PORT,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
_LOGGER = logging.getLogger(__name__)
ATTR_DEVICE = "device"
@@ -56,6 +59,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the HDDTemp sensor."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "hddtemp",
},
)
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)

View File

@@ -10,17 +10,17 @@
"macaddress": "C8D778*"
},
{
"hostname": "(bosch|siemens)-*",
"hostname": "(balay|bosch|neff|siemens)-*",
"macaddress": "68A40E*"
},
{
"hostname": "siemens-*",
"hostname": "(siemens|neff)-*",
"macaddress": "38B4D3*"
}
],
"documentation": "https://www.home-assistant.io/integrations/home_connect",
"iot_class": "cloud_push",
"loggers": ["aiohomeconnect"],
"requirements": ["aiohomeconnect==0.17.0"],
"requirements": ["aiohomeconnect==0.17.1"],
"zeroconf": ["_homeconnect._tcp.local."]
}

View File

@@ -18,6 +18,10 @@
"title": "The {integration_title} YAML configuration is being removed",
"description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
},
"deprecated_system_packages_config_flow_integration": {
"title": "The {integration_title} integration is being removed",
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove all \"{integration_title}\" config entries to fix this issue."
},
"deprecated_system_packages_yaml_integration": {
"title": "The {integration_title} integration is being removed",
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."

View File

@@ -27,6 +27,7 @@ PLATFORMS = [
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SIREN,
Platform.SWITCH,
Platform.VALVE,
]

View File

@@ -0,0 +1,49 @@
"""The homee siren platform."""
from typing import Any
from pyHomee.const import AttributeType
from homeassistant.components.siren import SirenEntity, SirenEntityFeature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeeConfigEntry
from .entity import HomeeEntity
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_devices: AddConfigEntryEntitiesCallback,
) -> None:
"""Add siren entities for homee."""
async_add_devices(
HomeeSiren(attribute, config_entry)
for node in config_entry.runtime_data.nodes
for attribute in node.attributes
if attribute.type == AttributeType.SIREN
)
class HomeeSiren(HomeeEntity, SirenEntity):
"""Representation of a homee siren device."""
_attr_name = None
_attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF
@property
def is_on(self) -> bool:
"""Return the state of the siren."""
return self._attribute.current_value == 1.0
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the siren on."""
await self.async_set_homee_value(1)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the siren off."""
await self.async_set_homee_value(0)

View File

@@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"],
"requirements": ["aiohomekit==3.2.14"],
"requirements": ["aiohomekit==3.2.15"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
}

View File

@@ -63,6 +63,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
)
await async_setup_services(hass)
return True
@@ -83,7 +85,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomematicIPConfigEntry)
if not await hap.async_setup():
return False
await async_setup_services(hass)
_async_remove_obsolete_entities(hass, entry, hap)
# Register on HA stop event to gracefully shutdown HomematicIP Cloud connection

View File

@@ -12,6 +12,6 @@
"iot_class": "local_polling",
"loggers": ["homewizard_energy"],
"quality_scale": "platinum",
"requirements": ["python-homewizard-energy==v8.3.2"],
"requirements": ["python-homewizard-energy==8.3.3"],
"zeroconf": ["_hwenergy._tcp.local.", "_homewizard._tcp.local."]
}

View File

@@ -39,14 +39,14 @@ def setup_cors(app: Application, origins: list[str]) -> None:
cors = aiohttp_cors.setup(
app,
defaults={
host: aiohttp_cors.ResourceOptions(
host: aiohttp_cors.ResourceOptions( # type: ignore[no-untyped-call]
allow_headers=ALLOWED_CORS_HEADERS, allow_methods="*"
)
for host in origins
},
)
cors_added = set()
cors_added: set[str] = set()
def _allow_cors(
route: AbstractRoute | AbstractResource,
@@ -69,13 +69,13 @@ def setup_cors(app: Application, origins: list[str]) -> None:
if path_str in cors_added:
return
cors.add(route, config)
cors.add(route, config) # type: ignore[arg-type]
cors_added.add(path_str)
app[KEY_ALLOW_ALL_CORS] = lambda route: _allow_cors(
route,
{
"*": aiohttp_cors.ResourceOptions(
"*": aiohttp_cors.ResourceOptions( # type: ignore[no-untyped-call]
allow_headers=ALLOWED_CORS_HEADERS, allow_methods="*"
)
},

View File

@@ -5,13 +5,24 @@ from aiohue.util import normalize_bridge_id
from homeassistant.components import persistent_notification
from homeassistant.config_entries import SOURCE_IGNORE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.typing import ConfigType
from .bridge import HueBridge, HueConfigEntry
from .const import DOMAIN, SERVICE_HUE_ACTIVATE_SCENE
from .const import DOMAIN
from .migration import check_migration
from .services import async_register_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Hue integration."""
async_register_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool:
"""Set up a bridge from a config entry."""
@@ -23,9 +34,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool:
if not await bridge.async_initialize_bridge():
return False
# register Hue domain services
async_register_services(hass)
api = bridge.api
# For backwards compat
@@ -106,7 +114,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool:
"""Unload a config entry."""
unload_success = await entry.runtime_data.async_reset()
if not hass.config_entries.async_loaded_entries(DOMAIN):
hass.services.async_remove(DOMAIN, SERVICE_HUE_ACTIVATE_SCENE)
return unload_success
return await entry.runtime_data.async_reset()

View File

@@ -59,21 +59,20 @@ def async_register_services(hass: HomeAssistant) -> None:
group_name,
)
if not hass.services.has_service(DOMAIN, SERVICE_HUE_ACTIVATE_SCENE):
# Register a local handler for scene activation
hass.services.async_register(
DOMAIN,
SERVICE_HUE_ACTIVATE_SCENE,
verify_domain_control(hass, DOMAIN)(hue_activate_scene),
schema=vol.Schema(
{
vol.Required(ATTR_GROUP_NAME): cv.string,
vol.Required(ATTR_SCENE_NAME): cv.string,
vol.Optional(ATTR_TRANSITION): cv.positive_int,
vol.Optional(ATTR_DYNAMIC): cv.boolean,
}
),
)
# Register a local handler for scene activation
hass.services.async_register(
DOMAIN,
SERVICE_HUE_ACTIVATE_SCENE,
verify_domain_control(hass, DOMAIN)(hue_activate_scene),
schema=vol.Schema(
{
vol.Required(ATTR_GROUP_NAME): cv.string,
vol.Required(ATTR_SCENE_NAME): cv.string,
vol.Optional(ATTR_TRANSITION): cv.positive_int,
vol.Optional(ATTR_DYNAMIC): cv.boolean,
}
),
)
async def hue_activate_scene_v1(

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aioimmich"],
"quality_scale": "silver",
"requirements": ["aioimmich==0.6.0"]
"requirements": ["aioimmich==0.8.0"]
}

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
from logging import getLogger
import mimetypes
from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse
from aioimmich.exceptions import ImmichError
@@ -30,11 +29,8 @@ LOGGER = getLogger(__name__)
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
"""Set up Immich media source."""
entries = hass.config_entries.async_entries(
DOMAIN, include_disabled=False, include_ignore=False
)
hass.http.register_view(ImmichMediaView(hass))
return ImmichMediaSource(hass, entries)
return ImmichMediaSource(hass)
class ImmichMediaSourceIdentifier:
@@ -42,12 +38,14 @@ class ImmichMediaSourceIdentifier:
def __init__(self, identifier: str) -> None:
"""Split identifier into parts."""
parts = identifier.split("/")
# coonfig_entry.unique_id/album_id/asset_it/filename
parts = identifier.split("|")
# config_entry.unique_id|collection|collection_id|asset_id|file_name|mime_type
self.unique_id = parts[0]
self.album_id = parts[1] if len(parts) > 1 else None
self.asset_id = parts[2] if len(parts) > 2 else None
self.file_name = parts[3] if len(parts) > 2 else None
self.collection = parts[1] if len(parts) > 1 else None
self.collection_id = parts[2] if len(parts) > 2 else None
self.asset_id = parts[3] if len(parts) > 3 else None
self.file_name = parts[4] if len(parts) > 3 else None
self.mime_type = parts[5] if len(parts) > 3 else None
class ImmichMediaSource(MediaSource):
@@ -55,18 +53,17 @@ class ImmichMediaSource(MediaSource):
name = "Immich"
def __init__(self, hass: HomeAssistant, entries: list[ConfigEntry]) -> None:
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize Immich media source."""
super().__init__(DOMAIN)
self.hass = hass
self.entries = entries
async def async_browse_media(
self,
item: MediaSourceItem,
) -> BrowseMediaSource:
"""Return media."""
if not self.hass.config_entries.async_loaded_entries(DOMAIN):
if not (entries := self.hass.config_entries.async_loaded_entries(DOMAIN)):
raise BrowseError("Immich is not configured")
return BrowseMediaSource(
domain=DOMAIN,
@@ -78,15 +75,16 @@ class ImmichMediaSource(MediaSource):
can_expand=True,
children_media_class=MediaClass.DIRECTORY,
children=[
*await self._async_build_immich(item),
*await self._async_build_immich(item, entries),
],
)
async def _async_build_immich(
self, item: MediaSourceItem
self, item: MediaSourceItem, entries: list[ConfigEntry]
) -> list[BrowseMediaSource]:
"""Handle browsing different immich instances."""
if not item.identifier:
LOGGER.debug("Render all Immich instances")
return [
BrowseMediaSource(
domain=DOMAIN,
@@ -97,7 +95,7 @@ class ImmichMediaSource(MediaSource):
can_play=False,
can_expand=True,
)
for entry in self.entries
for entry in entries
]
identifier = ImmichMediaSourceIdentifier(item.identifier)
entry: ImmichConfigEntry | None = (
@@ -108,8 +106,22 @@ class ImmichMediaSource(MediaSource):
assert entry
immich_api = entry.runtime_data.api
if identifier.album_id is None:
# Get Albums
if identifier.collection is None:
LOGGER.debug("Render all collections for %s", entry.title)
return [
BrowseMediaSource(
domain=DOMAIN,
identifier=f"{identifier.unique_id}|albums",
media_class=MediaClass.DIRECTORY,
media_content_type=MediaClass.IMAGE,
title="albums",
can_play=False,
can_expand=True,
)
]
if identifier.collection_id is None:
LOGGER.debug("Render all albums for %s", entry.title)
try:
albums = await immich_api.albums.async_get_all_albums()
except ImmichError:
@@ -118,80 +130,85 @@ class ImmichMediaSource(MediaSource):
return [
BrowseMediaSource(
domain=DOMAIN,
identifier=f"{item.identifier}/{album.album_id}",
identifier=f"{identifier.unique_id}|albums|{album.album_id}",
media_class=MediaClass.DIRECTORY,
media_content_type=MediaClass.IMAGE,
title=album.name,
title=album.album_name,
can_play=False,
can_expand=True,
thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumb.jpg/thumbnail",
thumbnail=f"/immich/{identifier.unique_id}/{album.album_thumbnail_asset_id}/thumbnail/image/jpg",
)
for album in albums
]
# Request items of album
LOGGER.debug(
"Render all assets of album %s for %s",
identifier.collection_id,
entry.title,
)
try:
album_info = await immich_api.albums.async_get_album_info(
identifier.album_id
identifier.collection_id
)
except ImmichError:
return []
ret = [
BrowseMediaSource(
domain=DOMAIN,
identifier=(
f"{identifier.unique_id}/"
f"{identifier.album_id}/"
f"{asset.asset_id}/"
f"{asset.file_name}"
),
media_class=MediaClass.IMAGE,
media_content_type=asset.mime_type,
title=asset.file_name,
can_play=False,
can_expand=False,
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/{asset.file_name}/thumbnail",
)
for asset in album_info.assets
if asset.mime_type.startswith("image/")
]
ret: list[BrowseMediaSource] = []
for asset in album_info.assets:
if not (mime_type := asset.original_mime_type) or not mime_type.startswith(
("image/", "video/")
):
continue
ret.extend(
BrowseMediaSource(
domain=DOMAIN,
identifier=(
f"{identifier.unique_id}/"
f"{identifier.album_id}/"
f"{asset.asset_id}/"
f"{asset.file_name}"
),
media_class=MediaClass.VIDEO,
media_content_type=asset.mime_type,
title=asset.file_name,
can_play=True,
can_expand=False,
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail.jpg/thumbnail",
if mime_type.startswith("image/"):
media_class = MediaClass.IMAGE
can_play = False
thumb_mime_type = mime_type
else:
media_class = MediaClass.VIDEO
can_play = True
thumb_mime_type = "image/jpeg"
ret.append(
BrowseMediaSource(
domain=DOMAIN,
identifier=(
f"{identifier.unique_id}|albums|"
f"{identifier.collection_id}|"
f"{asset.asset_id}|"
f"{asset.original_file_name}|"
f"{mime_type}"
),
media_class=media_class,
media_content_type=mime_type,
title=asset.original_file_name,
can_play=can_play,
can_expand=False,
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{thumb_mime_type}",
)
)
for asset in album_info.assets
if asset.mime_type.startswith("video/")
)
return ret
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
"""Resolve media to a url."""
identifier = ImmichMediaSourceIdentifier(item.identifier)
if identifier.file_name is None:
raise Unresolvable("No file name")
mime_type, _ = mimetypes.guess_type(identifier.file_name)
if not isinstance(mime_type, str):
raise Unresolvable("No file extension")
try:
identifier = ImmichMediaSourceIdentifier(item.identifier)
except IndexError as err:
raise Unresolvable(
f"Could not parse identifier: {item.identifier}"
) from err
if identifier.mime_type is None:
raise Unresolvable(
f"Could not resolve identifier that has no mime-type: {item.identifier}"
)
return PlayMedia(
(
f"/immich/{identifier.unique_id}/{identifier.asset_id}/{identifier.file_name}/fullsize"
f"/immich/{identifier.unique_id}/{identifier.asset_id}/fullsize/{identifier.mime_type}"
),
mime_type,
identifier.mime_type,
)
@@ -212,10 +229,10 @@ class ImmichMediaView(HomeAssistantView):
if not self.hass.config_entries.async_loaded_entries(DOMAIN):
raise HTTPNotFound
asset_id, file_name, size = location.split("/")
mime_type, _ = mimetypes.guess_type(file_name)
if not isinstance(mime_type, str):
raise HTTPNotFound
try:
asset_id, size, mime_type_base, mime_type_format = location.split("/")
except ValueError as err:
raise HTTPNotFound from err
entry: ImmichConfigEntry | None = (
self.hass.config_entries.async_entry_for_domain_unique_id(
@@ -226,7 +243,7 @@ class ImmichMediaView(HomeAssistantView):
immich_api = entry.runtime_data.api
# stream response for videos
if mime_type.startswith("video/"):
if mime_type_base == "video":
try:
resp = await immich_api.assets.async_play_video_stream(asset_id)
except ImmichError as exc:
@@ -243,4 +260,4 @@ class ImmichMediaView(HomeAssistantView):
image = await immich_api.assets.async_view_asset(asset_id, size)
except ImmichError as exc:
raise HTTPNotFound from exc
return Response(body=image, content_type=mime_type)
return Response(body=image, content_type=f"{mime_type_base}/{mime_type_format}")

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["pyiqvia"],
"requirements": ["numpy==2.2.2", "pyiqvia==2022.04.0"]
"requirements": ["numpy==2.2.6", "pyiqvia==2022.04.0"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["pyiskra"],
"requirements": ["pyiskra==0.1.15"]
"requirements": ["pyiskra==0.1.19"]
}

View File

@@ -26,6 +26,7 @@ from homeassistant.helpers import (
device_registry as dr,
)
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.typing import ConfigType
from .const import (
_LOGGER,
@@ -46,7 +47,7 @@ from .const import (
)
from .helpers import _categorize_nodes, _categorize_programs
from .models import IsyConfigEntry, IsyData
from .services import async_setup_services, async_unload_services
from .services import async_setup_services
from .util import _async_cleanup_registry_entries
CONFIG_SCHEMA = vol.Schema(
@@ -55,6 +56,14 @@ CONFIG_SCHEMA = vol.Schema(
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the ISY 994 integration."""
async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: IsyConfigEntry) -> bool:
"""Set up the ISY 994 integration."""
isy_config = entry.data
@@ -167,9 +176,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: IsyConfigEntry) -> bool:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_auto_update)
)
# Register Integration-wide Services:
async_setup_services(hass)
return True
@@ -221,9 +227,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: IsyConfigEntry) -> bool
_LOGGER.debug("ISY Stopping Event Stream and automatic updates")
entry.runtime_data.root.websocket.stop()
if not hass.config_entries.async_loaded_entries(DOMAIN):
async_unload_services(hass)
return unload_ok

View File

@@ -137,10 +137,6 @@ def async_get_entities(hass: HomeAssistant) -> dict[str, Entity]:
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Create and register services for the ISY integration."""
existing_services = hass.services.async_services_for_domain(DOMAIN)
if existing_services and SERVICE_SEND_PROGRAM_COMMAND in existing_services:
# Integration-level services have already been added. Return.
return
async def async_send_program_command_service_handler(service: ServiceCall) -> None:
"""Handle a send program command service call."""
@@ -230,18 +226,3 @@ def async_setup_services(hass: HomeAssistant) -> None:
schema=cv.make_entity_service_schema(SERVICE_RENAME_NODE_SCHEMA),
service_func=_async_rename_node,
)
@callback
def async_unload_services(hass: HomeAssistant) -> None:
"""Unload services for the ISY integration."""
existing_services = hass.services.async_services_for_domain(DOMAIN)
if not existing_services or SERVICE_SEND_PROGRAM_COMMAND not in existing_services:
return
_LOGGER.debug("Unloading ISY994 Services")
hass.services.async_remove(domain=DOMAIN, service=SERVICE_SEND_PROGRAM_COMMAND)
hass.services.async_remove(domain=DOMAIN, service=SERVICE_SEND_RAW_NODE_COMMAND)
hass.services.async_remove(domain=DOMAIN, service=SERVICE_SEND_NODE_COMMAND)
hass.services.async_remove(domain=DOMAIN, service=SERVICE_GET_ZWAVE_PARAMETER)
hass.services.async_remove(domain=DOMAIN, service=SERVICE_SET_ZWAVE_PARAMETER)

View File

@@ -7,7 +7,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input
from .const import CONF_CLIENT_DEVICE_ID, DOMAIN, PLATFORMS
from .const import CONF_CLIENT_DEVICE_ID, DEFAULT_NAME, DOMAIN, PLATFORMS
from .coordinator import JellyfinConfigEntry, JellyfinDataUpdateCoordinator
@@ -35,9 +35,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: JellyfinConfigEntry) ->
coordinator = JellyfinDataUpdateCoordinator(
hass, entry, client, server_info, user_id
)
await coordinator.async_config_entry_first_refresh()
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
entry_type=dr.DeviceEntryType.SERVICE,
identifiers={(DOMAIN, coordinator.server_id)},
manufacturer=DEFAULT_NAME,
name=coordinator.server_name,
sw_version=coordinator.server_version,
)
entry.runtime_data = coordinator
entry.async_on_unload(client.stop)

View File

@@ -4,10 +4,10 @@ from __future__ import annotations
from typing import Any
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DEFAULT_NAME, DOMAIN
from .const import DOMAIN
from .coordinator import JellyfinDataUpdateCoordinator
@@ -24,11 +24,7 @@ class JellyfinServerEntity(JellyfinEntity):
"""Initialize the Jellyfin entity."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, coordinator.server_id)},
manufacturer=DEFAULT_NAME,
name=coordinator.server_name,
sw_version=coordinator.server_version,
)

View File

@@ -30,7 +30,7 @@ from .const import (
DOMAIN,
)
from .entity import JewishCalendarConfigEntry, JewishCalendarData
from .service import async_setup_services
from .services import async_setup_services
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]

View File

@@ -11,8 +11,9 @@ from homeassistant.const import (
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_UP,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType
DOMAIN = "keyboard"
@@ -24,6 +25,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Listen for keyboard events."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Keyboard",
},
)
keyboard = PyKeyboard()
keyboard.special_key_assignment()

View File

@@ -1,34 +1,17 @@
"""Support KNX devices."""
"""The KNX integration."""
from __future__ import annotations
import contextlib
import logging
from pathlib import Path
from typing import Final
import voluptuous as vol
from xknx import XKNX
from xknx.core import XknxConnectionState
from xknx.core.state_updater import StateTrackerType, TrackerOptions
from xknx.core.telegram_queue import TelegramQueue
from xknx.dpt import DPTBase
from xknx.exceptions import ConversionError, CouldNotParseTelegram, XKNXException
from xknx.io import ConnectionConfig, ConnectionType, SecureConfig
from xknx.telegram import AddressFilter, Telegram
from xknx.telegram.address import DeviceGroupAddress, GroupAddress, InternalGroupAddress
from xknx.telegram.apci import GroupValueResponse, GroupValueWrite
from xknx.exceptions import XKNXException
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_EVENT,
CONF_HOST,
CONF_PORT,
CONF_TYPE,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.reload import async_integration_yaml_config
@@ -36,40 +19,17 @@ from homeassistant.helpers.storage import STORAGE_DIR
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_KNX_CONNECTION_TYPE,
CONF_KNX_EXPOSE,
CONF_KNX_INDIVIDUAL_ADDRESS,
CONF_KNX_KNXKEY_FILENAME,
CONF_KNX_KNXKEY_PASSWORD,
CONF_KNX_LOCAL_IP,
CONF_KNX_MCAST_GRP,
CONF_KNX_MCAST_PORT,
CONF_KNX_RATE_LIMIT,
CONF_KNX_ROUTE_BACK,
CONF_KNX_ROUTING,
CONF_KNX_ROUTING_BACKBONE_KEY,
CONF_KNX_ROUTING_SECURE,
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE,
CONF_KNX_SECURE_DEVICE_AUTHENTICATION,
CONF_KNX_SECURE_USER_ID,
CONF_KNX_SECURE_USER_PASSWORD,
CONF_KNX_STATE_UPDATER,
CONF_KNX_TELEGRAM_LOG_SIZE,
CONF_KNX_TUNNEL_ENDPOINT_IA,
CONF_KNX_TUNNELING,
CONF_KNX_TUNNELING_TCP,
CONF_KNX_TUNNELING_TCP_SECURE,
DATA_HASS_CONFIG,
DOMAIN,
KNX_ADDRESS,
KNX_MODULE_KEY,
SUPPORTED_PLATFORMS_UI,
SUPPORTED_PLATFORMS_YAML,
TELEGRAM_LOG_DEFAULT,
)
from .device import KNXInterfaceDevice
from .expose import KNXExposeSensor, KNXExposeTime, create_knx_exposure
from .project import STORAGE_KEY as PROJECT_STORAGE_KEY, KNXProject
from .expose import create_knx_exposure
from .knx_module import KNXModule
from .project import STORAGE_KEY as PROJECT_STORAGE_KEY
from .schema import (
BinarySensorSchema,
ButtonSchema,
@@ -92,12 +52,10 @@ from .schema import (
WeatherSchema,
)
from .services import register_knx_services
from .storage.config_store import STORAGE_KEY as CONFIG_STORAGE_KEY, KNXConfigStore
from .telegrams import STORAGE_KEY as TELEGRAMS_STORAGE_KEY, Telegrams
from .storage.config_store import STORAGE_KEY as CONFIG_STORAGE_KEY
from .telegrams import STORAGE_KEY as TELEGRAMS_STORAGE_KEY
from .websocket import register_panel
_LOGGER = logging.getLogger(__name__)
_KNX_YAML_CONFIG: Final = "knx_yaml_config"
CONFIG_SCHEMA = vol.Schema(
@@ -162,6 +120,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[KNX_MODULE_KEY] = knx_module
entry.async_on_unload(entry.add_update_listener(async_update_entry))
if CONF_KNX_EXPOSE in config:
for expose_config in config[CONF_KNX_EXPOSE]:
knx_module.exposures.append(
@@ -255,243 +215,3 @@ async def async_remove_config_entry_device(
if entity.device_id == device_entry.id:
await knx_module.config_store.delete_entity(entity.entity_id)
return True
class KNXModule:
"""Representation of KNX Object."""
def __init__(
self, hass: HomeAssistant, config: ConfigType, entry: ConfigEntry
) -> None:
"""Initialize KNX module."""
self.hass = hass
self.config_yaml = config
self.connected = False
self.exposures: list[KNXExposeSensor | KNXExposeTime] = []
self.service_exposures: dict[str, KNXExposeSensor | KNXExposeTime] = {}
self.entry = entry
self.project = KNXProject(hass=hass, entry=entry)
self.config_store = KNXConfigStore(hass=hass, config_entry=entry)
default_state_updater = (
TrackerOptions(tracker_type=StateTrackerType.EXPIRE, update_interval_min=60)
if self.entry.data[CONF_KNX_STATE_UPDATER]
else TrackerOptions(
tracker_type=StateTrackerType.INIT, update_interval_min=60
)
)
self.xknx = XKNX(
address_format=self.project.get_address_format(),
connection_config=self.connection_config(),
rate_limit=self.entry.data[CONF_KNX_RATE_LIMIT],
state_updater=default_state_updater,
)
self.xknx.connection_manager.register_connection_state_changed_cb(
self.connection_state_changed_cb
)
self.telegrams = Telegrams(
hass=hass,
xknx=self.xknx,
project=self.project,
log_size=entry.data.get(CONF_KNX_TELEGRAM_LOG_SIZE, TELEGRAM_LOG_DEFAULT),
)
self.interface_device = KNXInterfaceDevice(
hass=hass, entry=entry, xknx=self.xknx
)
self._address_filter_transcoder: dict[AddressFilter, type[DPTBase]] = {}
self.group_address_transcoder: dict[DeviceGroupAddress, type[DPTBase]] = {}
self.knx_event_callback: TelegramQueue.Callback = self.register_event_callback()
self.entry.async_on_unload(
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
)
self.entry.async_on_unload(self.entry.add_update_listener(async_update_entry))
async def start(self) -> None:
"""Start XKNX object. Connect to tunneling or Routing device."""
await self.project.load_project(self.xknx)
await self.config_store.load_data()
await self.telegrams.load_history()
await self.xknx.start()
async def stop(self, event: Event | None = None) -> None:
"""Stop XKNX object. Disconnect from tunneling or Routing device."""
await self.xknx.stop()
await self.telegrams.save_history()
def connection_config(self) -> ConnectionConfig:
"""Return the connection_config."""
_conn_type: str = self.entry.data[CONF_KNX_CONNECTION_TYPE]
_knxkeys_file: str | None = (
self.hass.config.path(
STORAGE_DIR,
self.entry.data[CONF_KNX_KNXKEY_FILENAME],
)
if self.entry.data.get(CONF_KNX_KNXKEY_FILENAME) is not None
else None
)
if _conn_type == CONF_KNX_ROUTING:
return ConnectionConfig(
connection_type=ConnectionType.ROUTING,
individual_address=self.entry.data[CONF_KNX_INDIVIDUAL_ADDRESS],
multicast_group=self.entry.data[CONF_KNX_MCAST_GRP],
multicast_port=self.entry.data[CONF_KNX_MCAST_PORT],
local_ip=self.entry.data.get(CONF_KNX_LOCAL_IP),
auto_reconnect=True,
secure_config=SecureConfig(
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
threaded=True,
)
if _conn_type == CONF_KNX_TUNNELING:
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING,
gateway_ip=self.entry.data[CONF_HOST],
gateway_port=self.entry.data[CONF_PORT],
local_ip=self.entry.data.get(CONF_KNX_LOCAL_IP),
route_back=self.entry.data.get(CONF_KNX_ROUTE_BACK, False),
auto_reconnect=True,
secure_config=SecureConfig(
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
threaded=True,
)
if _conn_type == CONF_KNX_TUNNELING_TCP:
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING_TCP,
individual_address=self.entry.data.get(CONF_KNX_TUNNEL_ENDPOINT_IA),
gateway_ip=self.entry.data[CONF_HOST],
gateway_port=self.entry.data[CONF_PORT],
auto_reconnect=True,
secure_config=SecureConfig(
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
threaded=True,
)
if _conn_type == CONF_KNX_TUNNELING_TCP_SECURE:
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING_TCP_SECURE,
individual_address=self.entry.data.get(CONF_KNX_TUNNEL_ENDPOINT_IA),
gateway_ip=self.entry.data[CONF_HOST],
gateway_port=self.entry.data[CONF_PORT],
secure_config=SecureConfig(
user_id=self.entry.data.get(CONF_KNX_SECURE_USER_ID),
user_password=self.entry.data.get(CONF_KNX_SECURE_USER_PASSWORD),
device_authentication_password=self.entry.data.get(
CONF_KNX_SECURE_DEVICE_AUTHENTICATION
),
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
auto_reconnect=True,
threaded=True,
)
if _conn_type == CONF_KNX_ROUTING_SECURE:
return ConnectionConfig(
connection_type=ConnectionType.ROUTING_SECURE,
individual_address=self.entry.data[CONF_KNX_INDIVIDUAL_ADDRESS],
multicast_group=self.entry.data[CONF_KNX_MCAST_GRP],
multicast_port=self.entry.data[CONF_KNX_MCAST_PORT],
local_ip=self.entry.data.get(CONF_KNX_LOCAL_IP),
secure_config=SecureConfig(
backbone_key=self.entry.data.get(CONF_KNX_ROUTING_BACKBONE_KEY),
latency_ms=self.entry.data.get(
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE
),
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
auto_reconnect=True,
threaded=True,
)
return ConnectionConfig(
auto_reconnect=True,
individual_address=self.entry.data.get(
CONF_KNX_TUNNEL_ENDPOINT_IA, # may be configured at knxkey upload
),
secure_config=SecureConfig(
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
threaded=True,
)
def connection_state_changed_cb(self, state: XknxConnectionState) -> None:
"""Call invoked after a KNX connection state change was received."""
self.connected = state == XknxConnectionState.CONNECTED
for device in self.xknx.devices:
device.after_update()
def telegram_received_cb(self, telegram: Telegram) -> None:
"""Call invoked after a KNX telegram was received."""
# Not all telegrams have serializable data.
data: int | tuple[int, ...] | None = None
value = None
if (
isinstance(telegram.payload, (GroupValueWrite, GroupValueResponse))
and telegram.payload.value is not None
and isinstance(
telegram.destination_address, (GroupAddress, InternalGroupAddress)
)
):
data = telegram.payload.value.value
if transcoder := (
self.group_address_transcoder.get(telegram.destination_address)
or next(
(
_transcoder
for _filter, _transcoder in self._address_filter_transcoder.items()
if _filter.match(telegram.destination_address)
),
None,
)
):
try:
value = transcoder.from_knx(telegram.payload.value)
except (ConversionError, CouldNotParseTelegram) as err:
_LOGGER.warning(
(
"Error in `knx_event` at decoding type '%s' from"
" telegram %s\n%s"
),
transcoder.__name__,
telegram,
err,
)
self.hass.bus.async_fire(
"knx_event",
{
"data": data,
"destination": str(telegram.destination_address),
"direction": telegram.direction.value,
"value": value,
"source": str(telegram.source_address),
"telegramtype": telegram.payload.__class__.__name__,
},
)
def register_event_callback(self) -> TelegramQueue.Callback:
"""Register callback for knx_event within XKNX TelegramQueue."""
address_filters = []
for filter_set in self.config_yaml[CONF_EVENT]:
_filters = list(map(AddressFilter, filter_set[KNX_ADDRESS]))
address_filters.extend(_filters)
if (dpt := filter_set.get(CONF_TYPE)) and (
transcoder := DPTBase.parse_transcoder(dpt)
):
self._address_filter_transcoder.update(
dict.fromkeys(_filters, transcoder)
)
return self.xknx.telegram_queue.register_telegram_received_cb(
self.telegram_received_cb,
address_filters=address_filters,
group_addresses=[],
match_for_outgoing=True,
)

View File

@@ -1,4 +1,4 @@
"""Support for KNX/IP binary sensors."""
"""Support for KNX binary sensor entities."""
from __future__ import annotations
@@ -25,7 +25,6 @@ from homeassistant.helpers.entity_platform import (
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import (
ATTR_COUNTER,
ATTR_SOURCE,
@@ -39,6 +38,7 @@ from .const import (
KNX_MODULE_KEY,
)
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
from .knx_module import KNXModule
from .storage.const import CONF_ENTITY, CONF_GA_PASSIVE, CONF_GA_SENSOR, CONF_GA_STATE

View File

@@ -1,4 +1,4 @@
"""Support for KNX/IP buttons."""
"""Support for KNX button entities."""
from __future__ import annotations
@@ -11,9 +11,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import CONF_PAYLOAD_LENGTH, KNX_ADDRESS, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .knx_module import KNXModule
async def async_setup_entry(

View File

@@ -1,4 +1,4 @@
"""Support for KNX/IP climate devices."""
"""Support for KNX climate entities."""
from __future__ import annotations
@@ -37,9 +37,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import CONTROLLER_MODES, CURRENT_HVAC_ACTIONS, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .knx_module import KNXModule
from .schema import ClimateSchema
ATTR_COMMAND_VALUE = "command_value"

View File

@@ -14,7 +14,7 @@ from homeassistant.const import Platform
from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from . import KNXModule
from .knx_module import KNXModule
DOMAIN: Final = "knx"
KNX_MODULE_KEY: HassKey[KNXModule] = HassKey(DOMAIN)

View File

@@ -1,4 +1,4 @@
"""Support for KNX/IP covers."""
"""Support for KNX cover entities."""
from __future__ import annotations
@@ -28,9 +28,9 @@ from homeassistant.helpers.entity_platform import (
)
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import CONF_SYNC_STATE, DOMAIN, KNX_MODULE_KEY, CoverConf
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
from .knx_module import KNXModule
from .schema import CoverSchema
from .storage.const import (
CONF_ENTITY,

View File

@@ -1,4 +1,4 @@
"""Support for KNX/IP date."""
"""Support for KNX date entities."""
from __future__ import annotations
@@ -22,7 +22,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import (
CONF_RESPOND_TO_READ,
CONF_STATE_ADDRESS,
@@ -31,6 +30,7 @@ from .const import (
KNX_MODULE_KEY,
)
from .entity import KnxYamlEntity
from .knx_module import KNXModule
async def async_setup_entry(

View File

@@ -1,4 +1,4 @@
"""Support for KNX/IP datetime."""
"""Support for KNX datetime entities."""
from __future__ import annotations
@@ -23,7 +23,6 @@ from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
from . import KNXModule
from .const import (
CONF_RESPOND_TO_READ,
CONF_STATE_ADDRESS,
@@ -32,6 +31,7 @@ from .const import (
KNX_MODULE_KEY,
)
from .entity import KnxYamlEntity
from .knx_module import KNXModule
async def async_setup_entry(

View File

@@ -1,4 +1,4 @@
"""Handle KNX Devices."""
"""Handle Home Assistant Devices for the KNX integration."""
from __future__ import annotations

View File

@@ -1,4 +1,4 @@
"""Provides device triggers for KNX."""
"""Provide device triggers for KNX."""
from __future__ import annotations

View File

@@ -1,4 +1,4 @@
"""Diagnostics support for KNX."""
"""Diagnostics support for the KNX integration."""
from __future__ import annotations

View File

@@ -1,4 +1,4 @@
"""Base class for KNX devices."""
"""Base classes for KNX entities."""
from __future__ import annotations
@@ -17,7 +17,7 @@ from .storage.config_store import PlatformControllerBase
from .storage.const import CONF_DEVICE_INFO
if TYPE_CHECKING:
from . import KNXModule
from .knx_module import KNXModule
class KnxUiEntityPlatformController(PlatformControllerBase):

View File

@@ -1,4 +1,4 @@
"""Exposures to KNX bus."""
"""Expose Home Assistant entity states to KNX."""
from __future__ import annotations

View File

@@ -1,4 +1,4 @@
"""Support for KNX/IP fans."""
"""Support for KNX fan entities."""
from __future__ import annotations
@@ -19,9 +19,9 @@ from homeassistant.util.percentage import (
)
from homeassistant.util.scaling import int_states_in_range
from . import KNXModule
from .const import KNX_ADDRESS, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .knx_module import KNXModule
from .schema import FanSchema
DEFAULT_PERCENTAGE: Final = 50

View File

@@ -0,0 +1,301 @@
"""Base module for the KNX integration."""
from __future__ import annotations
import logging
from xknx import XKNX
from xknx.core import XknxConnectionState
from xknx.core.state_updater import StateTrackerType, TrackerOptions
from xknx.core.telegram_queue import TelegramQueue
from xknx.dpt import DPTBase
from xknx.exceptions import ConversionError, CouldNotParseTelegram
from xknx.io import ConnectionConfig, ConnectionType, SecureConfig
from xknx.telegram import AddressFilter, Telegram
from xknx.telegram.address import DeviceGroupAddress, GroupAddress, InternalGroupAddress
from xknx.telegram.apci import GroupValueResponse, GroupValueWrite
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_EVENT,
CONF_HOST,
CONF_PORT,
CONF_TYPE,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.helpers.storage import STORAGE_DIR
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_KNX_CONNECTION_TYPE,
CONF_KNX_INDIVIDUAL_ADDRESS,
CONF_KNX_KNXKEY_FILENAME,
CONF_KNX_KNXKEY_PASSWORD,
CONF_KNX_LOCAL_IP,
CONF_KNX_MCAST_GRP,
CONF_KNX_MCAST_PORT,
CONF_KNX_RATE_LIMIT,
CONF_KNX_ROUTE_BACK,
CONF_KNX_ROUTING,
CONF_KNX_ROUTING_BACKBONE_KEY,
CONF_KNX_ROUTING_SECURE,
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE,
CONF_KNX_SECURE_DEVICE_AUTHENTICATION,
CONF_KNX_SECURE_USER_ID,
CONF_KNX_SECURE_USER_PASSWORD,
CONF_KNX_STATE_UPDATER,
CONF_KNX_TELEGRAM_LOG_SIZE,
CONF_KNX_TUNNEL_ENDPOINT_IA,
CONF_KNX_TUNNELING,
CONF_KNX_TUNNELING_TCP,
CONF_KNX_TUNNELING_TCP_SECURE,
KNX_ADDRESS,
TELEGRAM_LOG_DEFAULT,
)
from .device import KNXInterfaceDevice
from .expose import KNXExposeSensor, KNXExposeTime
from .project import KNXProject
from .storage.config_store import KNXConfigStore
from .telegrams import Telegrams
_LOGGER = logging.getLogger(__name__)
class KNXModule:
"""Representation of KNX Object."""
def __init__(
self, hass: HomeAssistant, config: ConfigType, entry: ConfigEntry
) -> None:
"""Initialize KNX module."""
self.hass = hass
self.config_yaml = config
self.connected = False
self.exposures: list[KNXExposeSensor | KNXExposeTime] = []
self.service_exposures: dict[str, KNXExposeSensor | KNXExposeTime] = {}
self.entry = entry
self.project = KNXProject(hass=hass, entry=entry)
self.config_store = KNXConfigStore(hass=hass, config_entry=entry)
default_state_updater = (
TrackerOptions(tracker_type=StateTrackerType.EXPIRE, update_interval_min=60)
if self.entry.data[CONF_KNX_STATE_UPDATER]
else TrackerOptions(
tracker_type=StateTrackerType.INIT, update_interval_min=60
)
)
self.xknx = XKNX(
address_format=self.project.get_address_format(),
connection_config=self.connection_config(),
rate_limit=self.entry.data[CONF_KNX_RATE_LIMIT],
state_updater=default_state_updater,
)
self.xknx.connection_manager.register_connection_state_changed_cb(
self.connection_state_changed_cb
)
self.telegrams = Telegrams(
hass=hass,
xknx=self.xknx,
project=self.project,
log_size=entry.data.get(CONF_KNX_TELEGRAM_LOG_SIZE, TELEGRAM_LOG_DEFAULT),
)
self.interface_device = KNXInterfaceDevice(
hass=hass, entry=entry, xknx=self.xknx
)
self._address_filter_transcoder: dict[AddressFilter, type[DPTBase]] = {}
self.group_address_transcoder: dict[DeviceGroupAddress, type[DPTBase]] = {}
self.knx_event_callback: TelegramQueue.Callback = self.register_event_callback()
self.entry.async_on_unload(
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
)
async def start(self) -> None:
"""Start XKNX object. Connect to tunneling or Routing device."""
await self.project.load_project(self.xknx)
await self.config_store.load_data()
await self.telegrams.load_history()
await self.xknx.start()
async def stop(self, event: Event | None = None) -> None:
"""Stop XKNX object. Disconnect from tunneling or Routing device."""
await self.xknx.stop()
await self.telegrams.save_history()
def connection_config(self) -> ConnectionConfig:
"""Return the connection_config."""
_conn_type: str = self.entry.data[CONF_KNX_CONNECTION_TYPE]
_knxkeys_file: str | None = (
self.hass.config.path(
STORAGE_DIR,
self.entry.data[CONF_KNX_KNXKEY_FILENAME],
)
if self.entry.data.get(CONF_KNX_KNXKEY_FILENAME) is not None
else None
)
if _conn_type == CONF_KNX_ROUTING:
return ConnectionConfig(
connection_type=ConnectionType.ROUTING,
individual_address=self.entry.data[CONF_KNX_INDIVIDUAL_ADDRESS],
multicast_group=self.entry.data[CONF_KNX_MCAST_GRP],
multicast_port=self.entry.data[CONF_KNX_MCAST_PORT],
local_ip=self.entry.data.get(CONF_KNX_LOCAL_IP),
auto_reconnect=True,
secure_config=SecureConfig(
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
threaded=True,
)
if _conn_type == CONF_KNX_TUNNELING:
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING,
gateway_ip=self.entry.data[CONF_HOST],
gateway_port=self.entry.data[CONF_PORT],
local_ip=self.entry.data.get(CONF_KNX_LOCAL_IP),
route_back=self.entry.data.get(CONF_KNX_ROUTE_BACK, False),
auto_reconnect=True,
secure_config=SecureConfig(
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
threaded=True,
)
if _conn_type == CONF_KNX_TUNNELING_TCP:
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING_TCP,
individual_address=self.entry.data.get(CONF_KNX_TUNNEL_ENDPOINT_IA),
gateway_ip=self.entry.data[CONF_HOST],
gateway_port=self.entry.data[CONF_PORT],
auto_reconnect=True,
secure_config=SecureConfig(
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
threaded=True,
)
if _conn_type == CONF_KNX_TUNNELING_TCP_SECURE:
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING_TCP_SECURE,
individual_address=self.entry.data.get(CONF_KNX_TUNNEL_ENDPOINT_IA),
gateway_ip=self.entry.data[CONF_HOST],
gateway_port=self.entry.data[CONF_PORT],
secure_config=SecureConfig(
user_id=self.entry.data.get(CONF_KNX_SECURE_USER_ID),
user_password=self.entry.data.get(CONF_KNX_SECURE_USER_PASSWORD),
device_authentication_password=self.entry.data.get(
CONF_KNX_SECURE_DEVICE_AUTHENTICATION
),
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
auto_reconnect=True,
threaded=True,
)
if _conn_type == CONF_KNX_ROUTING_SECURE:
return ConnectionConfig(
connection_type=ConnectionType.ROUTING_SECURE,
individual_address=self.entry.data[CONF_KNX_INDIVIDUAL_ADDRESS],
multicast_group=self.entry.data[CONF_KNX_MCAST_GRP],
multicast_port=self.entry.data[CONF_KNX_MCAST_PORT],
local_ip=self.entry.data.get(CONF_KNX_LOCAL_IP),
secure_config=SecureConfig(
backbone_key=self.entry.data.get(CONF_KNX_ROUTING_BACKBONE_KEY),
latency_ms=self.entry.data.get(
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE
),
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
auto_reconnect=True,
threaded=True,
)
return ConnectionConfig(
auto_reconnect=True,
individual_address=self.entry.data.get(
CONF_KNX_TUNNEL_ENDPOINT_IA, # may be configured at knxkey upload
),
secure_config=SecureConfig(
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
threaded=True,
)
def connection_state_changed_cb(self, state: XknxConnectionState) -> None:
"""Call invoked after a KNX connection state change was received."""
self.connected = state == XknxConnectionState.CONNECTED
for device in self.xknx.devices:
device.after_update()
def telegram_received_cb(self, telegram: Telegram) -> None:
"""Call invoked after a KNX telegram was received."""
# Not all telegrams have serializable data.
data: int | tuple[int, ...] | None = None
value = None
if (
isinstance(telegram.payload, (GroupValueWrite, GroupValueResponse))
and telegram.payload.value is not None
and isinstance(
telegram.destination_address, (GroupAddress, InternalGroupAddress)
)
):
data = telegram.payload.value.value
if transcoder := (
self.group_address_transcoder.get(telegram.destination_address)
or next(
(
_transcoder
for _filter, _transcoder in self._address_filter_transcoder.items()
if _filter.match(telegram.destination_address)
),
None,
)
):
try:
value = transcoder.from_knx(telegram.payload.value)
except (ConversionError, CouldNotParseTelegram) as err:
_LOGGER.warning(
(
"Error in `knx_event` at decoding type '%s' from"
" telegram %s\n%s"
),
transcoder.__name__,
telegram,
err,
)
self.hass.bus.async_fire(
"knx_event",
{
"data": data,
"destination": str(telegram.destination_address),
"direction": telegram.direction.value,
"value": value,
"source": str(telegram.source_address),
"telegramtype": telegram.payload.__class__.__name__,
},
)
def register_event_callback(self) -> TelegramQueue.Callback:
"""Register callback for knx_event within XKNX TelegramQueue."""
address_filters = []
for filter_set in self.config_yaml[CONF_EVENT]:
_filters = list(map(AddressFilter, filter_set[KNX_ADDRESS]))
address_filters.extend(_filters)
if (dpt := filter_set.get(CONF_TYPE)) and (
transcoder := DPTBase.parse_transcoder(dpt)
):
self._address_filter_transcoder.update(
dict.fromkeys(_filters, transcoder)
)
return self.xknx.telegram_queue.register_telegram_received_cb(
self.telegram_received_cb,
address_filters=address_filters,
group_addresses=[],
match_for_outgoing=True,
)

View File

@@ -1,4 +1,4 @@
"""Support for KNX/IP lights."""
"""Support for KNX light entities."""
from __future__ import annotations
@@ -28,9 +28,9 @@ from homeassistant.helpers.entity_platform import (
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import color as color_util
from . import KNXModule
from .const import CONF_SYNC_STATE, DOMAIN, KNX_ADDRESS, KNX_MODULE_KEY, ColorTempModes
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
from .knx_module import KNXModule
from .schema import LightSchema
from .storage.const import (
CONF_COLOR_TEMP_MAX,

View File

@@ -1,4 +1,4 @@
"""Support for KNX/IP notifications."""
"""Support for KNX notify entities."""
from __future__ import annotations
@@ -12,9 +12,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import KNX_ADDRESS, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .knx_module import KNXModule
async def async_setup_entry(

View File

@@ -1,4 +1,4 @@
"""Support for KNX/IP numeric values."""
"""Support for KNX number entities."""
from __future__ import annotations
@@ -22,9 +22,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, KNX_ADDRESS, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .knx_module import KNXModule
from .schema import NumberSchema

View File

@@ -1,4 +1,4 @@
"""Support for KNX scenes."""
"""Support for KNX scene entities."""
from __future__ import annotations
@@ -13,9 +13,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import KNX_ADDRESS, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .knx_module import KNXModule
from .schema import SceneSchema

View File

@@ -1,4 +1,4 @@
"""Support for KNX/IP select entities."""
"""Support for KNX select entities."""
from __future__ import annotations
@@ -20,7 +20,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import (
CONF_PAYLOAD_LENGTH,
CONF_RESPOND_TO_READ,
@@ -30,6 +29,7 @@ from .const import (
KNX_MODULE_KEY,
)
from .entity import KnxYamlEntity
from .knx_module import KNXModule
from .schema import SelectSchema

View File

@@ -1,4 +1,4 @@
"""Support for KNX/IP sensors."""
"""Support for KNX sensor entities."""
from __future__ import annotations
@@ -33,9 +33,9 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType, StateType
from homeassistant.util.enum import try_parse_enum
from . import KNXModule
from .const import ATTR_SOURCE, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .knx_module import KNXModule
from .schema import SensorSchema
SCAN_INTERVAL = timedelta(seconds=10)

View File

@@ -35,7 +35,7 @@ from .expose import create_knx_exposure
from .schema import ExposeSchema, dpt_base_type_validator, ga_validator
if TYPE_CHECKING:
from . import KNXModule
from .knx_module import KNXModule
_LOGGER = logging.getLogger(__name__)

View File

@@ -1 +1 @@
"""Helpers for KNX."""
"""Handle persistent storage for the KNX integration."""

View File

@@ -1,4 +1,4 @@
"""KNX Entity Store Validation."""
"""KNX entity store validation."""
from typing import Literal, TypedDict

View File

@@ -1,4 +1,4 @@
"""Support for KNX/IP switches."""
"""Support for KNX switch entities."""
from __future__ import annotations
@@ -25,7 +25,6 @@ from homeassistant.helpers.entity_platform import (
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import (
CONF_INVERT,
CONF_RESPOND_TO_READ,
@@ -35,6 +34,7 @@ from .const import (
KNX_MODULE_KEY,
)
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
from .knx_module import KNXModule
from .schema import SwitchSchema
from .storage.const import (
CONF_ENTITY,

View File

@@ -1,4 +1,4 @@
"""Support for KNX/IP text."""
"""Support for KNX text entities."""
from __future__ import annotations
@@ -22,9 +22,9 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, KNX_ADDRESS, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .knx_module import KNXModule
async def async_setup_entry(

View File

@@ -1,4 +1,4 @@
"""Support for KNX/IP time."""
"""Support for KNX time entities."""
from __future__ import annotations
@@ -22,7 +22,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import (
CONF_RESPOND_TO_READ,
CONF_STATE_ADDRESS,
@@ -31,6 +30,7 @@ from .const import (
KNX_MODULE_KEY,
)
from .entity import KnxYamlEntity
from .knx_module import KNXModule
async def async_setup_entry(

View File

@@ -1,4 +1,4 @@
"""Offer knx telegram automation triggers."""
"""Provide KNX automation triggers."""
from typing import Final

View File

@@ -1,4 +1,4 @@
"""Support for KNX/IP weather station."""
"""Support for KNX weather entities."""
from __future__ import annotations
@@ -19,9 +19,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .knx_module import KNXModule
from .schema import WeatherSchema

View File

@@ -36,7 +36,7 @@ from .storage.entity_store_validation import (
from .telegrams import SIGNAL_KNX_TELEGRAM, TelegramDict
if TYPE_CHECKING:
from . import KNXModule
from .knx_module import KNXModule
URL_BASE: Final = "/knx_static"

View File

@@ -20,8 +20,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import DOMAIN
SCAN_INTERVAL = timedelta(seconds=15)
SETTINGS_UPDATE_INTERVAL = timedelta(hours=1)
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=5)
SETTINGS_UPDATE_INTERVAL = timedelta(hours=8)
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=30)
STATISTICS_UPDATE_INTERVAL = timedelta(minutes=15)
_LOGGER = logging.getLogger(__name__)

View File

@@ -37,5 +37,5 @@
"iot_class": "cloud_push",
"loggers": ["pylamarzocco"],
"quality_scale": "platinum",
"requirements": ["pylamarzocco==2.0.6"]
"requirements": ["pylamarzocco==2.0.8"]
}

View File

@@ -119,7 +119,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
key="prebrew_on",
translation_key="prebrew_time_on",
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS,
native_min_value=0,
native_max_value=10,
@@ -158,7 +158,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
key="prebrew_off",
translation_key="prebrew_time_off",
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS,
native_min_value=0,
native_max_value=10,

View File

@@ -3,6 +3,7 @@
"name": "LG ThinQ",
"codeowners": ["@LG-ThinQ-Integration"],
"config_flow": true,
"dhcp": [{ "macaddress": "34E6E6*" }],
"documentation": "https://www.home-assistant.io/integrations/lg_thinq",
"iot_class": "cloud_push",
"loggers": ["thinqconnect"],

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