Compare commits

...

115 Commits

Author SHA1 Message Date
Franck Nijhof
14eed1778b Bump version to 2025.4.0b8 2025-03-28 20:46:26 +00:00
Norbert Rittel
049aaa7e8b Fix grammar / sentence-casing in workday (#141682)
* Fix grammar / sentence-casing in `workday`

Also replace "country" with common string.

* Add two more references

* Fix second data description reference

* Add "given" to action description for better translations
2025-03-28 20:46:17 +00:00
J. Nick Koston
35717e8216 Increase websocket_api allowed peak time to 10s (#141680)
* Increase websocket_api allowed peak time to 10s

fixes #141624

During integration reload or startup, we can end up sending a message for
each entity being created for integrations that create them from an external
source (ie MQTT) because the messages come in one at a time. This can overload
the loop and/or client for more than 5s. While we have done significant work
to optimize for this path, we are at the limit at what we can expect clients
to be able to process in the time window, so increase the time window.

* adjust test
2025-03-28 20:46:13 +00:00
Franck Nijhof
2a081abc18 Fix camera proxy with sole image quality settings (#141676) 2025-03-28 20:46:10 +00:00
puddly
b7f29c7358 Handle all firmware types for ZBT-1 and Yellow update entities (#141674)
Handle other firmware types
2025-03-28 20:46:06 +00:00
Jason Hunter
3bb6373df5 Update Duke Energy package to fix integration (#141669)
* Update Duke Energy package to fix integration

* fix tests
2025-03-28 20:46:03 +00:00
Michael Hansen
e1b4edec50 Bump intents and always prefer more literal text (#141663) 2025-03-28 20:46:00 +00:00
puddly
147bee57e1 Include ZBT-1 and Yellow in device registry (#141623)
* Add the Yellow and ZBT-1 to the device registry

* Unload platforms

* Fix unit tests

* Rename the Yellow update entity to `Radio firmware`

* Rename `EmberZNet` to `EmberZNet Zigbee`

* Prefix the `sw_version` with the firmware type and clean up

* Fix unit tests

* Remove unnecessary `always_update=False` from data update coordinator
2025-03-28 20:45:56 +00:00
Erwin Douna
fcdaea64da Tado add proper off state (#135480)
* Add proper off state

* Remove current temp

* Add default frost temp
2025-03-28 20:45:53 +00:00
Franck Nijhof
d1512d46be Bump version to 2025.4.0b7 2025-03-28 16:00:45 +00:00
Bram Kragten
0be7db6270 Update frontend to 20250328.0 (#141659) 2025-03-28 15:09:56 +00:00
Paulus Schoutsen
2af0282725 Enable the message box on default for satelitte announcement actions (#141654) 2025-03-28 15:09:51 +00:00
Franck Nijhof
ff458c8417 Bump version to 2025.4.0b6 2025-03-28 15:04:34 +00:00
Franck Nijhof
cc93152ff0 Fix ESPHome event entity staying unavailable (#141650) 2025-03-28 14:05:40 +00:00
Paulus Schoutsen
9965f01609 Ensure connection test sound has no preannouncement (#141647) 2025-03-28 14:05:37 +00:00
Jan Bouwhuis
e9c76ce694 Fix duplicate 'device' term in MQTT translation strings (#141646)
* Fix duplicate 'device' from MQTT translation strings

* Update homeassistant/components/mqtt/strings.json
2025-03-28 14:05:34 +00:00
Norbert Rittel
58ab7d350d Fix sentence-casing in airvisual user strings (#141632) 2025-03-28 14:05:30 +00:00
Nick Pesce
e4d6e20ebd Use correct default value for multi press buttons in the Matter integration (#141630)
* Respect the min 2 constraint for the switch MultiPressMax attribute

* Update test_event.py

* Update generic_switch_multi.json

* Fix issue and update tests
2025-03-28 14:05:27 +00:00
Tsvi Mostovicz
45e273897a Jewish calendar match omer service variables requirement to documentation (#141620)
The documentation and the omer schema require a Nusach to be specified, but the YAML misses that requirement
2025-03-28 14:05:23 +00:00
Jan Bouwhuis
d9ec7142d7 Fix volatile_organic_compounds_parts translation string to be referenced for MQTT subentries device class selector (#141618)
* Fix ` volatile_organic_compounds_parts` translation string to be referenced for MQTT subentries device class selector

* Fix tests
2025-03-28 14:05:20 +00:00
Petro31
e162499267 Fix an issue with the switch preview in beta (#141617)
Fix an issue with the switch preview
2025-03-28 14:05:16 +00:00
Jan-Philipp Benecke
67f21429e3 Bump aiowebdav2 to 0.4.4 (#141615) 2025-03-28 14:05:12 +00:00
J. Nick Koston
a0563f06c9 Fix zeroconf logging level not being respected (#141601)
Removes an old logging workaround that is no longer needed

fixes #141558
2025-03-28 14:05:05 +00:00
Luke Lashley
e7c4fdc8bb Bump Python-Snoo to 0.6.5 (#141599)
* Bump Python-Snoo to 0.6.5

* add to event_types
2025-03-28 14:05:00 +00:00
Norbert Rittel
c490e350bc Make names of switch entities in gree consistent with docs (#141580) 2025-03-28 14:04:56 +00:00
Robert Resch
e11409ef99 Reverts #141363 "Deprecate SmartThings machine state sensors" (#141573)
Reverts #141363
2025-03-28 14:04:52 +00:00
Joost Lekkerkerker
5c8e415a76 Add default string and icon for light effect off (#141567) 2025-03-28 14:04:49 +00:00
alorente
e795fb9497 Fix missing response for queued mode scripts (#141460) 2025-03-28 14:04:45 +00:00
Norbert Rittel
d0afabb85c Fix misleading friendly names of pvoutput sensors (#141312)
* Fix misleading friendly names of `pvoutput` sensors

* Update test_sensor.py

* Update test_sensor.py - prettier
2025-03-28 14:04:41 +00:00
Franck Nijhof
4f3e8e9b94 Bump version to 2025.4.0b5 2025-03-27 20:03:14 +00:00
Paul Bottein
46c1cbbc9c Update frontend to 20250327.1 (#141596) 2025-03-27 20:03:01 +00:00
Simon Lamon
8d9a4ea278 Fix typing error in NMBS (#141589)
Fix typing error
2025-03-27 20:02:58 +00:00
Jan-Philipp Benecke
22c83e2393 Bump aiowebdav2 to 0.4.3 (#141586) 2025-03-27 20:02:55 +00:00
Joost Lekkerkerker
c83a75f6f9 Add brand for Bosch (#141561) 2025-03-27 20:02:51 +00:00
Franck Nijhof
841c727112 Bump version to 2025.4.0b4 2025-03-27 16:59:36 +00:00
Bram Kragten
d8c9655bfd Update frontend to 20250327.0 (#141585) 2025-03-27 16:59:29 +00:00
Erik Montnemery
942ed89cc4 Revert "Promote after dependencies in bootstrap" (#141584)
Revert "Promote after dependencies in bootstrap (#140352)"

This reverts commit 3766040960.
2025-03-27 16:59:25 +00:00
Franck Nijhof
a1fe6b9cf3 Bump version to 2025.4.0b3 2025-03-27 15:38:31 +00:00
Luke Lashley
2567181cc2 Better handle Roborock discovery (#141575) 2025-03-27 15:38:24 +00:00
Joost Lekkerkerker
028e4f6029 Also migrate completion time entities in SmartThings (#141572) 2025-03-27 15:38:21 +00:00
Martin Hjelmare
b82e1a9bef Handle cloud subscription expired for backup upload (#141564)
Handle cloud backup subscription expired for upload
2025-03-27 15:38:18 +00:00
Joost Lekkerkerker
438f226c31 Add icons to hue effects (#141559) 2025-03-27 15:38:15 +00:00
Erwin Douna
2f139e3cb1 Tado fix HomeKit flow (#141525)
* Initial commit

* Fix

* Fix

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
2025-03-27 15:38:07 +00:00
Franck Nijhof
5d75e96fbf Bump version to 2025.4.0b2 2025-03-27 10:19:35 +00:00
Norbert Rittel
dcf2ec5c37 Fix sentence-casing in konnected strings, replace "override" with "custom" (#141553)
Fix sentence-casing in `konnected`strings, replace "Override" with "Custom"

Make string consistent with HA standards.

As "Override" can be misunderstood as the verb, replace it with "Custom".
2025-03-27 10:19:22 +00:00
Simon Lamon
2431e1ba98 Bump linkplay to v0.2.2 (#141542)
Bump linkplay
2025-03-27 10:19:18 +00:00
Thomas55555
4ead108c15 Handle webcal prefix in remote calendar (#141541)
Handel webcal prefix in remote calendar
2025-03-27 10:19:14 +00:00
Michael Hansen
ec8363fa49 Add default preannounce sound to Assist satellites (#141522)
* Add default preannounce sound

* Allow None to disable sound

* Register static path instead of HTTP view

* Fix path

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-03-27 10:19:09 +00:00
J. Diego Rodríguez Royo
e7ff0a3f8b Improve some Home Connect deprecations (#141508) 2025-03-27 10:19:06 +00:00
Ivan Lopez Hernandez
f4c0eb4189 Initialize google.genai.Client in the executor (#141432)
* Intialize the client on an executor thread

* Fix MyPy error

* MyPy error

* Exception error

* Fix ruff

* Update __init__.py

---------

Co-authored-by: tronikos <tronikos@users.noreply.github.com>
2025-03-27 10:19:02 +00:00
Manu
b1ee5a76e1 Support for upcoming pyLoad-ng release in pyLoad integration (#141297)
Fix extra key `proxy` in pyLoad
2025-03-27 10:18:58 +00:00
Norbert Rittel
6b9e8c301b Fix wrong friendly name for storage_power in solaredge (#141269)
* Fix wrong friendly name for `storage_power` in `solaredge`

"Stored power" is a contradiction in itself.
You can only store energy.

* Two additional spelling fixes

* Sentence-case "site"
2025-03-27 10:18:53 +00:00
Franck Nijhof
89c3266c7e Bump version to 2025.4.0b1 2025-03-26 23:21:26 +00:00
Jan Bouwhuis
cff0a632e8 Fix QoS schema issue in MQTT subentries (#141531) 2025-03-26 23:21:17 +00:00
Jan Bouwhuis
e04d8557ae Fix MQTT options flow QoS selector can not serialize (#141528) 2025-03-26 23:21:14 +00:00
Thomas55555
ca6286f241 Fix work area sensor for Husqvarna Automower (#141527)
* Fix work area sensor for Husqvarna Automower

* simplify
2025-03-26 23:21:10 +00:00
Robert Resch
35bcc9d5af Show box for Smartthings rise number entity (#141526) 2025-03-26 23:21:07 +00:00
Joost Lekkerkerker
25b45ce867 Sort SmartThings devices to be created by parent device id (#141515) 2025-03-26 23:21:03 +00:00
Robert Resch
d568209bd5 Bump deebot-client to 12.4.0 (#141501) 2025-03-26 23:21:00 +00:00
Simone Chemelli
8a43e8af9e Fix refresh state for Comelit alarm (#141370) 2025-03-26 23:20:56 +00:00
Franck Nijhof
785e5b2c16 Bump version to 2025.4.0b0 2025-03-26 17:41:03 +00:00
Joost Lekkerkerker
2e3853dd7d Deprecate SmartThings media player switch (#141467)
* Deprecate SmartThings media player switch

* Fix

* Fix

* Update homeassistant/components/smartthings/strings.json

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

* Fix

---------

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-03-26 18:40:11 +01:00
Joost Lekkerkerker
fe99c39e25 Deprecate media player sensors for SmartThings (#141469)
* Deprecate media player sensors for SmartThings

* Deprecate media player sensors
2025-03-26 18:21:49 +01:00
Maciej Bieniek
c8ab5bc796 Bump IMGW-PIB library to 1.0.10 (#141491) 2025-03-26 17:57:27 +01:00
Álvaro Fernández Rojas
4f3b36c2e1 Update aioairzone-cloud to v0.6.11 (#141488)
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2025-03-26 17:57:15 +01:00
Marc Mueller
222d89a84c Update meteofrance-api to 1.4.0 (#141490) 2025-03-26 17:56:45 +01:00
Joost Lekkerkerker
eb3cb0e0c7 Bump yt-dlp to 2025.03.26 (#141484) 2025-03-26 11:49:29 -05:00
Joost Lekkerkerker
69c8f4fbb6 Add button to reset the water filter in SmartThings (#141493)
* Add button to reset the water filter in SmartThings

* Add button to reset the water filter in SmartThings
2025-03-26 11:48:03 -05:00
Jan Bouwhuis
3bcf1c942c Cleanup missed QoS translation string for MQTT subentries (#141485) 2025-03-26 17:40:22 +01:00
Michael Hansen
220aaf93c6 Add preannounce media id support for ESPHome (#141474)
* Working on preannounce media id support for ESPHome

* Fix test

* Update tests
2025-03-26 11:31:05 -05:00
Jan Bouwhuis
febc455bc5 Add switch as entity platform on MQTT subentries (#140658) 2025-03-26 16:46:44 +01:00
Marc Mueller
57f65c205e Use SPDX identifier for container license (#141477) 2025-03-26 16:31:28 +01:00
Erik Montnemery
6e56486294 Bump pychromecast to 14.0.7 (#141479) 2025-03-26 16:30:37 +01:00
Joost Lekkerkerker
3a1e1684ea Add power binary sensor for Cooktop in SmartThings (#141482) 2025-03-26 16:29:02 +01:00
Bram Kragten
9d63a49812 Update frontend to 20250326.0 (#141481) 2025-03-26 16:27:43 +01:00
Markus Adrario
7a4ca6dcdc Add Homee lock platform (#140893)
* Add homee lock platform

* finish tests

* add locking & unlocking

* add PARALLEL_UPDATES

* fix review comments

* fix test review comment.

* fix another review comment
2025-03-26 09:46:21 -05:00
Marc Mueller
1622638f10 Update mypy-dev to 1.16.0a7 (#141472) 2025-03-26 15:21:38 +01:00
Jan Bouwhuis
0de3549e6e Move QoS setting to shared device properties in MQTT device subentries configuration (#141369)
* Move QoS setting to shared device properties in MQTT device subentries configuration

* Use kwargs for validate_user_input helper
2025-03-26 15:20:08 +01:00
Joost Lekkerkerker
63d4efda2e Deprecate switch entity for airdresser (#141470)
* Deprecate switch entity for airdresser

* Deprecate switch entity for airdresser
2025-03-26 15:06:13 +01:00
J. Diego Rodríguez Royo
b5910dd7d6 Move Home Connect alarm clock entity from time platform to number platform (#141400)
* Move alarm clock entity from time platform to number platform

* Deprecate alarm clock time entity

* Don't update unique id

* Fix tests

* Fixable issues

* improvement

* Make the issues persistent
2025-03-26 14:46:07 +01:00
Denis Shulyaka
c974285490 Add Web search to OpenAI Conversation integration (#141426)
* Add Web search to OpenAI Conversation integration

* Limit search for gpt-4o models

* Add more tests
2025-03-26 09:36:05 -04:00
Michael Hansen
8db91623ec Add language scores websocket command (#140480)
* Add language scores websocket command

* Don't store language scores in snapshot

* Add language/country args for preferred lang

* Bump intents to 2025.3.24 for dash lang code
2025-03-26 14:07:15 +01:00
Michael Hansen
3eda5333b0 Add info websocket command to wyoming integration (#139982)
* Add info websocket command to wyoming integration

* Add snapshot

* Add config schema

* Remove snapshots because of changing config entry ids
2025-03-26 14:06:51 +01:00
Robert Resch
3aaf859985 Add state class MEASUREMENT_ANGLE to wind direction sensor (#141392)
* Add state class MEASUREMENT_ANGLE to wind direction sensor

* Update snapshots

* Add some more
2025-03-26 13:58:23 +01:00
Sanjay Govind
dba4c197c8 Add bosch_alarm integration (#138497)
* Add bosch_alarm integration

* Remove other platforms for now

* update some strings not being consistant

* fix sentence-casing for strings

* remove options flow and versioning

* clean up config flow

* Add OSI license + tagged releases + ci to bosch-alarm-mode2

* Apply suggestions from code review

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

* apply changes from review

* apply changes from review

* remove options flow

* work on fixtures

* work on fixtures

* fix errors and complete flow

* use fixtures for alarm config

* Update homeassistant/components/bosch_alarm/manifest.json

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

* fix missing type

* mock setup entry

* remove use of patch in config flow test

* Use coordinator for managing panel data

* Use coordinator for managing panel data

* Coordinator cleanup

* remove unnecessary observers

* update listeners when error state changes

* Update homeassistant/components/bosch_alarm/coordinator.py

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

* Update homeassistant/components/bosch_alarm/quality_scale.yaml

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

* Update homeassistant/components/bosch_alarm/config_flow.py

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

* rename config flow

* Update homeassistant/components/bosch_alarm/quality_scale.yaml

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

* add missing types

* fix quality_scale.yaml

* enable strict typing

* enable strict typing

* Add test for alarm control panel

* add more tests

* add more tests

* Update homeassistant/components/bosch_alarm/coordinator.py

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

* Update homeassistant/components/bosch_alarm/coordinator.py

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

* Update homeassistant/components/bosch_alarm/alarm_control_panel.py

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

* Update homeassistant/components/bosch_alarm/alarm_control_panel.py

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

* Update homeassistant/components/bosch_alarm/alarm_control_panel.py

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

* Add snapshot test

* add snapshot test

* add snapshot test

* update quality scale

* update quality scale

* update quality scale

* update quality scale

* Apply suggestions from code review

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

* apply changes from code review

* apply changes from code review

* apply changes from code review

* Apply suggestions from code review

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

* apply changes from code review

* apply changes from code review

* Fix alarm control panel device name

* Fix

* Fix

* Fix

* Fix

---------

Co-authored-by: Josef Zweck <josef@zweck.dev>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-03-26 13:56:44 +01:00
Robert Resch
f842640249 Add check that sensor state classes are used only with valid unit of measurements (#141444) 2025-03-26 13:52:00 +01:00
Robert Resch
aa493ff97d Correct device class and state class for wind direction sensors (#141393)
* Fix state class on wind direction sensors

* Update snapshots
2025-03-26 13:48:08 +01:00
Joost Lekkerkerker
21d5885ded Add select entity for dishwasher operating state in SmartThings (#141468)
* Add select entity for dishwasher operating state in SmartThings

* Add select entity for dishwasher operating state in SmartThings
2025-03-26 13:39:36 +01:00
Tsvi Mostovicz
054b3bb26c Add service for counting the omer (#141008)
* Add service for counting the omer

* Add description and strings. Expect string from user

* Fix constraints on nusach and language + Make independent of config_entry

* Provide config schema

* Fix services.yaml and strings.json to match updated service.py

* Use LanguageSelector and some constants

* Action description -> third-person singular

* Use built-in language selector in yaml

* Fix schema

* Show the hebrew date in the correct language in the response

* Revert "Show the hebrew date in the correct language in the response"

This reverts commit 59442d16c5.

Requires a bugfix in the original library

* Don't return the hebrew date as it doesn't return correctly
2025-03-26 13:38:58 +01:00
Jan Bouwhuis
77bf977d63 Add sensor as entity platform on MQTT subentries (#139899)
* Add sensor as entity platform on MQTT subentries

* Fix typo

* Improve device class data description

* Tweak

* Rework reconfig calculation

* Filter out last_reset_value_template if state class is not total

* Collapse expire after as advanced setting

* Update suggested_display_precision translation strings

* Make options and last_reset_template conditional, use sections for advanced settings

* Ensure options are removed properly

* Improve sensor options label, ensure UOM is set when device class has units

* Use helper to apply suggested values from component config

* Rename to `Add option`

* Fix schema builder not hiding empty sections and removing fields excluded from reconfig

* Do not hide advanced settings if values are available or are defaults

* Improve spelling and Learn more links

* Improve unit of measurement validation

* Fix UOM selector and translation strings

* Address comments from code review

* Remove stale comment

* Rename selector constant, split validator

* Simplify config validator

* Return tuple with config and errors for config validation
2025-03-26 13:34:24 +01:00
Robert Resch
3f68e327f3 Bump uv to 0.6.10 (#141464) 2025-03-26 13:30:57 +01:00
Marc Mueller
82db1ffd12 Update typing-extensions to 4.13.0 (#141465) 2025-03-26 13:28:46 +01:00
Allen Porter
06f6c86ba5 Simplify roborock map storage test fixture (#141430) 2025-03-26 08:19:48 -04:00
Robert Resch
e3f2f30395 Add circular mean statistics and sensor state class MEASUREMENT_ANGLE (#138453)
* Add circular mean statistics

* fixes

* Add has_circular_mean and fix tests

* Fix mypy

* Rename to MEASUREMENT_ANGLE

* Fix kitchen_sink tests

* Fix sensor tests

* for testing only

* Revert ws command change

* Apply suggestions

* test only

* add custom handling for postgres

* fix recursion limit

* Check if column is already available

* Set default false and not nullable for has_circular_mean

* Proper fix to be backwards compatible

* Fix value is None

* Align with schema

* Remove has_circular_mean from test schemas as it's not required anymore

* fix wrong column type

* Use correct variable to reduce stats

* Add guard that the uom is matching a valid one from the state class

* Add some tests

* Fix tests again

* Use mean_type in StatisticsMetato difference between different mean type algorithms

* Fix leftovers

* Fix kitchen_sink tests

* Fix postgres

* Add circular mean test

* Add mean_type_changed stats issue

* Align the attributes with unit_changed

* Fix mean_type_change stats issue

* Add missing sensor recorder tests

* Add test_statistic_during_period_circular_mean

* Add mean_weight

* Add test_statistic_during_period_hole_circular_mean

* Use seperate migration step to null has_mean

* Typo ARITHMETIC

* Implement requested changes

* Implement requested changes

* Split into #141444

* Add StatisticMeanType.NONE and forbid that mean_type can be None

* Fix mean_type

* Implement requested changes

* Small leftover of latest StatisticMeanType changes
2025-03-26 13:15:58 +01:00
Brett Adams
4a6d2c91da Bump tesla-fleet-api to v1.0.16 (#140869)
* Add streaming climate

* fixes

* Add missing changes

* Fix restore

* Update homeassistant/components/teslemetry/climate.py

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

* Use dict

* Add fan mode translations

* Infer side

* WIP

* fix deps

* Migration in progress

* Working

* tesla-fleet-api==1.0.15

* tesla-fleet-api==1.0.16

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-03-26 12:28:16 +01:00
Simone Chemelli
d7de8c5f68 Add full test coverage for Comelit coordinator (#141321)
* Add full test coverage for Comelit coordinator

* add common const

* apply review comment
2025-03-26 13:21:58 +02:00
Norbert Rittel
7bcba2b639 Fix online docs URL in motionblinds plus gateway naming (#141453)
* Fix online docs URL in `motionblinds` plus gateway naming

- add missing "api" to the online docs URL to make it work
- fix sentence-casing of "API key"
- replace "Motion Gateway" with "Motionblinds gateway" as there is no brand "Motion" and the list of compatible bridges cover a lot more brands

* Replace comma with period to improve readability
2025-03-26 13:11:49 +02:00
Maciej Bieniek
53990f8fad Do not show the firmware changelog for Shelly Wall Display X2 update entities (#141457)
There is no firmware changelog for Wall Display X2
2025-03-26 12:11:09 +01:00
Joost Lekkerkerker
ed7c864869 Add switch for icemaker in SmartThings (#141313)
* Add switch for icemaker in SmartThings

* Fix
2025-03-26 12:10:44 +01:00
Joost Lekkerkerker
74ff40e253 Deprecate SmartThings machine state sensors (#141363)
* Deprecate SmartThings machine state sensors

* Fix
2025-03-26 11:46:50 +01:00
TimL
57d02d7a17 Cleanups related to improved typing on radios objects (#141455)
* Improved handling of radio objects

* Drop get_radio helper

* Remove mock of get_radio in tests
2025-03-26 11:45:07 +01:00
TimL
043603c9be Add SMLIGHT sensor entities for second radio (#137403)
* Add sensors for second radio

* Add test for zigbee2 sensor

* Update homeassistant/components/smlight/sensor.py

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

* drop useless replace

* Fix test failure

* Fix code coverage in config flow

* Update homeassistant/components/smlight/sensor.py

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

* fix conversion of iterator to list

* Remove assert on radios

* simplify handling of radios further

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-03-26 11:34:44 +01:00
TimL
e10801af80 Bump pysmlight to v0.2.4 (#141450) 2025-03-26 11:28:25 +01:00
Simone Chemelli
f4fa4056ac Make BT support detection dynamic for Shelly RPC devices (#137323) 2025-03-26 11:17:54 +01:00
Joost Lekkerkerker
208e8ae451 Deprecate SmartThings switch entity (#141360)
* Deprecate SmartThings switch entity

* Apply suggestions from code review

Co-authored-by: Robert Resch <robert@resch.dev>

* Fix

* Revert "Apply suggestions from code review"

This reverts commit c6d39d38de.

* Revert "Revert "Apply suggestions from code review""

This reverts commit d92411c156.

* Fix

* Fix

---------

Co-authored-by: Robert Resch <robert@resch.dev>
2025-03-26 11:05:31 +01:00
TheJulianJES
02f8322ac1 Bump ZHA to 0.0.54 (#141447)
* Bump ZHA to 0.0.54

* Add strings for v2 quirk entities

* Adjust cover tests for new ZHA behavior

* Improve cover tests further
2025-03-26 11:55:18 +02:00
Norbert Rittel
e8158234a9 Fix grammar in spotify reauthentication error (#141451) 2025-03-26 11:45:55 +02:00
Norbert Rittel
7848c3cd79 Fixes to user-facing strings of cloudflare integration (#141452)
- fix sentence-casing of a few strings
- fix grammar of action description
2025-03-26 11:45:05 +02:00
Norbert Rittel
2d8420b656 Fix spelling of "serial number" in smappee (#141449) 2025-03-26 10:25:12 +01:00
Joost Lekkerkerker
63a86763b1 Migrate unique ids in SmartThings (#141308)
* Migrate unique ids in SmartThings

* Migrate

* Migrate

* Migrate

* Fix

* Fix
2025-03-26 10:23:20 +01:00
Michael
b5117eb071 Proper handling of unavailable Synology DSM nas during backup (#140721)
* raise BackupAgentUnreachableError when NAS is unavailable

* also raise BackupAgentUnreachableError during upload when nas unavailable

* Revert "also raise BackupAgentUnreachableError during upload when nas unavailable"

This reverts commit 38877d8540.

* Revert "raise BackupAgentUnreachableError when NAS is unavailable"

This reverts commit 4d8cfae396.

* check last_update_success of  coordinator_central to get backup agents

* consider last_update_success before notify backup listeners

* add test

* use walrus :=  :)
2025-03-26 10:22:43 +01:00
Norbert Rittel
f0c774a4bd Small grammar fixes in hue user strings (#141446)
… including proper sentence-casing
2025-03-26 11:16:10 +02:00
Simone Chemelli
8bedf97382 Remove helpers and align coding style in Shelly tests (#140080)
* Cleanup hass.states method in Shelly tests (part 1)

* remove helper functions and align coding style

* missed

* revert unwanted changes

* apply review comment

* apply review comment

* apply review comment

* apply ATTR where missing

* apply walrus

* add missed walrus

* add walrus to entity_registry.async_get

* minor tweak

* align after merge
2025-03-26 10:05:42 +01:00
Robert Resch
65c05d66c0 Use a constant for sensor statistics issues (#141441) 2025-03-26 09:43:09 +01:00
Norbert Rittel
1cb4332a3c Fix sentence-case and naming of "Security code" in tradfri (#141440) 2025-03-26 10:07:30 +02:00
353 changed files with 13332 additions and 2491 deletions

View File

@@ -119,6 +119,7 @@ homeassistant.components.bluetooth_adapters.*
homeassistant.components.bluetooth_tracker.*
homeassistant.components.bmw_connected_drive.*
homeassistant.components.bond.*
homeassistant.components.bosch_alarm.*
homeassistant.components.braviatv.*
homeassistant.components.bring.*
homeassistant.components.brother.*

2
CODEOWNERS generated
View File

@@ -216,6 +216,8 @@ build.json @home-assistant/supervisor
/tests/components/bmw_connected_drive/ @gerard33 @rikroe
/homeassistant/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
/tests/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
/homeassistant/components/bosch_alarm/ @mag1024 @sanjay900
/tests/components/bosch_alarm/ @mag1024 @sanjay900
/homeassistant/components/bosch_shc/ @tschamm
/tests/components/bosch_shc/ @tschamm
/homeassistant/components/braviatv/ @bieniu @Drafteed

2
Dockerfile generated
View File

@@ -31,7 +31,7 @@ RUN \
&& go2rtc --version
# Install uv
RUN pip3 install uv==0.6.8
RUN pip3 install uv==0.6.10
WORKDIR /usr/src

View File

@@ -19,4 +19,4 @@ labels:
org.opencontainers.image.authors: The Home Assistant Authors
org.opencontainers.image.url: https://www.home-assistant.io/
org.opencontainers.image.documentation: https://www.home-assistant.io/docs/
org.opencontainers.image.licenses: Apache License 2.0
org.opencontainers.image.licenses: Apache-2.0

View File

@@ -859,14 +859,8 @@ async def _async_set_up_integrations(
integrations, all_integrations = await _async_resolve_domains_and_preload(
hass, config
)
# Detect all cycles
integrations_after_dependencies = (
await loader.resolve_integrations_after_dependencies(
hass, all_integrations.values(), set(all_integrations)
)
)
all_domains = set(integrations_after_dependencies)
domains = set(integrations) & all_domains
all_domains = set(all_integrations)
domains = set(integrations)
_LOGGER.info(
"Domains to be set up: %s | %s",
@@ -874,8 +868,6 @@ async def _async_set_up_integrations(
all_domains - domains,
)
async_set_domains_to_be_loaded(hass, all_domains)
# Initialize recorder
if "recorder" in all_domains:
recorder.async_initialize_recorder(hass)
@@ -908,12 +900,24 @@ async def _async_set_up_integrations(
stage_dep_domains_unfiltered = {
dep
for domain in stage_domains
for dep in integrations_after_dependencies[domain]
for dep in all_integrations[domain].all_dependencies
if dep not in stage_domains
}
stage_dep_domains = stage_dep_domains_unfiltered - hass.config.components
stage_all_domains = stage_domains | stage_dep_domains
stage_all_integrations = {
domain: all_integrations[domain] for domain in stage_all_domains
}
# Detect all cycles
stage_integrations_after_dependencies = (
await loader.resolve_integrations_after_dependencies(
hass, stage_all_integrations.values(), stage_all_domains
)
)
stage_all_domains = set(stage_integrations_after_dependencies)
stage_domains &= stage_all_domains
stage_dep_domains &= stage_all_domains
_LOGGER.info(
"Setting up stage %s: %s | %s\nDependencies: %s | %s",
@@ -924,6 +928,8 @@ async def _async_set_up_integrations(
stage_dep_domains_unfiltered - stage_dep_domains,
)
async_set_domains_to_be_loaded(hass, stage_all_domains)
if timeout is None:
await _async_setup_multi_components(hass, stage_all_domains, config)
continue

View File

@@ -0,0 +1,5 @@
{
"domain": "bosch",
"name": "Bosch",
"integrations": ["bosch_alarm", "bosch_shc", "home_connect"]
}

View File

@@ -2,7 +2,7 @@
"config": {
"step": {
"geography_by_coords": {
"title": "Configure a Geography",
"title": "Configure a geography",
"description": "Use the AirVisual cloud API to monitor a latitude/longitude.",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
@@ -56,12 +56,12 @@
"sensor": {
"pollutant_label": {
"state": {
"co": "Carbon Monoxide",
"n2": "Nitrogen Dioxide",
"co": "Carbon monoxide",
"n2": "Nitrogen dioxide",
"o3": "Ozone",
"p1": "PM10",
"p2": "PM2.5",
"s2": "Sulfur Dioxide"
"s2": "Sulfur dioxide"
}
},
"pollutant_level": {

View File

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

View File

@@ -1438,7 +1438,7 @@ class AlexaModeController(AlexaCapability):
# Fan preset_mode
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
mode = self.entity.attributes.get(fan.ATTR_PRESET_MODE, None)
if mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, None):
if mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, ()):
return f"{fan.ATTR_PRESET_MODE}.{mode}"
# Humidifier mode

View File

@@ -240,6 +240,7 @@ SENSOR_DESCRIPTIONS = (
suggested_display_precision=0,
entity_registry_enabled_default=False,
device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
),
SensorEntityDescription(
key=TYPE_WINDGUSTMPH,

View File

@@ -609,6 +609,7 @@ SENSOR_DESCRIPTIONS = (
translation_key="wind_direction",
native_unit_of_measurement=DEGREE,
device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
),
SensorEntityDescription(
key=TYPE_WINDDIR_AVG10M,

View File

@@ -6,7 +6,11 @@ import logging
from typing import Any
from homeassistant.components import mqtt
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.const import DEGREE, UnitOfPrecipitationDepth, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -98,6 +102,7 @@ def discover_sensors(topic: str, payload: dict[str, Any]) -> list[ArwnSensor] |
DEGREE,
"mdi:compass",
device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
),
]
return None
@@ -178,6 +183,7 @@ class ArwnSensor(SensorEntity):
units: str,
icon: str | None = None,
device_class: SensorDeviceClass | None = None,
state_class: SensorStateClass | None = None,
) -> None:
"""Initialize the sensor."""
self.entity_id = _slug(name)
@@ -188,6 +194,7 @@ class ArwnSensor(SensorEntity):
self._attr_native_unit_of_measurement = units
self._attr_icon = icon
self._attr_device_class = device_class
self._attr_state_class = state_class
def set_event(self, event: dict[str, Any]) -> None:
"""Update the sensor with the most recent event."""

View File

@@ -1,9 +1,11 @@
"""Base class for assist satellite entities."""
import logging
from pathlib import Path
import voluptuous as vol
from homeassistant.components.http import StaticPathConfig
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
@@ -15,6 +17,8 @@ from .const import (
CONNECTION_TEST_DATA,
DATA_COMPONENT,
DOMAIN,
PREANNOUNCE_FILENAME,
PREANNOUNCE_URL,
AssistSatelliteEntityFeature,
)
from .entity import (
@@ -56,7 +60,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
{
vol.Optional("message"): str,
vol.Optional("media_id"): str,
vol.Optional("preannounce_media_id"): str,
vol.Optional("preannounce_media_id"): vol.Any(str, None),
}
),
cv.has_at_least_one_key("message", "media_id"),
@@ -71,7 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
{
vol.Optional("start_message"): str,
vol.Optional("start_media_id"): str,
vol.Optional("preannounce_media_id"): str,
vol.Optional("preannounce_media_id"): vol.Any(str, None),
vol.Optional("extra_system_prompt"): str,
}
),
@@ -84,6 +88,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async_register_websocket_api(hass)
hass.http.register_view(ConnectionTestView())
# Default preannounce sound
await hass.http.async_register_static_paths(
[
StaticPathConfig(
PREANNOUNCE_URL, str(Path(__file__).parent / PREANNOUNCE_FILENAME)
)
]
)
return True

View File

@@ -20,6 +20,9 @@ CONNECTION_TEST_DATA: HassKey[dict[str, asyncio.Event]] = HassKey(
f"{DOMAIN}_connection_tests"
)
PREANNOUNCE_FILENAME = "preannounce.mp3"
PREANNOUNCE_URL = f"/api/assist_satellite/static/{PREANNOUNCE_FILENAME}"
class AssistSatelliteEntityFeature(IntFlag):
"""Supported features of Assist satellite entity."""

View File

@@ -28,7 +28,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import chat_session, entity
from homeassistant.helpers.entity import EntityDescription
from .const import AssistSatelliteEntityFeature
from .const import PREANNOUNCE_URL, AssistSatelliteEntityFeature
from .errors import AssistSatelliteError, SatelliteBusyError
_LOGGER = logging.getLogger(__name__)
@@ -180,7 +180,7 @@ class AssistSatelliteEntity(entity.Entity):
self,
message: str | None = None,
media_id: str | None = None,
preannounce_media_id: str | None = None,
preannounce_media_id: str | None = PREANNOUNCE_URL,
) -> None:
"""Play and show an announcement on the satellite.
@@ -190,7 +190,8 @@ class AssistSatelliteEntity(entity.Entity):
If media_id is provided, it is played directly. It is possible
to omit the message and the satellite will not show any text.
If preannounce_media_id is provided, it is played before the announcement.
If preannounce_media_id is provided, it overrides the default sound.
If preannounce_media_id is None, no sound is played.
Calls async_announce with message and media id.
"""
@@ -228,7 +229,7 @@ class AssistSatelliteEntity(entity.Entity):
start_message: str | None = None,
start_media_id: str | None = None,
extra_system_prompt: str | None = None,
preannounce_media_id: str | None = None,
preannounce_media_id: str | None = PREANNOUNCE_URL,
) -> None:
"""Start a conversation from the satellite.
@@ -239,6 +240,7 @@ class AssistSatelliteEntity(entity.Entity):
to omit the message and the satellite will not show any text.
If preannounce_media_id is provided, it is played before the announcement.
If preannounce_media_id is None, no sound is played.
Calls async_start_conversation.
"""

View File

@@ -8,6 +8,7 @@ announce:
message:
required: false
example: "Time to wake up!"
default: ""
selector:
text:
media_id:
@@ -28,6 +29,7 @@ start_conversation:
start_message:
required: false
example: "You left the lights on in the living room. Turn them off?"
default: ""
selector:
text:
start_media_id:

View File

@@ -198,7 +198,8 @@ async def websocket_test_connection(
hass.async_create_background_task(
satellite.async_internal_announce(
media_id=f"{CONNECTION_TEST_URL_BASE}/{connection_id}"
media_id=f"{CONNECTION_TEST_URL_BASE}/{connection_id}",
preannounce_media_id=None,
),
f"assist_satellite_connection_test_{msg['entity_id']}",
)

View File

@@ -0,0 +1,62 @@
"""The Bosch Alarm integration."""
from __future__ import annotations
from ssl import SSLError
from bosch_alarm_mode2 import Panel
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
PLATFORMS: list[Platform] = [Platform.ALARM_CONTROL_PANEL]
type BoschAlarmConfigEntry = ConfigEntry[Panel]
async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -> bool:
"""Set up Bosch Alarm from a config entry."""
panel = Panel(
host=entry.data[CONF_HOST],
port=entry.data[CONF_PORT],
automation_code=entry.data.get(CONF_PASSWORD),
installer_or_user_code=entry.data.get(
CONF_INSTALLER_CODE, entry.data.get(CONF_USER_CODE)
),
)
try:
await panel.connect()
except (PermissionError, ValueError) as err:
await panel.disconnect()
raise ConfigEntryNotReady from err
except (TimeoutError, OSError, ConnectionRefusedError, SSLError) as err:
await panel.disconnect()
raise ConfigEntryNotReady("Connection failed") from err
entry.runtime_data = panel
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, entry.unique_id or entry.entry_id)},
name=f"Bosch {panel.model}",
manufacturer="Bosch Security Systems",
model=panel.model,
sw_version=panel.firmware_version,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
await entry.runtime_data.disconnect()
return unload_ok

View File

@@ -0,0 +1,109 @@
"""Support for Bosch Alarm Panel."""
from __future__ import annotations
from bosch_alarm_mode2 import Panel
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
AlarmControlPanelState,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BoschAlarmConfigEntry
from .const import DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BoschAlarmConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up control panels for each area."""
panel = config_entry.runtime_data
async_add_entities(
AreaAlarmControlPanel(
panel,
area_id,
config_entry.unique_id or config_entry.entry_id,
)
for area_id in panel.areas
)
class AreaAlarmControlPanel(AlarmControlPanelEntity):
"""An alarm control panel entity for a bosch alarm panel."""
_attr_has_entity_name = True
_attr_supported_features = (
AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY
)
_attr_code_arm_required = False
_attr_name = None
def __init__(self, panel: Panel, area_id: int, unique_id: str) -> None:
"""Initialise a Bosch Alarm control panel entity."""
self.panel = panel
self._area = panel.areas[area_id]
self._area_id = area_id
self._attr_unique_id = f"{unique_id}_area_{area_id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)},
name=self._area.name,
manufacturer="Bosch Security Systems",
via_device=(
DOMAIN,
unique_id,
),
)
@property
def alarm_state(self) -> AlarmControlPanelState | None:
"""Return the state of the alarm."""
if self._area.is_triggered():
return AlarmControlPanelState.TRIGGERED
if self._area.is_disarmed():
return AlarmControlPanelState.DISARMED
if self._area.is_arming():
return AlarmControlPanelState.ARMING
if self._area.is_pending():
return AlarmControlPanelState.PENDING
if self._area.is_part_armed():
return AlarmControlPanelState.ARMED_HOME
if self._area.is_all_armed():
return AlarmControlPanelState.ARMED_AWAY
return None
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Disarm this panel."""
await self.panel.area_disarm(self._area_id)
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
await self.panel.area_arm_part(self._area_id)
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
await self.panel.area_arm_all(self._area_id)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self.panel.connection_status()
async def async_added_to_hass(self) -> None:
"""Run when entity attached to hass."""
await super().async_added_to_hass()
self._area.status_observer.attach(self.schedule_update_ha_state)
self.panel.connection_status_observer.attach(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Run when entity removed from hass."""
await super().async_will_remove_from_hass()
self._area.status_observer.detach(self.schedule_update_ha_state)
self.panel.connection_status_observer.detach(self.schedule_update_ha_state)

View File

@@ -0,0 +1,165 @@
"""Config flow for Bosch Alarm integration."""
from __future__ import annotations
import asyncio
import logging
import ssl
from typing import Any
from bosch_alarm_mode2 import Panel
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_CODE,
CONF_HOST,
CONF_MODEL,
CONF_PASSWORD,
CONF_PORT,
)
import homeassistant.helpers.config_validation as cv
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=7700): cv.positive_int,
}
)
STEP_AUTH_DATA_SCHEMA_SOLUTION = vol.Schema(
{
vol.Required(CONF_USER_CODE): str,
}
)
STEP_AUTH_DATA_SCHEMA_AMAX = vol.Schema(
{
vol.Required(CONF_INSTALLER_CODE): str,
vol.Required(CONF_PASSWORD): str,
}
)
STEP_AUTH_DATA_SCHEMA_BG = vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
}
)
STEP_INIT_DATA_SCHEMA = vol.Schema({vol.Optional(CONF_CODE): str})
async def try_connect(
data: dict[str, Any], load_selector: int = 0
) -> tuple[str, int | None]:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
panel = Panel(
host=data[CONF_HOST],
port=data[CONF_PORT],
automation_code=data.get(CONF_PASSWORD),
installer_or_user_code=data.get(CONF_INSTALLER_CODE, data.get(CONF_USER_CODE)),
)
try:
await panel.connect(load_selector)
finally:
await panel.disconnect()
return (panel.model, panel.serial_number)
class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Bosch Alarm."""
def __init__(self) -> None:
"""Init config flow."""
self._data: dict[str, Any] = {}
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
try:
# Use load_selector = 0 to fetch the panel model without authentication.
(model, serial) = await try_connect(user_input, 0)
except (
OSError,
ConnectionRefusedError,
ssl.SSLError,
asyncio.exceptions.TimeoutError,
) as e:
_LOGGER.error("Connection Error: %s", e)
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
self._data = user_input
self._data[CONF_MODEL] = model
return await self.async_step_auth()
return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(
STEP_USER_DATA_SCHEMA, user_input
),
errors=errors,
)
async def async_step_auth(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the auth step."""
errors: dict[str, str] = {}
# Each model variant requires a different authentication flow
if "Solution" in self._data[CONF_MODEL]:
schema = STEP_AUTH_DATA_SCHEMA_SOLUTION
elif "AMAX" in self._data[CONF_MODEL]:
schema = STEP_AUTH_DATA_SCHEMA_AMAX
else:
schema = STEP_AUTH_DATA_SCHEMA_BG
if user_input is not None:
self._data.update(user_input)
try:
(model, serial_number) = await try_connect(
self._data, Panel.LOAD_EXTENDED_INFO
)
except (PermissionError, ValueError) as e:
errors["base"] = "invalid_auth"
_LOGGER.error("Authentication Error: %s", e)
except (
OSError,
ConnectionRefusedError,
ssl.SSLError,
TimeoutError,
) as e:
_LOGGER.error("Connection Error: %s", e)
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
if serial_number:
await self.async_set_unique_id(str(serial_number))
self._abort_if_unique_id_configured()
else:
self._async_abort_entries_match({CONF_HOST: self._data[CONF_HOST]})
return self.async_create_entry(title=f"Bosch {model}", data=self._data)
return self.async_show_form(
step_id="auth",
data_schema=self.add_suggested_values_to_schema(schema, user_input),
errors=errors,
)

View File

@@ -0,0 +1,6 @@
"""Constants for the Bosch Alarm integration."""
DOMAIN = "bosch_alarm"
HISTORY_ATTR = "history"
CONF_INSTALLER_CODE = "installer_code"
CONF_USER_CODE = "user_code"

View File

@@ -0,0 +1,11 @@
{
"domain": "bosch_alarm",
"name": "Bosch Alarm",
"codeowners": ["@mag1024", "@sanjay900"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bosch_alarm",
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "bronze",
"requirements": ["bosch-alarm-mode2==0.4.3"]
}

View File

@@ -0,0 +1,84 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
No custom actions defined
appropriate-polling:
status: exempt
comment: |
No polling
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
No custom actions are defined.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: |
No custom actions are defined.
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: todo
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices:
status: exempt
comment: |
Device type integration
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations: todo
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: |
No repairs
stale-devices:
status: exempt
comment: |
Device type integration
# Platinum
async-dependency: done
inject-websession:
status: exempt
comment: |
Integration does not make any HTTP requests.
strict-typing: done

View File

@@ -0,0 +1,36 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of your Bosch alarm panel",
"port": "The port used to connect to your Bosch alarm panel. This is usually 7700"
}
},
"auth": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"installer_code": "Installer code",
"user_code": "User code"
},
"data_description": {
"password": "The Mode 2 automation code from your panel",
"installer_code": "The installer code from your panel",
"user_code": "The user code from your panel"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View File

@@ -170,6 +170,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
native_unit_of_measurement=DEGREE,
icon="mdi:compass-outline",
device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
),
SensorEntityDescription(
key="pressure",

View File

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

View File

@@ -4,13 +4,14 @@ from __future__ import annotations
import asyncio
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
from http import HTTPStatus
import logging
import random
from typing import Any
from aiohttp import ClientError
from aiohttp import ClientError, ClientResponseError
from hass_nabucasa import Cloud, CloudError
from hass_nabucasa.api import CloudApiNonRetryableError
from hass_nabucasa.api import CloudApiError, CloudApiNonRetryableError
from hass_nabucasa.cloud_api import (
FilesHandlerListEntry,
async_files_delete_file,
@@ -120,6 +121,8 @@ class CloudBackupAgent(BackupAgent):
"""
if not backup.protected:
raise BackupAgentError("Cloud backups must be protected")
if self._cloud.subscription_expired:
raise BackupAgentError("Cloud subscription has expired")
size = backup.size
try:
@@ -152,6 +155,13 @@ class CloudBackupAgent(BackupAgent):
) from err
raise BackupAgentError(f"Failed to upload backup {err}") from err
except CloudError as err:
if (
isinstance(err, CloudApiError)
and isinstance(err.orig_exc, ClientResponseError)
and err.orig_exc.status == HTTPStatus.FORBIDDEN
and self._cloud.subscription_expired
):
raise BackupAgentError("Cloud subscription has expired") from err
if tries == _RETRY_LIMIT:
raise BackupAgentError(f"Failed to upload backup {err}") from err
tries += 1

View File

@@ -4,19 +4,19 @@
"step": {
"user": {
"title": "Connect to Cloudflare",
"description": "This integration requires an API Token created with Zone:Zone:Read and Zone:DNS:Edit permissions for all zones in your account.",
"description": "This integration requires an API token created with Zone:Zone:Read and Zone:DNS:Edit permissions for all zones in your account.",
"data": {
"api_token": "[%key:common::config_flow::data::api_token%]"
}
},
"zone": {
"title": "Choose the Zone to Update",
"title": "Choose the zone to update",
"data": {
"zone": "Zone"
}
},
"records": {
"title": "Choose the Records to Update",
"title": "Choose the records to update",
"data": {
"records": "Records"
}
@@ -40,7 +40,7 @@
"services": {
"update_records": {
"name": "Update records",
"description": "Manually trigger update to Cloudflare records."
"description": "Manually triggers an update of Cloudflare records."
}
}
}

View File

@@ -41,6 +41,7 @@ ALARM_ACTIONS: dict[str, str] = {
ALARM_AREA_ARMED_STATUS: dict[str, int] = {
DISABLE: 0,
HOME_P1: 1,
HOME_P2: 2,
NIGHT: 3,
@@ -128,20 +129,38 @@ class ComelitAlarmEntity(CoordinatorEntity[ComelitVedoSystem], AlarmControlPanel
AlarmAreaState.TRIGGERED: AlarmControlPanelState.TRIGGERED,
}.get(self._area.human_status)
async def _async_update_state(self, area_state: AlarmAreaState, armed: int) -> None:
"""Update state after action."""
self._area.human_status = area_state
self._area.armed = armed
await self.async_update_ha_state()
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
if code != str(self._api.device_pin):
return
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[DISABLE])
await self._async_update_state(
AlarmAreaState.DISARMED, ALARM_AREA_ARMED_STATUS[DISABLE]
)
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[AWAY])
await self._async_update_state(
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[AWAY]
)
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[HOME])
await self._async_update_state(
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[HOME_P1]
)
async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command."""
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[NIGHT])
await self._async_update_state(
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[NIGHT]
)

View File

@@ -9,3 +9,5 @@ _LOGGER = logging.getLogger(__package__)
DOMAIN = "comelit"
DEFAULT_PORT = 80
DEVICE_TYPE_LIST = [BRIDGE, VEDO]
SCAN_INTERVAL = 5

View File

@@ -22,7 +22,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import _LOGGER, DOMAIN
from .const import _LOGGER, DOMAIN, SCAN_INTERVAL
type ComelitConfigEntry = ConfigEntry[ComelitBaseCoordinator]
@@ -53,7 +53,7 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[T]):
logger=_LOGGER,
config_entry=entry,
name=f"{DOMAIN}-{host}-coordinator",
update_interval=timedelta(seconds=5),
update_interval=timedelta(seconds=SCAN_INTERVAL),
)
device_registry = dr.async_get(self.hass)
device_registry.async_get_or_create(

View File

@@ -650,7 +650,14 @@ class DefaultAgent(ConversationEntity):
if (
(maybe_result is None) # first result
or (num_matched_entities > best_num_matched_entities)
or (
# More literal text matched
result.text_chunks_matched > maybe_result.text_chunks_matched
)
or (
# More entities matched
num_matched_entities > best_num_matched_entities
)
or (
# Fewer unmatched entities
(num_matched_entities == best_num_matched_entities)
@@ -662,16 +669,6 @@ class DefaultAgent(ConversationEntity):
and (num_unmatched_entities == best_num_unmatched_entities)
and (num_unmatched_ranges > best_num_unmatched_ranges)
)
or (
# More literal text matched
(num_matched_entities == best_num_matched_entities)
and (num_unmatched_entities == best_num_unmatched_entities)
and (num_unmatched_ranges == best_num_unmatched_ranges)
and (
result.text_chunks_matched
> maybe_result.text_chunks_matched
)
)
or (
# Prefer match failures with entities
(result.text_chunks_matched == maybe_result.text_chunks_matched)

View File

@@ -3,11 +3,13 @@
from __future__ import annotations
from collections.abc import Iterable
from dataclasses import asdict
from typing import Any
from aiohttp import web
from hassil.recognize import MISSING_ENTITY, RecognizeResult
from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity
from home_assistant_intents import get_language_scores
import voluptuous as vol
from homeassistant.components import http, websocket_api
@@ -38,6 +40,7 @@ def async_setup(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, websocket_list_agents)
websocket_api.async_register_command(hass, websocket_list_sentences)
websocket_api.async_register_command(hass, websocket_hass_agent_debug)
websocket_api.async_register_command(hass, websocket_hass_agent_language_scores)
@websocket_api.websocket_command(
@@ -336,6 +339,36 @@ def _get_unmatched_slots(
return unmatched_slots
@websocket_api.websocket_command(
{
vol.Required("type"): "conversation/agent/homeassistant/language_scores",
vol.Optional("language"): str,
vol.Optional("country"): str,
}
)
@websocket_api.async_response
async def websocket_hass_agent_language_scores(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Get support scores per language."""
language = msg.get("language", hass.config.language)
country = msg.get("country", hass.config.country)
scores = await hass.async_add_executor_job(get_language_scores)
matching_langs = language_util.matches(language, scores.keys(), country=country)
preferred_lang = matching_langs[0] if matching_langs else language
result = {
"languages": {
lang_key: asdict(lang_scores) for lang_key, lang_scores in scores.items()
},
"preferred_language": preferred_lang,
}
connection.send_result(msg["id"], result)
class ConversationProcessView(http.HomeAssistantView):
"""View to process text."""

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.3.23"]
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.3.28"]
}

View File

@@ -50,10 +50,10 @@ class DukeEnergyConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
username = auth["cdp_internal_user_id"].lower()
username = auth["internalUserID"].lower()
await self.async_set_unique_id(username)
self._abort_if_unique_id_configured()
email = auth["email"].lower()
email = auth["loginEmailAddress"].lower()
data = {
CONF_EMAIL: email,
CONF_USERNAME: username,

View File

@@ -8,7 +8,11 @@ from aiodukeenergy import DukeEnergy
from aiohttp import ClientError
from homeassistant.components.recorder import get_instance
from homeassistant.components.recorder.models import StatisticData, StatisticMetaData
from homeassistant.components.recorder.models import (
StatisticData,
StatisticMeanType,
StatisticMetaData,
)
from homeassistant.components.recorder.statistics import (
async_add_external_statistics,
get_last_statistics,
@@ -137,7 +141,7 @@ class DukeEnergyCoordinator(DataUpdateCoordinator[None]):
f"Duke Energy {meter['serviceType'].capitalize()} {serial_number}"
)
consumption_metadata = StatisticMetaData(
has_mean=False,
mean_type=StatisticMeanType.NONE,
has_sum=True,
name=f"{name_prefix} Consumption",
source=DOMAIN,

View File

@@ -6,5 +6,5 @@
"dependencies": ["recorder"],
"documentation": "https://www.home-assistant.io/integrations/duke_energy",
"iot_class": "cloud_polling",
"requirements": ["aiodukeenergy==0.2.2"]
"requirements": ["aiodukeenergy==0.3.0"]
}

View File

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

View File

@@ -68,6 +68,7 @@ ECOWITT_SENSORS_MAPPING: Final = {
key="DEGREE",
native_unit_of_measurement=DEGREE,
device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
),
EcoWittSensorTypes.WATT_METERS_SQUARED: SensorEntityDescription(
key="WATT_METERS_SQUARED",

View File

@@ -7,7 +7,11 @@ from typing import TYPE_CHECKING, cast
from elvia import Elvia, error as ElviaError
from homeassistant.components.recorder.models import StatisticData, StatisticMetaData
from homeassistant.components.recorder.models import (
StatisticData,
StatisticMeanType,
StatisticMetaData,
)
from homeassistant.components.recorder.statistics import (
async_add_external_statistics,
get_last_statistics,
@@ -144,7 +148,7 @@ class ElviaImporter:
async_add_external_statistics(
hass=self.hass,
metadata=StatisticMetaData(
has_mean=False,
mean_type=StatisticMeanType.NONE,
has_sum=True,
name=f"{self.metering_point_id} Consumption",
source=DOMAIN,

View File

@@ -168,6 +168,7 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = (
native_unit_of_measurement=DEGREE,
value_fn=lambda data: data.conditions.get("wind_bearing", {}).get("value"),
device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
),
ECSensorEntityDescription(
key="wind_chill",

View File

@@ -370,8 +370,10 @@ class EsphomeAssistSatellite(
announcement.media_id,
)
media_id = announcement.media_id
if announcement.media_id_source != "tts":
# Route non-TTS media through the proxy
is_media_tts = announcement.media_id_source == "tts"
preannounce_media_id = announcement.preannounce_media_id
if (not is_media_tts) or preannounce_media_id:
# Route media through the proxy
format_to_use: MediaPlayerSupportedFormat | None = None
for supported_format in chain(
*self.entry_data.media_player_formats.values()
@@ -384,22 +386,33 @@ class EsphomeAssistSatellite(
assert (self.registry_entry is not None) and (
self.registry_entry.device_id is not None
)
proxy_url = async_create_proxy_url(
self.hass,
self.registry_entry.device_id,
media_id,
make_proxy_url = partial(
async_create_proxy_url,
hass=self.hass,
device_id=self.registry_entry.device_id,
media_format=format_to_use.format,
rate=format_to_use.sample_rate or None,
channels=format_to_use.num_channels or None,
width=format_to_use.sample_bytes or None,
)
media_id = async_process_play_media_url(self.hass, proxy_url)
if not is_media_tts:
media_id = async_process_play_media_url(
self.hass, make_proxy_url(media_url=media_id)
)
if preannounce_media_id:
preannounce_media_id = async_process_play_media_url(
self.hass, make_proxy_url(media_url=preannounce_media_id)
)
await self.cli.send_voice_assistant_announcement_await_response(
media_id,
_ANNOUNCEMENT_TIMEOUT_SEC,
announcement.message,
start_conversation=run_pipeline_after,
preannounce_media_id=preannounce_media_id or "",
)
async def handle_pipeline_start(

View File

@@ -33,6 +33,16 @@ class EsphomeEvent(EsphomeEntity[EventInfo, Event], EventEntity):
self._trigger_event(self._state.event_type)
self.async_write_ha_state()
@callback
def _on_device_update(self) -> None:
"""Call when device updates or entry data changes."""
super()._on_device_update()
if self._entry_data.available:
# Event entities should go available directly
# when the device comes online and not wait
# for the next data push.
self.async_write_ha_state()
async_setup_entry = partial(
platform_async_setup_entry,

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from datetime import timedelta
import logging
from typing import Any
from typing import Any, cast
import pyeverlights
import voluptuous as vol
@@ -84,7 +84,7 @@ class EverLightsLight(LightEntity):
api: pyeverlights.EverLights,
channel: int,
status: dict[str, Any],
effects,
effects: list[str],
) -> None:
"""Initialize the light."""
self._api = api
@@ -106,8 +106,10 @@ class EverLightsLight(LightEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
hs_color = kwargs.get(ATTR_HS_COLOR, self._attr_hs_color)
brightness = kwargs.get(ATTR_BRIGHTNESS, self._attr_brightness)
hs_color = cast(
tuple[float, float], kwargs.get(ATTR_HS_COLOR, self._attr_hs_color)
)
brightness = cast(int, kwargs.get(ATTR_BRIGHTNESS, self._attr_brightness))
effect = kwargs.get(ATTR_EFFECT)
if effect is not None:
@@ -116,7 +118,7 @@ class EverLightsLight(LightEntity):
rgb = color_int_to_rgb(colors[0])
hsv = color_util.color_RGB_to_hsv(*rgb)
hs_color = hsv[:2]
brightness = hsv[2] / 100 * 255
brightness = round(hsv[2] / 100 * 255)
else:
rgb = color_util.color_hsv_to_RGB(

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from collections import namedtuple
from datetime import timedelta
import logging
from typing import Any
from typing import Any, cast
from fints.client import FinTS3PinTanClient
from fints.models import SEPAAccount
@@ -73,7 +73,7 @@ def setup_platform(
credentials = BankCredentials(
config[CONF_BIN], config[CONF_USERNAME], config[CONF_PIN], config[CONF_URL]
)
fints_name = config.get(CONF_NAME, config[CONF_BIN])
fints_name = cast(str, config.get(CONF_NAME, config[CONF_BIN]))
account_config = {
acc[CONF_ACCOUNT]: acc[CONF_NAME] for acc in config[CONF_ACCOUNTS]

View File

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

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
import mimetypes
from pathlib import Path
from google import genai # type: ignore[attr-defined]
from google.genai import Client
from google.genai.errors import APIError, ClientError
from requests.exceptions import Timeout
import voluptuous as vol
@@ -43,7 +43,7 @@ CONF_FILENAMES = "filenames"
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = (Platform.CONVERSATION,)
type GoogleGenerativeAIConfigEntry = ConfigEntry[genai.Client]
type GoogleGenerativeAIConfigEntry = ConfigEntry[Client]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@@ -139,7 +139,11 @@ async def async_setup_entry(
"""Set up Google Generative AI Conversation from a config entry."""
try:
client = genai.Client(api_key=entry.data[CONF_API_KEY])
def _init_client() -> Client:
return Client(api_key=entry.data[CONF_API_KEY])
client = await hass.async_add_executor_job(_init_client)
await client.aio.models.get(
model=entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL),
config={"http_options": {"timeout": TIMEOUT_MILLIS}},

View File

@@ -16,13 +16,13 @@
"name": "Panel light"
},
"quiet": {
"name": "Quiet"
"name": "Quiet mode"
},
"fresh_air": {
"name": "Fresh air"
},
"xfan": {
"name": "XFan"
"name": "Xtra fan"
},
"health_mode": {
"name": "Health mode"

View File

@@ -244,6 +244,7 @@ class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
BSH_DOOR_STATE_LOCKED: False,
BSH_DOOR_STATE_OPEN: True,
},
entity_registry_enabled_default=False,
),
)
self._attr_unique_id = f"{appliance.info.ha_id}-Door"
@@ -283,7 +284,8 @@ class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
DOMAIN,
f"deprecated_binary_common_door_sensor_{self.entity_id}",
breaks_in_ha_version="2025.5.0",
is_fixable=False,
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_binary_common_door_sensor",
translation_placeholders={

View File

@@ -207,11 +207,13 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
brightness = round(
color_util.brightness_to_value(
self._brightness_scale,
kwargs.get(ATTR_BRIGHTNESS, self._attr_brightness),
cast(int, kwargs.get(ATTR_BRIGHTNESS, self._attr_brightness)),
)
)
hs_color = kwargs.get(ATTR_HS_COLOR, self._attr_hs_color)
hs_color = cast(
tuple[float, float], kwargs.get(ATTR_HS_COLOR, self._attr_hs_color)
)
rgb = color_util.color_hsv_to_RGB(hs_color[0], hs_color[1], brightness)
hex_val = color_util.color_rgb_to_hex(*rgb)

View File

@@ -26,6 +26,11 @@ _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
NUMBERS = (
NumberEntityDescription(
key=SettingKey.BSH_COMMON_ALARM_CLOCK,
device_class=NumberDeviceClass.DURATION,
translation_key="alarm_clock",
),
NumberEntityDescription(
key=SettingKey.REFRIGERATION_FRIDGE_FREEZER_SETPOINT_TEMPERATURE_REFRIGERATOR,
device_class=NumberDeviceClass.TEMPERATURE,

View File

@@ -110,17 +110,71 @@
}
},
"issues": {
"deprecated_time_alarm_clock_in_automations_scripts": {
"title": "Deprecated alarm clock entity detected in some automations or scripts",
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::home_connect::issues::deprecated_time_alarm_clock_in_automations_scripts::title%]",
"description": "The alarm clock entity `{entity_id}`, which is deprecated because it's being moved to the `number` platform, is used in the following automations or scripts:\n{items}\n\nPlease, fix this issue by updating your automations or scripts to use the new `number` entity."
}
}
}
},
"deprecated_time_alarm_clock": {
"title": "Deprecated alarm clock entity",
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::home_connect::issues::deprecated_time_alarm_clock::title%]",
"description": "The alarm clock entity `{entity_id}` is deprecated because it's being moved to the `number` platform.\n\nPlease use the new `number` entity."
}
}
}
},
"deprecated_binary_common_door_sensor": {
"title": "Deprecated binary door sensor detected in some automations or scripts",
"description": "The binary door sensor `{entity}`, which is deprecated, is used in the following automations or scripts:\n{items}\n\nA sensor entity with additional possible states is available and should be used going forward; Please use it on the above automations or scripts to fix this issue."
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::home_connect::issues::deprecated_binary_common_door_sensor::title%]",
"description": "The binary door sensor `{entity}`, which is deprecated, is used in the following automations or scripts:\n{items}\n\nA sensor entity with additional possible states is available and should be used going forward; Please use it on the above automations or scripts to fix this issue."
}
}
}
},
"deprecated_command_actions": {
"title": "The command related actions are deprecated in favor of the new buttons",
"description": "The `pause_program` and `resume_program` actions have been deprecated in favor of new button entities, if the command is available for your appliance. Please update your automations, scripts and panels that use this action to use the button entities instead, and press on submit to fix the issue."
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::home_connect::issues::deprecated_command_actions::title%]",
"description": "The `pause_program` and `resume_program` actions have been deprecated in favor of new button entities, if the command is available for your appliance. Please update your automations, scripts and panels that use this action to use the button entities instead, and press on submit to fix the issue."
}
}
}
},
"deprecated_program_switch_in_automations_scripts": {
"title": "Deprecated program switch detected in some automations or scripts",
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::home_connect::issues::deprecated_program_switch_in_automations_scripts::title%]",
"description": "Program switches are deprecated and {entity_id} is used in the following automations or scripts:\n{items}\n\nYou can use the active program select entity to run the program without any additional options and get the current running program on the above automations or scripts to fix this issue."
}
}
}
},
"deprecated_program_switch": {
"title": "Deprecated program switch detected in some automations or scripts",
"description": "Program switches are deprecated and {entity_id} is used in the following automations or scripts:\n{items}\n\nYou can use the active program select entity to run the program without any additional options and get the current running program on the above automations or scripts to fix this issue."
"title": "Deprecated program switch entities",
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::home_connect::issues::deprecated_program_switch::title%]",
"description": "The switch entity `{entity_id}` and all the other program switches are deprecated.\n\nPlease use the active program select entity instead."
}
}
}
},
"deprecated_set_program_and_option_actions": {
"title": "The executed action is deprecated",
@@ -868,6 +922,9 @@
}
},
"number": {
"alarm_clock": {
"name": "Alarm clock"
},
"refrigerator_setpoint_temperature": {
"name": "Refrigerator temperature"
},

View File

@@ -266,7 +266,10 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
super().__init__(
coordinator,
appliance,
SwitchEntityDescription(key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM),
SwitchEntityDescription(
key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
entity_registry_enabled_default=False,
),
)
self._attr_name = f"{appliance.info.name} {desc}"
self._attr_unique_id = f"{appliance.info.ha_id}-{desc}"
@@ -304,11 +307,12 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
async_create_issue(
self.hass,
DOMAIN,
f"deprecated_program_switch_{self.entity_id}",
f"deprecated_program_switch_in_automations_scripts_{self.entity_id}",
breaks_in_ha_version="2025.6.0",
is_fixable=False,
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_program_switch",
translation_key="deprecated_program_switch_in_automations_scripts",
translation_placeholders={
"entity_id": self.entity_id,
"items": "\n".join(items_list),
@@ -317,12 +321,34 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
async def async_will_remove_from_hass(self) -> None:
"""Call when entity will be removed from hass."""
async_delete_issue(
self.hass,
DOMAIN,
f"deprecated_program_switch_in_automations_scripts_{self.entity_id}",
)
async_delete_issue(
self.hass, DOMAIN, f"deprecated_program_switch_{self.entity_id}"
)
def create_action_handler_issue(self) -> None:
"""Create deprecation issue."""
async_create_issue(
self.hass,
DOMAIN,
f"deprecated_program_switch_{self.entity_id}",
breaks_in_ha_version="2025.6.0",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_program_switch",
translation_placeholders={
"entity_id": self.entity_id,
},
)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Start the program."""
self.create_action_handler_issue()
try:
await self.coordinator.client.start_program(
self.appliance.info.ha_id, program_key=self.program.key
@@ -339,6 +365,7 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Stop the program."""
self.create_action_handler_issue()
try:
await self.coordinator.client.stop_program(self.appliance.info.ha_id)
except HomeConnectError as err:

View File

@@ -6,10 +6,18 @@ from typing import cast
from aiohomeconnect.model import SettingKey
from aiohomeconnect.model.error import HomeConnectError
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.script import scripts_with_entity
from homeassistant.components.time import TimeEntity, TimeEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from .common import setup_home_connect_entry
from .const import DOMAIN
@@ -23,6 +31,7 @@ TIME_ENTITIES = (
TimeEntityDescription(
key=SettingKey.BSH_COMMON_ALARM_CLOCK,
translation_key="alarm_clock",
entity_registry_enabled_default=False,
),
)
@@ -67,8 +76,78 @@ def time_to_seconds(t: time) -> int:
class HomeConnectTimeEntity(HomeConnectEntity, TimeEntity):
"""Time setting class for Home Connect."""
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
await super().async_added_to_hass()
if self.bsh_key == SettingKey.BSH_COMMON_ALARM_CLOCK:
automations = automations_with_entity(self.hass, self.entity_id)
scripts = scripts_with_entity(self.hass, self.entity_id)
items = automations + scripts
if not items:
return
entity_reg: er.EntityRegistry = er.async_get(self.hass)
entity_automations = [
automation_entity
for automation_id in automations
if (automation_entity := entity_reg.async_get(automation_id))
]
entity_scripts = [
script_entity
for script_id in scripts
if (script_entity := entity_reg.async_get(script_id))
]
items_list = [
f"- [{item.original_name}](/config/automation/edit/{item.unique_id})"
for item in entity_automations
] + [
f"- [{item.original_name}](/config/script/edit/{item.unique_id})"
for item in entity_scripts
]
async_create_issue(
self.hass,
DOMAIN,
f"deprecated_time_alarm_clock_in_automations_scripts_{self.entity_id}",
breaks_in_ha_version="2025.10.0",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_time_alarm_clock",
translation_placeholders={
"entity_id": self.entity_id,
"items": "\n".join(items_list),
},
)
async def async_will_remove_from_hass(self) -> None:
"""Call when entity will be removed from hass."""
if self.bsh_key == SettingKey.BSH_COMMON_ALARM_CLOCK:
async_delete_issue(
self.hass,
DOMAIN,
f"deprecated_time_alarm_clock_in_automations_scripts_{self.entity_id}",
)
async_delete_issue(
self.hass, DOMAIN, f"deprecated_time_alarm_clock_{self.entity_id}"
)
async def async_set_value(self, value: time) -> None:
"""Set the native value of the entity."""
async_create_issue(
self.hass,
DOMAIN,
f"deprecated_time_alarm_clock_{self.entity_id}",
breaks_in_ha_version="2025.10.0",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_time_alarm_clock",
translation_placeholders={
"entity_id": self.entity_id,
},
)
try:
await self.coordinator.client.set_setting(
self.appliance.info.ha_id,

View File

@@ -31,7 +31,6 @@ class FirmwareUpdateCoordinator(DataUpdateCoordinator[FirmwareManifest]):
_LOGGER,
name="firmware update coordinator",
update_interval=FIRMWARE_REFRESH_INTERVAL,
always_update=False,
)
self.hass = hass
self.session = session

View File

@@ -199,7 +199,7 @@ class BaseFirmwareUpdateEntity(
# This entity is not currently associated with a device so we must manually
# give it a name
self._attr_name = f"{self._config_entry.title} Update"
self._attr_title = self.entity_description.firmware_name or "unknown"
self._attr_title = self.entity_description.firmware_name or "Unknown"
if (
self._current_firmware_info is None

View File

@@ -15,14 +15,13 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a Home Assistant SkyConnect config entry."""
await hass.config_entries.async_forward_entry_setups(entry, ["update"])
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
await hass.config_entries.async_unload_platforms(entry, ["update"])
return True

View File

@@ -21,11 +21,20 @@ from homeassistant.components.update import UpdateDeviceClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import FIRMWARE, FIRMWARE_VERSION, NABU_CASA_FIRMWARE_RELEASES_URL
from .const import (
DOMAIN,
FIRMWARE,
FIRMWARE_VERSION,
NABU_CASA_FIRMWARE_RELEASES_URL,
PRODUCT,
SERIAL_NUMBER,
HardwareVariant,
)
_LOGGER = logging.getLogger(__name__)
@@ -42,7 +51,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
fw_type="skyconnect_zigbee_ncp",
version_key="ezsp_version",
expected_firmware_type=ApplicationType.EZSP,
firmware_name="EmberZNet",
firmware_name="EmberZNet Zigbee",
),
ApplicationType.SPINEL: FirmwareUpdateEntityDescription(
key="firmware",
@@ -55,6 +64,28 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
expected_firmware_type=ApplicationType.SPINEL,
firmware_name="OpenThread RCP",
),
ApplicationType.CPC: FirmwareUpdateEntityDescription(
key="firmware",
display_precision=0,
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=EntityCategory.CONFIG,
version_parser=lambda fw: fw,
fw_type="skyconnect_multipan",
version_key="cpc_version",
expected_firmware_type=ApplicationType.CPC,
firmware_name="Multiprotocol",
),
ApplicationType.GECKO_BOOTLOADER: FirmwareUpdateEntityDescription(
key="firmware",
display_precision=0,
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=EntityCategory.CONFIG,
version_parser=lambda fw: fw,
fw_type=None, # We don't want to update the bootloader
version_key="gecko_bootloader_version",
expected_firmware_type=ApplicationType.GECKO_BOOTLOADER,
firmware_name="Gecko Bootloader",
),
None: FirmwareUpdateEntityDescription(
key="firmware",
display_precision=0,
@@ -77,9 +108,16 @@ def _async_create_update_entity(
) -> FirmwareUpdateEntity:
"""Create an update entity that handles firmware type changes."""
firmware_type = config_entry.data[FIRMWARE]
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[
ApplicationType(firmware_type) if firmware_type is not None else None
]
try:
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[
ApplicationType(firmware_type)
]
except (KeyError, ValueError):
_LOGGER.debug(
"Unknown firmware type %r, using default entity description", firmware_type
)
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[None]
entity = FirmwareUpdateEntity(
device=config_entry.data["device"],
@@ -130,6 +168,7 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
"""SkyConnect firmware update entity."""
bootloader_reset_type = None
_attr_has_entity_name = True
def __init__(
self,
@@ -141,8 +180,18 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
"""Initialize the SkyConnect firmware update entity."""
super().__init__(device, config_entry, update_coordinator, entity_description)
self._attr_unique_id = (
f"{self._config_entry.data['serial_number']}_{self.entity_description.key}"
variant = HardwareVariant.from_usb_product_name(
self._config_entry.data[PRODUCT]
)
serial_number = self._config_entry.data[SERIAL_NUMBER]
self._attr_unique_id = f"{serial_number}_{self.entity_description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_number)},
name=f"{variant.full_name} ({serial_number[:8]})",
model=variant.full_name,
manufacturer="Nabu Casa",
serial_number=serial_number,
)
# Use the cached firmware info if it exists
@@ -155,6 +204,17 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
source="homeassistant_sky_connect",
)
def _update_attributes(self) -> None:
"""Recompute the attributes of the entity."""
super()._update_attributes()
assert self.device_entry is not None
device_registry = dr.async_get(self.hass)
device_registry.async_update_device(
device_id=self.device_entry.id,
sw_version=f"{self.entity_description.firmware_name} {self._attr_installed_version}",
)
@callback
def _firmware_info_callback(self, firmware_info: FirmwareInfo) -> None:
"""Handle updated firmware info being pushed by an integration."""

View File

@@ -62,6 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
await hass.config_entries.async_unload_platforms(entry, ["update"])
return True

View File

@@ -2,8 +2,9 @@
DOMAIN = "homeassistant_yellow"
RADIO_MODEL = "Home Assistant Yellow"
RADIO_MANUFACTURER = "Nabu Casa"
MODEL = "Home Assistant Yellow"
MANUFACTURER = "Nabu Casa"
RADIO_DEVICE = "/dev/ttyAMA1"
ZHA_HW_DISCOVERY_DATA = {

View File

@@ -149,5 +149,12 @@
"run_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::run_zigbee_flasher_addon%]",
"uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::uninstall_zigbee_flasher_addon%]"
}
},
"entity": {
"update": {
"firmware": {
"name": "Radio firmware"
}
}
}
}

View File

@@ -21,13 +21,17 @@ from homeassistant.components.update import UpdateDeviceClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
DOMAIN,
FIRMWARE,
FIRMWARE_VERSION,
MANUFACTURER,
MODEL,
NABU_CASA_FIRMWARE_RELEASES_URL,
RADIO_DEVICE,
)
@@ -39,7 +43,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
ApplicationType | None, FirmwareUpdateEntityDescription
] = {
ApplicationType.EZSP: FirmwareUpdateEntityDescription(
key="firmware",
key="radio_firmware",
display_precision=0,
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=EntityCategory.CONFIG,
@@ -47,10 +51,10 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
fw_type="yellow_zigbee_ncp",
version_key="ezsp_version",
expected_firmware_type=ApplicationType.EZSP,
firmware_name="EmberZNet",
firmware_name="EmberZNet Zigbee",
),
ApplicationType.SPINEL: FirmwareUpdateEntityDescription(
key="firmware",
key="radio_firmware",
display_precision=0,
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=EntityCategory.CONFIG,
@@ -60,12 +64,34 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
expected_firmware_type=ApplicationType.SPINEL,
firmware_name="OpenThread RCP",
),
None: FirmwareUpdateEntityDescription(
ApplicationType.CPC: FirmwareUpdateEntityDescription(
key="firmware",
display_precision=0,
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=EntityCategory.CONFIG,
version_parser=lambda fw: fw,
fw_type="yellow_multipan",
version_key="cpc_version",
expected_firmware_type=ApplicationType.CPC,
firmware_name="Multiprotocol",
),
ApplicationType.GECKO_BOOTLOADER: FirmwareUpdateEntityDescription(
key="firmware",
display_precision=0,
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=EntityCategory.CONFIG,
version_parser=lambda fw: fw,
fw_type=None, # We don't want to update the bootloader
version_key="gecko_bootloader_version",
expected_firmware_type=ApplicationType.GECKO_BOOTLOADER,
firmware_name="Gecko Bootloader",
),
None: FirmwareUpdateEntityDescription(
key="radio_firmware",
display_precision=0,
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=EntityCategory.CONFIG,
version_parser=lambda fw: fw,
fw_type=None,
version_key=None,
expected_firmware_type=None,
@@ -82,9 +108,16 @@ def _async_create_update_entity(
) -> FirmwareUpdateEntity:
"""Create an update entity that handles firmware type changes."""
firmware_type = config_entry.data[FIRMWARE]
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[
ApplicationType(firmware_type) if firmware_type is not None else None
]
try:
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[
ApplicationType(firmware_type)
]
except (KeyError, ValueError):
_LOGGER.debug(
"Unknown firmware type %r, using default entity description", firmware_type
)
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[None]
entity = FirmwareUpdateEntity(
device=RADIO_DEVICE,
@@ -135,6 +168,7 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
"""Yellow firmware update entity."""
bootloader_reset_type = "yellow" # Triggers a GPIO reset
_attr_has_entity_name = True
def __init__(
self,
@@ -145,8 +179,13 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
) -> None:
"""Initialize the Yellow firmware update entity."""
super().__init__(device, config_entry, update_coordinator, entity_description)
self._attr_unique_id = self.entity_description.key
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, "yellow")},
name=MODEL,
model=MODEL,
manufacturer=MANUFACTURER,
)
# Use the cached firmware info if it exists
if self._config_entry.data[FIRMWARE] is not None:
@@ -158,6 +197,17 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
source="homeassistant_yellow",
)
def _update_attributes(self) -> None:
"""Recompute the attributes of the entity."""
super()._update_attributes()
assert self.device_entry is not None
device_registry = dr.async_get(self.hass)
device_registry.async_update_device(
device_id=self.device_entry.id,
sw_version=f"{self.entity_description.firmware_name} {self._attr_installed_version}",
)
@callback
def _firmware_info_callback(self, firmware_info: FirmwareInfo) -> None:
"""Handle updated firmware info being pushed by an integration."""

View File

@@ -19,6 +19,7 @@ PLATFORMS = [
Platform.BUTTON,
Platform.COVER,
Platform.LIGHT,
Platform.LOCK,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,

View File

@@ -0,0 +1,73 @@
"""The Homee lock platform."""
from typing import Any
from pyHomee.const import AttributeChangedBy, AttributeType
from homeassistant.components.lock import LockEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeeConfigEntry
from .entity import HomeeEntity
from .helpers import get_name_for_enum
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_devices: AddConfigEntryEntitiesCallback,
) -> None:
"""Add the Homee platform for the lock component."""
async_add_devices(
HomeeLock(attribute, config_entry)
for node in config_entry.runtime_data.nodes
for attribute in node.attributes
if (attribute.type == AttributeType.LOCK_STATE and attribute.editable)
)
class HomeeLock(HomeeEntity, LockEntity):
"""Representation of a Homee lock."""
_attr_name = None
@property
def is_locked(self) -> bool:
"""Return if lock is locked."""
return self._attribute.current_value == 1.0
@property
def is_locking(self) -> bool:
"""Return if lock is locking."""
return self._attribute.target_value > self._attribute.current_value
@property
def is_unlocking(self) -> bool:
"""Return if lock is unlocking."""
return self._attribute.target_value < self._attribute.current_value
@property
def changed_by(self) -> str:
"""Return by whom or what the lock was last changed."""
changed_id = str(self._attribute.changed_by_id)
changed_by_name = get_name_for_enum(
AttributeChangedBy, self._attribute.changed_by
)
if self._attribute.changed_by == AttributeChangedBy.USER:
changed_id = self._entry.runtime_data.get_user_by_id(
self._attribute.changed_by_id
).username
return f"{changed_by_name}-{changed_id}"
async def async_lock(self, **kwargs: Any) -> None:
"""Lock specified lock. A code to lock the lock with may be specified."""
await self.async_set_homee_value(1)
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock specified lock. A code to unlock the lock with may be specified."""
await self.async_set_homee_value(0)

View File

@@ -178,6 +178,7 @@ SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = {
key="WIND_DIRECTION",
native_unit_of_measurement=DEGREE,
device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
),
"WIND_DIRECTION_RANGE": SensorEntityDescription(
key="WIND_DIRECTION_RANGE",

View File

@@ -1,4 +1,28 @@
{
"entity": {
"light": {
"hue_light": {
"state_attributes": {
"effect": {
"state": {
"candle": "mdi:candle",
"sparkle": "mdi:shimmer",
"glisten": "mdi:creation",
"sunrise": "mdi:weather-sunset-up",
"sunset": "mdi:weather-sunset",
"fire": "mdi:fire",
"prism": "mdi:triangle-outline",
"opal": "mdi:diamond-stone",
"underwater": "mdi:waves",
"cosmos": "mdi:star-shooting",
"sunbeam": "mdi:spotlight-beam",
"enchant": "mdi:magic-staff"
}
}
}
}
}
},
"services": {
"hue_activate_scene": {
"service": "mdi:palette"

View File

@@ -11,7 +11,7 @@
}
},
"manual": {
"title": "Manual configure a Hue bridge",
"title": "Manually configure a Hue bridge",
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
@@ -46,8 +46,8 @@
"button_2": "Second button",
"button_3": "Third button",
"button_4": "Fourth button",
"double_buttons_1_3": "First and Third buttons",
"double_buttons_2_4": "Second and Fourth buttons",
"double_buttons_1_3": "First and third button",
"double_buttons_2_4": "Second and fourth button",
"dim_down": "Dim down",
"dim_up": "Dim up",
"turn_off": "[%key:common::action::turn_off%]",

View File

@@ -227,12 +227,16 @@ def _get_work_area_names(data: MowerAttributes) -> list[str]:
@callback
def _get_current_work_area_name(data: MowerAttributes) -> str:
"""Return the name of the current work area."""
if data.mower.work_area_id is None:
return STATE_NO_WORK_AREA_ACTIVE
if TYPE_CHECKING:
# Sensor does not get created if values are None
assert data.work_areas is not None
return data.work_areas[data.mower.work_area_id].name
if (
data.mower.work_area_id is not None
and data.mower.work_area_id in data.work_areas
):
return data.work_areas[data.mower.work_area_id].name
return STATE_NO_WORK_AREA_ACTIVE
@callback

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling",
"requirements": ["imgw_pib==1.0.9"]
"requirements": ["imgw_pib==1.0.10"]
}

View File

@@ -8,6 +8,7 @@ import datetime
from enum import StrEnum
import logging
from homeassistant.components.recorder.models import StatisticMeanType
from homeassistant.components.recorder.models.statistics import (
StatisticData,
StatisticMetaData,
@@ -270,7 +271,7 @@ class IstaSensor(CoordinatorEntity[IstaCoordinator], SensorEntity):
]
metadata: StatisticMetaData = {
"has_mean": False,
"mean_type": StatisticMeanType.NONE,
"has_sum": True,
"name": f"{self.device_entry.name} {self.name}",
"source": DOMAIN,

View File

@@ -16,7 +16,8 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_CANDLE_LIGHT_MINUTES,
@@ -26,11 +27,21 @@ from .const import (
DEFAULT_DIASPORA,
DEFAULT_HAVDALAH_OFFSET_MINUTES,
DEFAULT_LANGUAGE,
DOMAIN,
)
from .entity import JewishCalendarConfigEntry, JewishCalendarData
from .service import async_setup_services
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Jewish Calendar service."""
async_setup_services(hass)
return True
async def async_setup_entry(

View File

@@ -2,6 +2,9 @@
DOMAIN = "jewish_calendar"
ATTR_DATE = "date"
ATTR_NUSACH = "nusach"
CONF_DIASPORA = "diaspora"
CONF_CANDLE_LIGHT_MINUTES = "candle_lighting_minutes_before_sunset"
CONF_HAVDALAH_OFFSET_MINUTES = "havdalah_minutes_after_sunset"
@@ -11,3 +14,5 @@ DEFAULT_CANDLE_LIGHT = 18
DEFAULT_DIASPORA = False
DEFAULT_HAVDALAH_OFFSET_MINUTES = 0
DEFAULT_LANGUAGE = "english"
SERVICE_COUNT_OMER = "count_omer"

View File

@@ -0,0 +1,7 @@
{
"services": {
"count_omer": {
"service": "mdi:counter"
}
}
}

View File

@@ -0,0 +1,63 @@
"""Services for Jewish Calendar."""
import datetime
from typing import cast
from hdate import HebrewDate
from hdate.omer import Nusach, Omer
from hdate.translator import Language
import voluptuous as vol
from homeassistant.const import CONF_LANGUAGE
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.selector import LanguageSelector, LanguageSelectorConfig
from .const import ATTR_DATE, ATTR_NUSACH, DOMAIN, SERVICE_COUNT_OMER
SUPPORTED_LANGUAGES = {"en": "english", "fr": "french", "he": "hebrew"}
OMER_SCHEMA = vol.Schema(
{
vol.Required(ATTR_DATE, default=datetime.date.today): cv.date,
vol.Required(ATTR_NUSACH, default="sfarad"): vol.In(
[nusach.name.lower() for nusach in Nusach]
),
vol.Required(CONF_LANGUAGE, default="he"): LanguageSelector(
LanguageSelectorConfig(languages=list(SUPPORTED_LANGUAGES.keys()))
),
}
)
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the Jewish Calendar services."""
async def get_omer_count(call: ServiceCall) -> ServiceResponse:
"""Return the Omer blessing for a given date."""
hebrew_date = HebrewDate.from_gdate(call.data["date"])
nusach = Nusach[call.data["nusach"].upper()]
# Currently Omer only supports Hebrew, English, and French and requires
# the full language name
language = cast(Language, SUPPORTED_LANGUAGES[call.data[CONF_LANGUAGE]])
omer = Omer(date=hebrew_date, nusach=nusach, language=language)
return {
"message": str(omer.count_str()),
"weeks": omer.week,
"days": omer.day,
"total_days": omer.total_days,
}
hass.services.async_register(
DOMAIN,
SERVICE_COUNT_OMER,
get_omer_count,
schema=OMER_SCHEMA,
supports_response=SupportsResponse.ONLY,
)

View File

@@ -0,0 +1,29 @@
count_omer:
fields:
date:
required: true
example: "2025-04-14"
selector:
date:
nusach:
required: true
example: "sfarad"
default: "sfarad"
selector:
select:
translation_key: "nusach"
options:
- "sfarad"
- "ashkenaz"
- "adot_mizrah"
- "italian"
language:
required: true
default: "he"
example: "he"
selector:
language:
languages:
- "en"
- "he"
- "fr"

View File

@@ -45,5 +45,35 @@
}
}
}
},
"selector": {
"nusach": {
"options": {
"sfarad": "Sfarad",
"ashkenaz": "Ashkenaz",
"adot_mizrah": "Adot Mizrah",
"italian": "Italian"
}
}
},
"services": {
"count_omer": {
"name": "Count the Omer",
"description": "Returns the phrase for counting the Omer on a given date.",
"fields": {
"date": {
"name": "Date",
"description": "Date to count the Omer for."
},
"nusach": {
"name": "Nusach",
"description": "Nusach to count the Omer in."
},
"language": {
"name": "Language",
"description": "Language to count the Omer in."
}
}
}
}
}

View File

@@ -12,14 +12,24 @@ from random import random
import voluptuous as vol
from homeassistant.components.recorder import DOMAIN as RECORDER_DOMAIN, get_instance
from homeassistant.components.recorder.models import StatisticData, StatisticMetaData
from homeassistant.components.recorder.models import (
StatisticData,
StatisticMeanType,
StatisticMetaData,
)
from homeassistant.components.recorder.statistics import (
async_add_external_statistics,
async_import_statistics,
get_last_statistics,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import Platform, UnitOfEnergy, UnitOfTemperature, UnitOfVolume
from homeassistant.const import (
DEGREE,
Platform,
UnitOfEnergy,
UnitOfTemperature,
UnitOfVolume,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
@@ -72,6 +82,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set the config entry up."""
if "recorder" in hass.config.components:
# Insert stats for mean_type_changed issue
await _insert_wrong_wind_direction_statistics(hass)
# Set up demo platforms with config entry
await hass.config_entries.async_forward_entry_setups(
entry, COMPONENTS_WITH_DEMO_PLATFORM
@@ -233,7 +247,7 @@ async def _insert_statistics(hass: HomeAssistant) -> None:
"name": "Outdoor temperature",
"statistic_id": f"{DOMAIN}:temperature_outdoor",
"unit_of_measurement": UnitOfTemperature.CELSIUS,
"has_mean": True,
"mean_type": StatisticMeanType.ARITHMETIC,
"has_sum": False,
}
statistics = _generate_mean_statistics(yesterday_midnight, today_midnight, 15, 1)
@@ -246,7 +260,7 @@ async def _insert_statistics(hass: HomeAssistant) -> None:
"name": "Energy consumption 1",
"statistic_id": f"{DOMAIN}:energy_consumption_kwh",
"unit_of_measurement": UnitOfEnergy.KILO_WATT_HOUR,
"has_mean": False,
"mean_type": StatisticMeanType.NONE,
"has_sum": True,
}
await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 1)
@@ -258,7 +272,7 @@ async def _insert_statistics(hass: HomeAssistant) -> None:
"name": "Energy consumption 2",
"statistic_id": f"{DOMAIN}:energy_consumption_mwh",
"unit_of_measurement": UnitOfEnergy.MEGA_WATT_HOUR,
"has_mean": False,
"mean_type": StatisticMeanType.NONE,
"has_sum": True,
}
await _insert_sum_statistics(
@@ -272,7 +286,7 @@ async def _insert_statistics(hass: HomeAssistant) -> None:
"name": "Gas consumption 1",
"statistic_id": f"{DOMAIN}:gas_consumption_m3",
"unit_of_measurement": UnitOfVolume.CUBIC_METERS,
"has_mean": False,
"mean_type": StatisticMeanType.NONE,
"has_sum": True,
}
await _insert_sum_statistics(
@@ -286,7 +300,7 @@ async def _insert_statistics(hass: HomeAssistant) -> None:
"name": "Gas consumption 2",
"statistic_id": f"{DOMAIN}:gas_consumption_ft3",
"unit_of_measurement": UnitOfVolume.CUBIC_FEET,
"has_mean": False,
"mean_type": StatisticMeanType.NONE,
"has_sum": True,
}
await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 15)
@@ -298,7 +312,7 @@ async def _insert_statistics(hass: HomeAssistant) -> None:
"name": None,
"statistic_id": "sensor.statistics_issues_issue_1",
"unit_of_measurement": UnitOfVolume.CUBIC_METERS,
"has_mean": True,
"mean_type": StatisticMeanType.ARITHMETIC,
"has_sum": False,
}
statistics = _generate_mean_statistics(yesterday_midnight, today_midnight, 15, 1)
@@ -310,7 +324,7 @@ async def _insert_statistics(hass: HomeAssistant) -> None:
"name": None,
"statistic_id": "sensor.statistics_issues_issue_2",
"unit_of_measurement": "cats",
"has_mean": True,
"mean_type": StatisticMeanType.ARITHMETIC,
"has_sum": False,
}
statistics = _generate_mean_statistics(yesterday_midnight, today_midnight, 15, 1)
@@ -322,7 +336,7 @@ async def _insert_statistics(hass: HomeAssistant) -> None:
"name": None,
"statistic_id": "sensor.statistics_issues_issue_3",
"unit_of_measurement": UnitOfVolume.CUBIC_METERS,
"has_mean": True,
"mean_type": StatisticMeanType.ARITHMETIC,
"has_sum": False,
}
statistics = _generate_mean_statistics(yesterday_midnight, today_midnight, 15, 1)
@@ -334,8 +348,28 @@ async def _insert_statistics(hass: HomeAssistant) -> None:
"name": None,
"statistic_id": "sensor.statistics_issues_issue_4",
"unit_of_measurement": UnitOfVolume.CUBIC_METERS,
"has_mean": True,
"mean_type": StatisticMeanType.ARITHMETIC,
"has_sum": False,
}
statistics = _generate_mean_statistics(yesterday_midnight, today_midnight, 15, 1)
async_import_statistics(hass, metadata, statistics)
async def _insert_wrong_wind_direction_statistics(hass: HomeAssistant) -> None:
"""Insert some fake wind direction statistics."""
now = dt_util.now()
yesterday = now - datetime.timedelta(days=1)
yesterday_midnight = yesterday.replace(hour=0, minute=0, second=0, microsecond=0)
today_midnight = yesterday_midnight + datetime.timedelta(days=1)
# Add some statistics required to raise the mean_type_changed issue later
metadata: StatisticMetaData = {
"source": RECORDER_DOMAIN,
"name": None,
"statistic_id": "sensor.statistics_issues_issue_5",
"unit_of_measurement": DEGREE,
"mean_type": StatisticMeanType.ARITHMETIC,
"has_sum": False,
}
statistics = _generate_mean_statistics(yesterday_midnight, today_midnight, 0, 360)
async_import_statistics(hass, metadata, statistics)

View File

@@ -8,7 +8,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfPower
from homeassistant.const import DEGREE, UnitOfPower
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -87,6 +87,16 @@ async def async_setup_entry(
state_class=None,
unit_of_measurement=UnitOfPower.WATT,
),
DemoSensor(
device_unique_id="statistics_issues",
unique_id="statistics_issue_5",
device_name="Statistics issues",
entity_name="Issue 5",
state=100,
device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
unit_of_measurement=DEGREE,
),
]
)

View File

@@ -2,19 +2,19 @@
"config": {
"step": {
"import_confirm": {
"title": "Import Konnected Device",
"description": "A Konnected Alarm Panel with ID {id} has been discovered in configuration.yaml. This flow will allow you to import it into a config entry."
"title": "Import Konnected device",
"description": "A Konnected alarm panel with ID {id} has been discovered in configuration.yaml. This flow will allow you to import it into a config entry."
},
"user": {
"description": "Please enter the host information for your Konnected Panel.",
"description": "Please enter the host information for your Konnected panel.",
"data": {
"host": "[%key:common::config_flow::data::ip%]",
"port": "[%key:common::config_flow::data::port%]"
}
},
"confirm": {
"title": "Konnected Device Ready",
"description": "Model: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nYou can configure the IO and panel behavior in the Konnected Alarm Panel settings."
"title": "Konnected device ready",
"description": "Model: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nYou can configure the IO and panel behavior in the Konnected alarm panel settings."
}
},
"error": {
@@ -45,8 +45,8 @@
}
},
"options_io_ext": {
"title": "Configure Extended I/O",
"description": "Select the configuration of the remaining I/O below. You'll be able to configure detailed options in the next steps.",
"title": "Configure extended I/O",
"description": "Select the configuration of the remaining I/O below. You'll be able to configure detailed options in the next steps.",
"data": {
"8": "Zone 8",
"9": "Zone 9",
@@ -59,25 +59,25 @@
}
},
"options_binary": {
"title": "Configure Binary Sensor",
"title": "Configure binary sensor",
"description": "{zone} options",
"data": {
"type": "Binary Sensor Type",
"type": "Binary sensor type",
"name": "[%key:common::config_flow::data::name%]",
"inverse": "Invert the open/close state"
}
},
"options_digital": {
"title": "Configure Digital Sensor",
"title": "Configure digital sensor",
"description": "[%key:component::konnected::options::step::options_binary::description%]",
"data": {
"type": "Sensor Type",
"type": "Sensor type",
"name": "[%key:common::config_flow::data::name%]",
"poll_interval": "Poll Interval (minutes)"
"poll_interval": "Poll interval (minutes)"
}
},
"options_switch": {
"title": "Configure Switchable Output",
"title": "Configure switchable output",
"description": "{zone} options: state {state}",
"data": {
"name": "[%key:common::config_flow::data::name%]",
@@ -89,18 +89,18 @@
}
},
"options_misc": {
"title": "Configure Misc",
"title": "Configure misc",
"description": "Please select the desired behavior for your panel",
"data": {
"discovery": "Respond to discovery requests on your network",
"blink": "Blink panel LED on when sending state change",
"override_api_host": "Override default Home Assistant API host panel URL",
"api_host": "Override API host URL"
"override_api_host": "Override default Home Assistant API host URL",
"api_host": "Custom API host URL"
}
}
},
"error": {
"bad_host": "Invalid Override API host URL"
"bad_host": "Invalid custom API host URL"
},
"abort": {
"not_konn_panel": "[%key:component::konnected::config::abort::not_konn_panel%]"

View File

@@ -106,6 +106,7 @@ SENSOR_DESCRIPTIONS = {
native_unit_of_measurement=DEGREE,
suggested_display_precision=2,
device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
),
"WetDry": LaCrosseSensorEntityDescription(
key="WetDry",

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from typing import Any
from typing import Any, cast
from led_ble import LEDBLE
@@ -83,7 +83,7 @@ class LEDBLEEntity(CoordinatorEntity[DataUpdateCoordinator[None]], LightEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Instruct the light to turn on."""
brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness)
brightness = cast(int, kwargs.get(ATTR_BRIGHTNESS, self.brightness))
if effect := kwargs.get(ATTR_EFFECT):
await self._async_set_effect(effect, brightness)
return

View File

@@ -465,7 +465,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
):
params.pop(_DEPRECATED_ATTR_COLOR_TEMP.value)
color_temp = params.pop(ATTR_COLOR_TEMP_KELVIN)
brightness = params.get(ATTR_BRIGHTNESS, light.brightness)
brightness = cast(int, params.get(ATTR_BRIGHTNESS, light.brightness))
params[ATTR_RGBWW_COLOR] = color_util.color_temperature_to_rgbww(
color_temp,
brightness,

View File

@@ -1,7 +1,15 @@
{
"entity_component": {
"_": {
"default": "mdi:lightbulb"
"default": "mdi:lightbulb",
"state_attributes": {
"effect": {
"default": "mdi:circle-medium",
"state": {
"off": "mdi:star-off"
}
}
}
}
},
"services": {

View File

@@ -93,7 +93,10 @@
"name": "Color temperature (Kelvin)"
},
"effect": {
"name": "Effect"
"name": "Effect",
"state": {
"off": "[%key:common::state::off%]"
}
},
"effect_list": {
"name": "Available effects"

View File

@@ -7,6 +7,6 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["linkplay"],
"requirements": ["python-linkplay==0.2.1"],
"requirements": ["python-linkplay==0.2.2"],
"zeroconf": ["_linkplay._tcp.local."]
}

View File

@@ -69,7 +69,7 @@ class MatterEventEntity(MatterEntity, EventEntity):
max_presses_supported = self.get_matter_attribute_value(
clusters.Switch.Attributes.MultiPressMax
)
max_presses_supported = min(max_presses_supported or 1, 8)
max_presses_supported = min(max_presses_supported or 2, 8)
for i in range(max_presses_supported):
event_types.append(f"multi_press_{i + 1}") # noqa: PERF401
elif feature_map & SwitchFeature.kMomentarySwitch:

View File

@@ -8,6 +8,6 @@
"iot_class": "calculated",
"loggers": ["yt_dlp"],
"quality_scale": "internal",
"requirements": ["yt-dlp[default]==2025.02.19"],
"requirements": ["yt-dlp[default]==2025.03.26"],
"single_config_entry": true
}

View File

@@ -23,7 +23,11 @@ from homeassistant.helpers.network import (
from .const import CONTENT_AUTH_EXPIRY_TIME, MediaClass, MediaType
# Paths that we don't need to sign
PATHS_WITHOUT_AUTH = ("/api/tts_proxy/", "/api/esphome/ffmpeg_proxy/")
PATHS_WITHOUT_AUTH = (
"/api/tts_proxy/",
"/api/esphome/ffmpeg_proxy/",
"/api/assist_satellite/static/",
)
@callback

View File

@@ -3,7 +3,7 @@
from __future__ import annotations
from datetime import timedelta
from typing import Any
from typing import Any, cast
from pymelcloud import DEVICE_TYPE_ATA, DEVICE_TYPE_ATW, AtaDevice, AtwDevice
import pymelcloud.ata_device as ata
@@ -236,7 +236,7 @@ class AtaDeviceClimate(MelCloudClimate):
set_dict: dict[str, Any] = {}
if ATTR_HVAC_MODE in kwargs:
self._apply_set_hvac_mode(
kwargs.get(ATTR_HVAC_MODE, self.hvac_mode), set_dict
cast(HVACMode, kwargs.get(ATTR_HVAC_MODE, self.hvac_mode)), set_dict
)
if ATTR_TEMPERATURE in kwargs:

View File

@@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Fetch data from API endpoint."""
assert isinstance(department, str)
return await hass.async_add_executor_job(
client.get_warning_current_phenomenoms, department, 0, True
client.get_warning_current_phenomenons, department, 0, True
)
coordinator_forecast = DataUpdateCoordinator(

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/meteo_france",
"iot_class": "cloud_polling",
"loggers": ["meteofrance_api"],
"requirements": ["meteofrance-api==1.3.0"]
"requirements": ["meteofrance-api==1.4.0"]
}

View File

@@ -7,7 +7,7 @@ from typing import Any
from meteofrance_api.helpers import (
get_warning_text_status_from_indice_color,
readeable_phenomenoms_dict,
readable_phenomenons_dict,
)
from meteofrance_api.model.forecast import Forecast
from meteofrance_api.model.rain import Rain
@@ -336,7 +336,7 @@ class MeteoFranceAlertSensor(MeteoFranceSensor[CurrentPhenomenons]):
def extra_state_attributes(self):
"""Return the state attributes."""
return {
**readeable_phenomenoms_dict(self.coordinator.data.phenomenons_max_colors),
**readable_phenomenons_dict(self.coordinator.data.phenomenons_max_colors),
}

View File

@@ -102,6 +102,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
native_unit_of_measurement=DEGREE,
icon="mdi:weather-windy",
device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
),
SensorEntityDescription(
key="rain",

View File

@@ -3,20 +3,20 @@
"flow_title": "{short_mac} ({ip_address})",
"step": {
"user": {
"description": "Connect to your Motion Gateway, if the IP address is not set, auto-discovery is used",
"description": "Connect to your Motionblinds gateway. If the IP address is not set, auto-discovery is used",
"data": {
"host": "[%key:common::config_flow::data::ip%]"
}
},
"connect": {
"description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions",
"description": "You will need the 16 character API key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-api-key for instructions",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
}
},
"select": {
"title": "Select the Motion Gateway that you wish to connect",
"description": "Run the setup again if you want to connect additional Motion Gateways",
"title": "Select the Motionblinds gateway that you wish to connect",
"description": "Run the setup again if you want to connect additional Motionblinds gateways",
"data": {
"select_ip": "[%key:common::config_flow::data::ip%]"
}
@@ -29,7 +29,7 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"connection_error": "[%key:common::config_flow::error::cannot_connect%]",
"not_motionblinds": "Discovered device is not a Motion gateway"
"not_motionblinds": "Discovered device is not a Motionblinds gateway"
}
},
"options": {

View File

@@ -27,6 +27,13 @@ import voluptuous as vol
from homeassistant.components.file_upload import process_uploaded_file
from homeassistant.components.hassio import AddonError, AddonManager, AddonState
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DEVICE_CLASS_UNITS,
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.components.switch import SwitchDeviceClass
from homeassistant.config_entries import (
SOURCE_RECONFIGURE,
ConfigEntry,
@@ -45,18 +52,22 @@ from homeassistant.const import (
ATTR_SW_VERSION,
CONF_CLIENT_ID,
CONF_DEVICE,
CONF_DEVICE_CLASS,
CONF_DISCOVERY,
CONF_HOST,
CONF_NAME,
CONF_OPTIMISTIC,
CONF_PASSWORD,
CONF_PAYLOAD,
CONF_PLATFORM,
CONF_PORT,
CONF_PROTOCOL,
CONF_UNIT_OF_MEASUREMENT,
CONF_USERNAME,
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.data_entry_flow import AbortFlow, SectionConfig, section
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.json import json_dumps
@@ -99,11 +110,16 @@ from .const import (
CONF_COMMAND_TOPIC,
CONF_DISCOVERY_PREFIX,
CONF_ENTITY_PICTURE,
CONF_EXPIRE_AFTER,
CONF_KEEPALIVE,
CONF_LAST_RESET_VALUE_TEMPLATE,
CONF_OPTIONS,
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE,
CONF_QOS,
CONF_RETAIN,
CONF_STATE_TOPIC,
CONF_SUGGESTED_DISPLAY_PRECISION,
CONF_TLS_INSECURE,
CONF_TRANSPORT,
CONF_WILL_MESSAGE,
@@ -120,6 +136,7 @@ from .const import (
DEFAULT_PORT,
DEFAULT_PREFIX,
DEFAULT_PROTOCOL,
DEFAULT_QOS,
DEFAULT_TRANSPORT,
DEFAULT_WILL,
DEFAULT_WS_PATH,
@@ -133,9 +150,9 @@ from .models import MqttAvailabilityData, MqttDeviceData, MqttSubentryData
from .util import (
async_create_certificate_temp_files,
get_file_path,
learn_more_url,
valid_birth_will,
valid_publish_topic,
valid_qos_schema,
valid_subscribe_topic,
valid_subscribe_topic_template,
)
@@ -164,7 +181,6 @@ PASSWORD_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.PASSWO
QOS_SELECTOR = NumberSelector(
NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0, max=2)
)
QOS_DATA_SCHEMA = vol.All(QOS_SELECTOR, valid_qos_schema)
KEEPALIVE_SELECTOR = vol.All(
NumberSelector(
NumberSelectorConfig(
@@ -217,7 +233,7 @@ KEY_UPLOAD_SELECTOR = FileSelector(
)
# Subentry selectors
SUBENTRY_PLATFORMS = [Platform.NOTIFY]
SUBENTRY_PLATFORMS = [Platform.NOTIFY, Platform.SENSOR, Platform.SWITCH]
SUBENTRY_PLATFORM_SELECTOR = SelectSelector(
SelectSelectorConfig(
options=[platform.value for platform in SUBENTRY_PLATFORMS],
@@ -225,7 +241,6 @@ SUBENTRY_PLATFORM_SELECTOR = SelectSelector(
translation_key=CONF_PLATFORM,
)
)
TEMPLATE_SELECTOR = TemplateSelector(TemplateSelectorConfig())
SUBENTRY_AVAILABILITY_SCHEMA = vol.Schema(
@@ -241,17 +256,118 @@ SUBENTRY_AVAILABILITY_SCHEMA = vol.Schema(
}
)
# Sensor specific selectors
SENSOR_DEVICE_CLASS_SELECTOR = SelectSelector(
SelectSelectorConfig(
options=[device_class.value for device_class in SensorDeviceClass],
mode=SelectSelectorMode.DROPDOWN,
translation_key="device_class_sensor",
sort=True,
)
)
SENSOR_STATE_CLASS_SELECTOR = SelectSelector(
SelectSelectorConfig(
options=[device_class.value for device_class in SensorStateClass],
mode=SelectSelectorMode.DROPDOWN,
translation_key=CONF_STATE_CLASS,
)
)
OPTIONS_SELECTOR = SelectSelector(
SelectSelectorConfig(
options=[],
custom_value=True,
multiple=True,
)
)
SUGGESTED_DISPLAY_PRECISION_SELECTOR = NumberSelector(
NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0, max=9)
)
EXPIRE_AFTER_SELECTOR = NumberSelector(
NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0)
)
# Switch specific selectors
SWITCH_DEVICE_CLASS_SELECTOR = SelectSelector(
SelectSelectorConfig(
options=[device_class.value for device_class in SwitchDeviceClass],
mode=SelectSelectorMode.DROPDOWN,
translation_key="device_class_switch",
)
)
@callback
def validate_sensor_platform_config(
config: dict[str, Any],
) -> dict[str, str]:
"""Validate the sensor options, state and device class config."""
errors: dict[str, str] = {}
# Only allow `options` to be set for `enum` sensors
# to limit the possible sensor values
if config.get(CONF_OPTIONS) is not None:
if config.get(CONF_STATE_CLASS) or config.get(CONF_UNIT_OF_MEASUREMENT):
errors[CONF_OPTIONS] = "options_not_allowed_with_state_class_or_uom"
if (device_class := config.get(CONF_DEVICE_CLASS)) != SensorDeviceClass.ENUM:
errors[CONF_DEVICE_CLASS] = "options_device_class_enum"
if (
(device_class := config.get(CONF_DEVICE_CLASS)) == SensorDeviceClass.ENUM
and errors is not None
and CONF_OPTIONS not in config
):
errors[CONF_OPTIONS] = "options_with_enum_device_class"
if (
device_class in DEVICE_CLASS_UNITS
and (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is None
and errors is not None
):
# Do not allow an empty unit of measurement in a subentry data flow
errors[CONF_UNIT_OF_MEASUREMENT] = "uom_required_for_device_class"
return errors
if (
device_class is not None
and device_class in DEVICE_CLASS_UNITS
and unit_of_measurement not in DEVICE_CLASS_UNITS[device_class]
):
errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom"
return errors
@dataclass(frozen=True)
class PlatformField:
"""Stores a platform config field schema, required flag and validator."""
selector: Selector
selector: Selector[Any] | Callable[..., Selector[Any]]
required: bool
validator: Callable[..., Any]
error: str | None = None
default: str | int | vol.Undefined = vol.UNDEFINED
exclude_from_reconfig: bool = False
conditions: tuple[dict[str, Any], ...] | None = None
custom_filtering: bool = False
section: str | None = None
@callback
def unit_of_measurement_selector(user_data: dict[str, Any | None]) -> Selector:
"""Return a context based unit of measurement selector."""
if (
user_data is None
or (device_class := user_data.get(CONF_DEVICE_CLASS)) is None
or device_class not in DEVICE_CLASS_UNITS
):
return TEXT_SELECTOR
return SelectSelector(
SelectSelectorConfig(
options=[str(uom) for uom in DEVICE_CLASS_UNITS[device_class]],
sort=True,
custom_value=True,
)
)
COMMON_ENTITY_FIELDS = {
@@ -262,9 +378,30 @@ COMMON_ENTITY_FIELDS = {
CONF_ENTITY_PICTURE: PlatformField(TEXT_SELECTOR, False, cv.url, "invalid_url"),
}
COMMON_MQTT_FIELDS = {
CONF_QOS: PlatformField(QOS_SELECTOR, False, valid_qos_schema, default=0),
CONF_RETAIN: PlatformField(BOOLEAN_SELECTOR, False, bool),
PLATFORM_ENTITY_FIELDS = {
Platform.NOTIFY.value: {},
Platform.SENSOR.value: {
CONF_DEVICE_CLASS: PlatformField(SENSOR_DEVICE_CLASS_SELECTOR, False, str),
CONF_STATE_CLASS: PlatformField(SENSOR_STATE_CLASS_SELECTOR, False, str),
CONF_UNIT_OF_MEASUREMENT: PlatformField(
unit_of_measurement_selector, False, str, custom_filtering=True
),
CONF_SUGGESTED_DISPLAY_PRECISION: PlatformField(
SUGGESTED_DISPLAY_PRECISION_SELECTOR,
False,
cv.positive_int,
section="advanced_settings",
),
CONF_OPTIONS: PlatformField(
OPTIONS_SELECTOR,
False,
cv.ensure_list,
conditions=({"device_class": "enum"},),
),
},
Platform.SWITCH.value: {
CONF_DEVICE_CLASS: PlatformField(SWITCH_DEVICE_CLASS_SELECTOR, False, str),
},
}
PLATFORM_MQTT_FIELDS = {
Platform.NOTIFY.value: {
@@ -274,19 +411,63 @@ PLATFORM_MQTT_FIELDS = {
CONF_COMMAND_TEMPLATE: PlatformField(
TEMPLATE_SELECTOR, False, cv.template, "invalid_template"
),
CONF_RETAIN: PlatformField(BOOLEAN_SELECTOR, False, bool),
},
Platform.SENSOR.value: {
CONF_STATE_TOPIC: PlatformField(
TEXT_SELECTOR, True, valid_subscribe_topic, "invalid_subscribe_topic"
),
CONF_VALUE_TEMPLATE: PlatformField(
TEMPLATE_SELECTOR, False, cv.template, "invalid_template"
),
CONF_LAST_RESET_VALUE_TEMPLATE: PlatformField(
TEMPLATE_SELECTOR,
False,
cv.template,
"invalid_template",
conditions=({CONF_STATE_CLASS: "total"},),
),
CONF_EXPIRE_AFTER: PlatformField(
EXPIRE_AFTER_SELECTOR, False, cv.positive_int, section="advanced_settings"
),
},
Platform.SWITCH.value: {
CONF_COMMAND_TOPIC: PlatformField(
TEXT_SELECTOR, True, valid_publish_topic, "invalid_publish_topic"
),
CONF_COMMAND_TEMPLATE: PlatformField(
TEMPLATE_SELECTOR, False, cv.template, "invalid_template"
),
CONF_STATE_TOPIC: PlatformField(
TEXT_SELECTOR, False, valid_subscribe_topic, "invalid_subscribe_topic"
),
CONF_VALUE_TEMPLATE: PlatformField(
TEMPLATE_SELECTOR, False, cv.template, "invalid_template"
),
CONF_RETAIN: PlatformField(BOOLEAN_SELECTOR, False, bool),
CONF_OPTIMISTIC: PlatformField(BOOLEAN_SELECTOR, False, bool),
},
}
ENTITY_CONFIG_VALIDATOR: dict[
str,
Callable[[dict[str, Any]], dict[str, str]] | None,
] = {
Platform.NOTIFY.value: None,
Platform.SENSOR.value: validate_sensor_platform_config,
Platform.SWITCH.value: None,
}
MQTT_DEVICE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_NAME): TEXT_SELECTOR,
vol.Optional(ATTR_SW_VERSION): TEXT_SELECTOR,
vol.Optional(ATTR_HW_VERSION): TEXT_SELECTOR,
vol.Optional(ATTR_MODEL): TEXT_SELECTOR,
vol.Optional(ATTR_MODEL_ID): TEXT_SELECTOR,
vol.Optional(ATTR_CONFIGURATION_URL): TEXT_SELECTOR,
}
)
MQTT_DEVICE_PLATFORM_FIELDS = {
ATTR_NAME: PlatformField(TEXT_SELECTOR, False, str),
ATTR_SW_VERSION: PlatformField(TEXT_SELECTOR, False, str),
ATTR_HW_VERSION: PlatformField(TEXT_SELECTOR, False, str),
ATTR_MODEL: PlatformField(TEXT_SELECTOR, False, str),
ATTR_MODEL_ID: PlatformField(TEXT_SELECTOR, False, str),
ATTR_CONFIGURATION_URL: PlatformField(TEXT_SELECTOR, False, cv.url, "invalid_url"),
CONF_QOS: PlatformField(
QOS_SELECTOR, False, int, default=DEFAULT_QOS, section="mqtt_settings"
),
}
REAUTH_SCHEMA = vol.Schema(
{
@@ -337,38 +518,151 @@ def validate_field(
errors[field] = error
@callback
def _check_conditions(
platform_field: PlatformField, component_data: dict[str, Any] | None = None
) -> bool:
"""Only include field if one of conditions match, or no conditions are set."""
if platform_field.conditions is None or component_data is None:
return True
return any(
all(component_data.get(key) == value for key, value in condition.items())
for condition in platform_field.conditions
)
@callback
def calculate_merged_config(
merged_user_input: dict[str, Any],
data_schema_fields: dict[str, PlatformField],
component_data: dict[str, Any],
) -> dict[str, Any]:
"""Calculate merged config."""
base_schema_fields = {
key
for key, platform_field in data_schema_fields.items()
if _check_conditions(platform_field, component_data)
} - set(merged_user_input)
return {
key: value
for key, value in component_data.items()
if key not in base_schema_fields
} | merged_user_input
@callback
def validate_user_input(
user_input: dict[str, Any],
data_schema_fields: dict[str, PlatformField],
errors: dict[str, str],
) -> None:
*,
component_data: dict[str, Any] | None = None,
config_validator: Callable[[dict[str, Any]], dict[str, str]] | None = None,
) -> tuple[dict[str, Any], dict[str, str]]:
"""Validate user input."""
for field, value in user_input.items():
errors: dict[str, str] = {}
# Merge sections
merged_user_input: dict[str, Any] = {}
for key, value in user_input.items():
if isinstance(value, dict):
merged_user_input.update(value)
else:
merged_user_input[key] = value
for field, value in merged_user_input.items():
validator = data_schema_fields[field].validator
try:
validator(value)
except (ValueError, vol.Invalid):
errors[field] = data_schema_fields[field].error or "invalid_input"
if config_validator is not None:
if TYPE_CHECKING:
assert component_data is not None
errors |= config_validator(
calculate_merged_config(
merged_user_input, data_schema_fields, component_data
),
)
return merged_user_input, errors
@callback
def data_schema_from_fields(
data_schema_fields: dict[str, PlatformField],
reconfig: bool,
component_data: dict[str, Any] | None = None,
user_input: dict[str, Any] | None = None,
device_data: MqttDeviceData | None = None,
) -> vol.Schema:
"""Generate data schema from platform fields."""
return vol.Schema(
{
"""Generate custom data schema from platform fields or device data."""
if device_data is not None:
component_data_with_user_input: dict[str, Any] | None = dict(device_data)
if TYPE_CHECKING:
assert component_data_with_user_input is not None
component_data_with_user_input.update(
component_data_with_user_input.pop("mqtt_settings", {})
)
else:
component_data_with_user_input = deepcopy(component_data)
if component_data_with_user_input is not None and user_input is not None:
component_data_with_user_input |= user_input
sections: dict[str | None, None] = {
field_details.section: None for field_details in data_schema_fields.values()
}
data_schema: dict[Any, Any] = {}
all_data_element_options: set[Any] = set()
no_reconfig_options: set[Any] = set()
for schema_section in sections:
data_schema_element = {
vol.Required(field_name, default=field_details.default)
if field_details.required
else vol.Optional(
field_name, default=field_details.default
): field_details.selector
): field_details.selector(component_data_with_user_input) # type: ignore[operator]
if field_details.custom_filtering
else field_details.selector
for field_name, field_details in data_schema_fields.items()
if not field_details.exclude_from_reconfig or not reconfig
if field_details.section == schema_section
and (not field_details.exclude_from_reconfig or not reconfig)
and _check_conditions(field_details, component_data_with_user_input)
}
)
data_element_options = set(data_schema_element)
all_data_element_options |= data_element_options
no_reconfig_options |= {
field_name
for field_name, field_details in data_schema_fields.items()
if field_details.section == schema_section
and field_details.exclude_from_reconfig
}
if schema_section is None:
data_schema.update(data_schema_element)
continue
collapsed = (
not any(
(default := data_schema_fields[str(option)].default) is vol.UNDEFINED
or component_data_with_user_input[str(option)] != default
for option in data_element_options
if option in component_data_with_user_input
)
if component_data_with_user_input is not None
else True
)
data_schema[vol.Optional(schema_section)] = section(
vol.Schema(data_schema_element), SectionConfig({"collapsed": collapsed})
)
# Reset all fields from the component_data not in the schema
if component_data:
filtered_fields = (
set(data_schema_fields) - all_data_element_options - no_reconfig_options
)
for field in filtered_fields:
if field in component_data:
del component_data[field]
return vol.Schema(data_schema)
class FlowHandler(ConfigFlow, domain=DOMAIN):
@@ -849,7 +1143,7 @@ class MQTTOptionsFlowHandler(OptionsFlow):
"birth_payload", description={"suggested_value": birth[CONF_PAYLOAD]}
)
] = TEXT_SELECTOR
fields[vol.Optional("birth_qos", default=birth[ATTR_QOS])] = QOS_DATA_SCHEMA
fields[vol.Optional("birth_qos", default=birth[ATTR_QOS])] = QOS_SELECTOR
fields[vol.Optional("birth_retain", default=birth[ATTR_RETAIN])] = (
BOOLEAN_SELECTOR
)
@@ -872,7 +1166,7 @@ class MQTTOptionsFlowHandler(OptionsFlow):
"will_payload", description={"suggested_value": will[CONF_PAYLOAD]}
)
] = TEXT_SELECTOR
fields[vol.Optional("will_qos", default=will[ATTR_QOS])] = QOS_DATA_SCHEMA
fields[vol.Optional("will_qos", default=will[ATTR_QOS])] = QOS_SELECTOR
fields[vol.Optional("will_retain", default=will[ATTR_RETAIN])] = (
BOOLEAN_SELECTOR
)
@@ -893,20 +1187,56 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
@callback
def update_component_fields(
self, data_schema: vol.Schema, user_input: dict[str, Any]
self,
data_schema_fields: dict[str, PlatformField],
merged_user_input: dict[str, Any],
) -> None:
"""Update the componment fields."""
if TYPE_CHECKING:
assert self._component_id is not None
component_data = self._subentry_data["components"][self._component_id]
# Remove the fields from the component data if they are not in the user input
for field in [
form_field
for form_field in data_schema.schema
if form_field in component_data and form_field not in user_input
]:
# Remove the fields from the component data
# if they are not in the schema and not in the user input
config = calculate_merged_config(
merged_user_input, data_schema_fields, component_data
)
for field in (
field
for field, platform_field in data_schema_fields.items()
if field in (set(component_data) - set(config))
and not platform_field.exclude_from_reconfig
):
component_data.pop(field)
component_data.update(user_input)
component_data.update(merged_user_input)
@callback
def generate_names(self) -> tuple[str, str]:
"""Generate the device and full entity name."""
if TYPE_CHECKING:
assert self._component_id is not None
device_name = self._subentry_data[CONF_DEVICE][CONF_NAME]
if entity_name := self._subentry_data["components"][self._component_id].get(
CONF_NAME
):
full_entity_name: str = f"{device_name} {entity_name}"
else:
full_entity_name = device_name
return device_name, full_entity_name
@callback
def get_suggested_values_from_component(
self, data_schema: vol.Schema
) -> dict[str, Any]:
"""Get suggestions from component data based on the data schema."""
if TYPE_CHECKING:
assert self._component_id is not None
component_data = self._subentry_data["components"][self._component_id]
return {
field_key: self.get_suggested_values_from_component(value.schema)
if isinstance(value, section)
else component_data.get(field_key)
for field_key, value in data_schema.schema.items()
}
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -929,17 +1259,22 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""Add a new MQTT device."""
errors: dict[str, str] = {}
validate_field("configuration_url", cv.url, user_input, errors, "invalid_url")
if not errors and user_input is not None:
self._subentry_data[CONF_DEVICE] = cast(MqttDeviceData, user_input)
if self.source == SOURCE_RECONFIGURE:
return await self.async_step_summary_menu()
return await self.async_step_entity()
errors: dict[str, Any] = {}
device_data = self._subentry_data[CONF_DEVICE]
data_schema = data_schema_from_fields(
MQTT_DEVICE_PLATFORM_FIELDS,
device_data=device_data,
reconfig=True,
)
if user_input is not None:
_, errors = validate_user_input(user_input, MQTT_DEVICE_PLATFORM_FIELDS)
if not errors:
self._subentry_data[CONF_DEVICE] = cast(MqttDeviceData, user_input)
if self.source == SOURCE_RECONFIGURE:
return await self.async_step_summary_menu()
return await self.async_step_entity()
data_schema = self.add_suggested_values_to_schema(
MQTT_DEVICE_SCHEMA,
self._subentry_data[CONF_DEVICE] if user_input is None else user_input,
data_schema, device_data if user_input is None else user_input
)
return self.async_show_form(
step_id=CONF_DEVICE,
@@ -956,25 +1291,28 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
data_schema_fields = COMMON_ENTITY_FIELDS
entity_name_label: str = ""
platform_label: str = ""
component_data: dict[str, Any] | None = None
if reconfig := (self._component_id is not None):
name: str | None = self._subentry_data["components"][
self._component_id
].get(CONF_NAME)
component_data = self._subentry_data["components"][self._component_id]
name: str | None = component_data.get(CONF_NAME)
platform_label = f"{self._subentry_data['components'][self._component_id][CONF_PLATFORM]} "
entity_name_label = f" ({name})" if name is not None else ""
data_schema = data_schema_from_fields(data_schema_fields, reconfig=reconfig)
if user_input is not None:
validate_user_input(user_input, data_schema_fields, errors)
merged_user_input, errors = validate_user_input(
user_input, data_schema_fields, component_data=component_data
)
if not errors:
if self._component_id is None:
self._component_id = uuid4().hex
self._subentry_data["components"].setdefault(self._component_id, {})
self.update_component_fields(data_schema, user_input)
return await self.async_step_mqtt_platform_config()
self.update_component_fields(data_schema_fields, merged_user_input)
return await self.async_step_entity_platform_config()
data_schema = self.add_suggested_values_to_schema(data_schema, user_input)
elif self.source == SOURCE_RECONFIGURE and self._component_id is not None:
data_schema = self.add_suggested_values_to_schema(
data_schema, self._subentry_data["components"][self._component_id]
data_schema,
self.get_suggested_values_from_component(data_schema),
)
device_name = self._subentry_data[CONF_DEVICE][CONF_NAME]
return self.async_show_form(
@@ -994,9 +1332,11 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
device_name = self._subentry_data[CONF_DEVICE][CONF_NAME]
entities = [
SelectOptionDict(
value=key, label=f"{device_name} {component.get(CONF_NAME, '-')}"
value=key,
label=f"{device_name} {component_data.get(CONF_NAME, '-')}"
f" ({component_data[CONF_PLATFORM]})",
)
for key, component in self._subentry_data["components"].items()
for key, component_data in self._subentry_data["components"].items()
]
data_schema = vol.Schema(
{
@@ -1034,6 +1374,61 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
return await self.async_step_summary_menu()
return self._show_update_or_delete_form("delete_entity")
async def async_step_entity_platform_config(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""Configure platform entity details."""
if TYPE_CHECKING:
assert self._component_id is not None
component_data = self._subentry_data["components"][self._component_id]
platform = component_data[CONF_PLATFORM]
data_schema_fields = PLATFORM_ENTITY_FIELDS[platform]
errors: dict[str, str] = {}
data_schema = data_schema_from_fields(
data_schema_fields,
reconfig=bool(
{field for field in data_schema_fields if field in component_data}
),
component_data=component_data,
user_input=user_input,
)
if not data_schema.schema:
return await self.async_step_mqtt_platform_config()
if user_input is not None:
# Test entity fields against the validator
merged_user_input, errors = validate_user_input(
user_input,
data_schema_fields,
component_data=component_data,
config_validator=ENTITY_CONFIG_VALIDATOR[platform],
)
if not errors:
self.update_component_fields(data_schema_fields, merged_user_input)
return await self.async_step_mqtt_platform_config()
data_schema = self.add_suggested_values_to_schema(data_schema, user_input)
else:
data_schema = self.add_suggested_values_to_schema(
data_schema,
self.get_suggested_values_from_component(data_schema),
)
device_name, full_entity_name = self.generate_names()
return self.async_show_form(
step_id="entity_platform_config",
data_schema=data_schema,
description_placeholders={
"mqtt_device": device_name,
CONF_PLATFORM: platform,
"entity": full_entity_name,
"url": learn_more_url(platform),
}
| (user_input or {}),
errors=errors,
last_step=False,
)
async def async_step_mqtt_platform_config(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
@@ -1041,16 +1436,26 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
errors: dict[str, str] = {}
if TYPE_CHECKING:
assert self._component_id is not None
platform = self._subentry_data["components"][self._component_id][CONF_PLATFORM]
data_schema_fields = PLATFORM_MQTT_FIELDS[platform] | COMMON_MQTT_FIELDS
component_data = self._subentry_data["components"][self._component_id]
platform = component_data[CONF_PLATFORM]
data_schema_fields = PLATFORM_MQTT_FIELDS[platform]
data_schema = data_schema_from_fields(
data_schema_fields, reconfig=self._component_id is not None
data_schema_fields,
reconfig=bool(
{field for field in data_schema_fields if field in component_data}
),
component_data=component_data,
)
if user_input is not None:
# Test entity fields against the validator
validate_user_input(user_input, data_schema_fields, errors)
merged_user_input, errors = validate_user_input(
user_input,
data_schema_fields,
component_data=component_data,
config_validator=ENTITY_CONFIG_VALIDATOR[platform],
)
if not errors:
self.update_component_fields(data_schema, user_input)
self.update_component_fields(data_schema_fields, merged_user_input)
self._component_id = None
if self.source == SOURCE_RECONFIGURE:
return await self.async_step_summary_menu()
@@ -1059,16 +1464,10 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
data_schema = self.add_suggested_values_to_schema(data_schema, user_input)
else:
data_schema = self.add_suggested_values_to_schema(
data_schema, self._subentry_data["components"][self._component_id]
data_schema,
self.get_suggested_values_from_component(data_schema),
)
device_name = self._subentry_data[CONF_DEVICE][CONF_NAME]
entity_name: str | None
if entity_name := self._subentry_data["components"][self._component_id].get(
CONF_NAME
):
full_entity_name: str = f"{device_name} {entity_name}"
else:
full_entity_name = device_name
device_name, full_entity_name = self.generate_names()
return self.async_show_form(
step_id="mqtt_platform_config",
data_schema=data_schema,
@@ -1076,6 +1475,7 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
"mqtt_device": device_name,
CONF_PLATFORM: platform,
"entity": full_entity_name,
"url": learn_more_url(platform),
},
errors=errors,
last_step=False,
@@ -1087,12 +1487,12 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
) -> SubentryFlowResult:
"""Create a subentry for a new MQTT device."""
device_name = self._subentry_data[CONF_DEVICE][CONF_NAME]
component: dict[str, Any] = next(
component_data: dict[str, Any] = next(
iter(self._subentry_data["components"].values())
)
platform = component[CONF_PLATFORM]
platform = component_data[CONF_PLATFORM]
entity_name: str | None
if entity_name := component.get(CONF_NAME):
if entity_name := component_data.get(CONF_NAME):
full_entity_name: str = f"{device_name} {entity_name}"
else:
full_entity_name = device_name
@@ -1151,8 +1551,8 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
self._component_id = None
mqtt_device = self._subentry_data[CONF_DEVICE][CONF_NAME]
mqtt_items = ", ".join(
f"{mqtt_device} {component.get(CONF_NAME, '-')}"
for component in self._subentry_data["components"].values()
f"{mqtt_device} {component_data.get(CONF_NAME, '-')} ({component_data[CONF_PLATFORM]})"
for component_data in self._subentry_data["components"].values()
)
menu_options = [
"entity",

View File

@@ -86,6 +86,7 @@ CONF_EFFECT_STATE_TOPIC = "effect_state_topic"
CONF_EFFECT_TEMPLATE = "effect_template"
CONF_EFFECT_VALUE_TEMPLATE = "effect_value_template"
CONF_ENTITY_PICTURE = "entity_picture"
CONF_EXPIRE_AFTER = "expire_after"
CONF_FLASH_TIME_LONG = "flash_time_long"
CONF_FLASH_TIME_SHORT = "flash_time_short"
CONF_GREEN_TEMPLATE = "green_template"
@@ -93,6 +94,7 @@ CONF_HS_COMMAND_TEMPLATE = "hs_command_template"
CONF_HS_COMMAND_TOPIC = "hs_command_topic"
CONF_HS_STATE_TOPIC = "hs_state_topic"
CONF_HS_VALUE_TEMPLATE = "hs_value_template"
CONF_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template"
CONF_MAX_KELVIN = "max_kelvin"
CONF_MAX_MIREDS = "max_mireds"
CONF_MIN_KELVIN = "min_kelvin"
@@ -128,6 +130,7 @@ CONF_STATE_CLOSED = "state_closed"
CONF_STATE_CLOSING = "state_closing"
CONF_STATE_OPEN = "state_open"
CONF_STATE_OPENING = "state_opening"
CONF_SUGGESTED_DISPLAY_PRECISION = "suggested_display_precision"
CONF_SUPPORTED_COLOR_MODES = "supported_color_modes"
CONF_TEMP_COMMAND_TEMPLATE = "temperature_command_template"
CONF_TEMP_COMMAND_TOPIC = "temperature_command_topic"

View File

@@ -123,7 +123,7 @@ from .subscription import (
async_subscribe_topics_internal,
async_unsubscribe_topics,
)
from .util import mqtt_config_entry_enabled
from .util import learn_more_url, mqtt_config_entry_enabled
_LOGGER = logging.getLogger(__name__)
@@ -300,6 +300,7 @@ def async_setup_entity_entry_helper(
availability_config = subentry_data.get("availability", {})
subentry_entities: list[Entity] = []
device_config = subentry_data["device"].copy()
device_mqtt_options = device_config.pop("mqtt_settings", {})
device_config["identifiers"] = config_subentry_id
for component_id, component_data in subentry_data["components"].items():
if component_data["platform"] != domain:
@@ -311,6 +312,7 @@ def async_setup_entity_entry_helper(
component_config[CONF_DEVICE] = device_config
component_config.pop("platform")
component_config.update(availability_config)
component_config.update(device_mqtt_options)
try:
config = platform_schema_modern(component_config)
@@ -346,9 +348,6 @@ def async_setup_entity_entry_helper(
line = getattr(yaml_config, "__line__", "?")
issue_id = hex(hash(frozenset(yaml_config)))
yaml_config_str = yaml_dump(yaml_config)
learn_more_url = (
f"https://www.home-assistant.io/integrations/{domain}.mqtt/"
)
async_create_issue(
hass,
DOMAIN,
@@ -356,7 +355,7 @@ def async_setup_entity_entry_helper(
issue_domain=domain,
is_fixable=False,
severity=IssueSeverity.ERROR,
learn_more_url=learn_more_url,
learn_more_url=learn_more_url(domain),
translation_placeholders={
"domain": domain,
"config_file": config_file,

View File

@@ -420,6 +420,12 @@ class MqttComponentConfig:
discovery_payload: MQTTDiscoveryPayload
class DeviceMqttOptions(TypedDict, total=False):
"""Hold the shared MQTT specific options for an MQTT device."""
qos: int
class MqttDeviceData(TypedDict, total=False):
"""Hold the data for an MQTT device."""
@@ -430,6 +436,7 @@ class MqttDeviceData(TypedDict, total=False):
hw_version: str
model: str
model_id: str
mqtt_settings: DeviceMqttOptions
class MqttAvailabilityData(TypedDict, total=False):

View File

@@ -41,7 +41,15 @@ from homeassistant.util import dt as dt_util
from . import subscription
from .config import MQTT_RO_SCHEMA
from .const import CONF_OPTIONS, CONF_STATE_TOPIC, DOMAIN, PAYLOAD_NONE
from .const import (
CONF_EXPIRE_AFTER,
CONF_LAST_RESET_VALUE_TEMPLATE,
CONF_OPTIONS,
CONF_STATE_TOPIC,
CONF_SUGGESTED_DISPLAY_PRECISION,
DOMAIN,
PAYLOAD_NONE,
)
from .entity import MqttAvailabilityMixin, MqttEntity, async_setup_entity_entry_helper
from .models import MqttValueTemplate, PayloadSentinel, ReceiveMessage
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
@@ -51,10 +59,6 @@ _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
CONF_EXPIRE_AFTER = "expire_after"
CONF_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template"
CONF_SUGGESTED_DISPLAY_PRECISION = "suggested_display_precision"
MQTT_SENSOR_ATTRIBUTES_BLOCKED = frozenset(
{
sensor.ATTR_LAST_RESET,

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