Compare commits

...

250 Commits

Author SHA1 Message Date
Martin Hjelmare e49799f5ef Improve available commands fixture 2025-03-05 09:14:46 +01:00
dependabot[bot] 1c045ab222 Bump actions/download-artifact from 4.1.8 to 4.1.9 (#139814)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.1.8 to 4.1.9.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4.1.8...v4.1.9)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-05 08:16:42 +01:00
dependabot[bot] 0d329bd83d Bump actions/upload-artifact from 4.6.0 to 4.6.1 (#139813)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.0 to 4.6.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.6.0...v4.6.1)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-05 07:49:18 +01:00
J. Nick Koston d1995086cc Bump habluetooth to 3.25.0 (#139811)
changelog: https://github.com/Bluetooth-Devices/habluetooth/compare/v3.24.1...v3.25.0
2025-03-05 00:15:00 -05:00
J. Nick Koston f0ad0e6eae Bump cached-ipaddress to 0.10.0 (#139807) 2025-03-04 17:51:46 -10:00
J. Nick Koston 457a7216ff Bump dbus-fast to 2.35.1 (#139809) 2025-03-04 17:51:31 -10:00
tdfountain 782f504522 Add common PDU sensors to NUT (#139669)
* Add common PDU sensors and alphabetize sensors list

* Back out code quality improvements

* Change voltage and current status to diagnostic and disabled by default
2025-03-04 17:26:43 -10:00
J. Nick Koston e60a284354 Bump aioesphomeapi to 29.4.0 (#139806)
changelog: https://github.com/esphome/aioesphomeapi/compare/v29.3.2...v29.4.0
2025-03-04 17:25:43 -10:00
J. Nick Koston d5d9bc1df6 Bump ulid-transform to 1.3.0 (#139808)
changelog: https://github.com/Bluetooth-Devices/ulid-transform/compare/v1.2.1...v1.3.0
2025-03-04 17:25:11 -10:00
J. Nick Koston 27fd0a88f4 Bump bleak-esphome to 2.11.0 (#139803)
changelog: https://github.com/Bluetooth-Devices/bleak-esphome/compare/v2.10.2...v2.11.0
2025-03-04 17:12:45 -10:00
J. Nick Koston 24188ffb31 Bump zeroconf to 0.146.0 (#139804)
changelog: https://github.com/python-zeroconf/python-zeroconf/compare/0.145.1...0.146.0
2025-03-04 17:10:07 -10:00
Petro31 e51d9bd6f4 Remove redundant is not None checks in Template integration (#139790)
Remove redundant is not None checks
2025-03-04 21:58:41 -05:00
J. Nick Koston 3eb7302fde Bump fnv-hash-fast to 1.4.0 (#139801)
changelog: https://github.com/Bluetooth-Devices/fnv-hash-fast/compare/v1.2.6...v1.4.0
2025-03-04 21:57:43 -05:00
J. Nick Koston 49b2f8fd7f Bump bluetooth-data-tools to 1.25.0 (#139802)
changelog: https://github.com/Bluetooth-Devices/bluetooth-data-tools/compare/v1.23.4...v1.25.0
2025-03-04 21:57:27 -05:00
Shay Levy 0143a71e97 Bump aiowebostv to 0.7.3 (#139788) 2025-03-04 16:45:23 -10:00
J. Nick Koston 9bc806ab21 Bump nexia to 2.2.1 (#139786)
* Bump nexia to 2.2.0

changelog: https://github.com/bdraco/nexia/compare/2.1.1...2.2.0

* Apply suggestions from code review
2025-03-05 01:57:03 +01:00
Simone Chemelli 366c5c3f10 Improve unique_id tests for Shelly block devices (#139778)
* Improve unique_id tests for Shelly block devices

* type test

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2025-03-04 14:03:38 -10:00
peteS-UK 3ee5262a8d Clean up squeezebox build_item_response part 2 (#139595) 2025-03-04 17:48:13 -06:00
Martin Hjelmare c671862d3f Improve Home Connect appliances test fixture (#139787)
Improve Home Connect appliances fixture
2025-03-05 00:45:58 +01:00
Manu 50ba93042b Add create_habit action to Habitica integration (#139673) 2025-03-04 21:43:49 +00:00
Norbert Rittel 1456d9d800 Capitalize "Suez Water" and "ID" in user-facing strings (#139782) 2025-03-04 21:00:51 +01:00
Erik Montnemery 3b9bb96784 Align google_drive with changes in BackupAgent (#139767) 2025-03-04 11:45:10 -08:00
epenet 7359013db0 Move ForkedDaapdUpdater setup to __init__ module (#139733)
* Move ForkedDaapdUpdater setup to __init__ module

* Adjust tests

* One more
2025-03-04 20:24:36 +01:00
Erik Montnemery be3d678f23 Align hassio with changes in BackupAgent (#139780) 2025-03-04 20:20:49 +01:00
Martin Hjelmare e8099fd3b2 Fix home connect available (#139760)
* Fix home connect available

* Extend and clarify test

* Do not change connected state on stream interrupted
2025-03-04 19:26:20 +01:00
Erik Montnemery 344cfedd6f Align synology_dsm with changes in BackupAgent (#139770) 2025-03-04 19:22:18 +01:00
Robert Resch c0d882e305 Upload test result artifacts always (#139776)
Upload test results artificats always
2025-03-04 19:19:38 +01:00
Erik Montnemery e86fc88631 Minor improvement of hassio backup tests (#139775) 2025-03-04 18:20:55 +01:00
J. Nick Koston e1127fc78c Bump nexia to 2.1.1 (#139772)
changelog: https://github.com/bdraco/nexia/compare/2.0.9...2.1.1

fixes #133368
2025-03-04 18:01:40 +01:00
Erik Montnemery 95fbba1d74 Align cloud with changes in BackupAgent (#139766) 2025-03-04 17:46:13 +01:00
Erik Montnemery 46ac44c248 Align webdav with changes in BackupAgent (#139771) 2025-03-04 17:44:26 +01:00
Erik Montnemery 0ebdb1c2a8 Align kitchen_sink with changes in BackupAgent (#139768) 2025-03-04 16:38:03 +01:00
Erik Montnemery e3a90831bf Align onedrive with changes in BackupAgent (#139769) 2025-03-04 16:32:47 +01:00
Erik Montnemery ec100e5a6c Align azure_storage with changes in BackupAgent (#139765) 2025-03-04 16:10:33 +01:00
Joost Lekkerkerker 0eb087ba3f Bump pysmartthings to 2.5.0 (#139758)
* Bump pysmartthings to 2.5.0

* Bump pysmartthings to 2.5.0
2025-03-04 15:59:38 +01:00
Erik Montnemery e55757dc82 Simplify error handling in BackupAgent when a backup is not found (#139754)
Simplify error handling in BackupAgent when backup is not found
2025-03-04 15:56:12 +01:00
Paulus Schoutsen c51a2317e1 Add timer support to VoIP (#139763) 2025-03-04 15:48:10 +01:00
Anthony Hou 7fb949dff7 Fix incorrect weather state returned by HKO (#139757)
* Fix incorrect weather state

* Clean up unused import

---------

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-03-04 15:25:47 +01:00
J. Diego Rodríguez Royo 74ea553b63 Bump aiohomeconnect to 0.16.2 (#139750) 2025-03-04 15:17:05 +01:00
Joost Lekkerkerker d9690507a4 Add Apollo Automation virtual integration (#139751)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-03-04 15:08:14 +01:00
Marc Mueller e69b4f389f Simplify lint-only job config [ci] (#139748) 2025-03-04 14:07:27 +01:00
Paulus Schoutsen 8a97c2bfca VoIP block non-TTS announcements (#139658)
* VoIP block non-TTS announcements

* Migrate VoIP to use pipeline token
2025-03-04 08:02:58 -05:00
Robert Resch d5ba55d2fc Disable test results upload on forks (#139749)
Disable test result uploads on forks
2025-03-04 13:27:51 +01:00
J. Nick Koston d38e046494 Bump bleak-esphome to 2.10.2 (#139731)
* Bump bleak-esphome to 2.10.0

changelog: https://github.com/Bluetooth-Devices/bleak-esphome/compare/v2.9.0...v2.10.0

* again for wheel fix

* disable name check since its a binary now
2025-03-04 11:49:44 +01:00
Robert Resch 50cec420ef Upload test results to codecov (#138512)
* Upload test results to codecov

* Upload tests results in single job
2025-03-04 11:43:41 +01:00
Markus Adrario 23dac3933f Fix Homee brightness sensors reporting in percent (#139409)
* fix brigtness sensor having percent as unit.

* add test for percent-brightness-sensor

* remove valve position and update tests

* Removed test, because covered by Snapshots

* fix review comments

* move device calss to init.

* fix test

* fix review comments

* add battery sensor back to test fixture

* fix
2025-03-04 11:40:36 +01:00
Erik Montnemery 32f59bfd25 Remove unused constant from recorder (#139741) 2025-03-04 11:39:35 +01:00
Erik Montnemery 4f36bbdfe6 Fix regression in template flag introduced by #139645 (#139742) 2025-03-04 11:33:27 +01:00
J. Diego Rodríguez Royo 973fee9fe1 Delete refresh after a non-breaking error at event stream at Home Connect (#139740)
* Delete refresh after non-breaking error

And improve how many time does it take to retry to open stream

* Update tests
2025-03-04 11:07:44 +01:00
Norbert Rittel 13001faf51 Improve strings in openai_conversation.generate_image action (#139736)
Use descriptive wording, fix sentence-casing.
2025-03-04 09:57:38 +01:00
Marcel van der Veldt 9f780a5308 Fix ability to remove orphan device in Music Assistant integration (#139431)
* Fix ability to remove orphan device in Music Assistant integration

* Add test

* Remove orphaned device entries at startup as well

* adjust mocked client
2025-03-04 09:56:42 +01:00
Abílio Costa d87c963db5 Prevent zero interval in Calendar get_events service (#139378)
* Prevent zero interval in Calendar get_events service

* Fix holiday calendar tests

* Remove redundant entity_id

* Use translation for exception

* Replace check with voluptuous validator

* Revert strings.xml
2025-03-04 09:52:29 +01:00
Allen Porter c6a9472fdb Add nest translation string for already_in_progress (#139727) 2025-03-04 09:46:56 +01:00
Allen Porter cd0a983850 Bump google-nest-sdm to 7.1.4 (#139728) 2025-03-04 09:28:10 +01:00
Petro31 890d3f4af4 Add a base class for template entities to inherit from (#139645)
* add-abstract-template-entity-base-class

* review 1 changes
2025-03-04 07:23:05 +01:00
Joshua Leaper a778092941 Support up to 8 AUX outputs in Ness Alarm (#139718)
Support up to 8 AUX outputs
2025-03-03 23:35:20 +00:00
J. Nick Koston 9ea582de26 Bump sense-energy to 0.13.6 (#139714)
changes: https://github.com/scottbonline/sense/releases/tag/0.13.6
2025-03-03 11:20:25 -10:00
Shay Levy b6f2d8f30b Bump aiowebostv to 0.7.2 (#139712) 2025-03-03 10:26:16 -10:00
G Johansson 139072bb59 Bump holidays to 0.68 (#139711) 2025-03-03 21:47:38 +02:00
Paul Bottein 07a93dade2 Add translations for switch state by device class (#139693) 2025-03-03 20:24:36 +01:00
Allen Porter 9dc04cb088 Improve failure handling and logging for invalid map responses (#139681) 2025-03-03 20:23:29 +01:00
StaleLoafOfBread 890c672f8c Add charging binary_sensor so front end can render battery icon properly (#139684)
* Add charging binary sensor

* Add charging binary sensor test
2025-03-03 20:21:05 +01:00
Simone Chemelli e28e4d210f Bump aiocomelit to 0.11.2 (#139707) 2025-03-03 20:19:09 +01:00
Joost Lekkerkerker dcd2d42894 Abort SmartThings flow if default_config is not enabled (#139700)
* Abort SmartThings flow if default_config is not enabled

* Abort SmartThings flow if default_config is not enabled

* Abort SmartThings flow if default_config is not enabled
2025-03-03 20:07:07 +01:00
Allen Porter e47e151259 Add additional roborock debug logging (#139680) 2025-03-03 21:02:45 +02:00
Andrew Jackson 2c44043e6a Bump mastodon.py to 2.0.1 (#139701)
* Bump mastodon to 2.0.1

* Fix mypy
2025-03-03 20:57:30 +02:00
Norbert Rittel f248901ea8 Grammar fixes in user-facing strings of the LinkPlay integration (#139709)
Grammar fixes in user-facing string of the LinkPlay integration

Fix spelling of "set up", "media player", "ID" and improve the descriptions of the `play_preset` action.
2025-03-03 20:55:47 +02:00
Elias Wernicke 62b6be900f Add complete item intent function for todo component (#127806)
* add complete item intent

* fix error and add tests

* fix merge conflict

* improve error tests

* improve error tests

* add response_key

* add check for non completed

---------

Co-authored-by: Michael Hansen <mike@rhasspy.org>
2025-03-03 12:16:43 -06:00
J. Nick Koston 1b15df3075 Bump ESPHome stable BLE version to 2025.2.2 (#139704)
ensure proxies have https://github.com/esphome/esphome/pull/8328
so they do not reboot themselves if disconnecting takes
too long
2025-03-03 12:44:49 -05:00
Norbert Rittel 229407d685 Fix missing sentence-casing in three Fully Kiosk Browser strings (#139705)
Fix missing sentence-casing in Fully Kiosk Browser strings
2025-03-03 18:25:18 +01:00
Simone Chemelli aaecb47125 Add strict typing to Comelit (#139455)
* Add quality scale and strict typing to Comelit

* mypy

* fix strings

* remove quality scale

* revert quality scale changes

* improve typing

* letfover

* update typing based on new lib

* align to platform

* cleanup

* apply review comments (part 1)

* apply review comment ( part 2)

* apply review comments

* align

* align test data

* TypedDict

* better casting
2025-03-03 17:57:42 +01:00
Joakim Sørensen b17ee78dec Bump hass-nabucasa from 0.92.0 to 0.94.0 (#139697) 2025-03-03 15:51:04 +00:00
Erik Montnemery 20e48054cf Fix stale docstrings in onboarding tests (#139696) 2025-03-03 15:08:39 +00:00
cs12ag ee486c269c Fix unique identifiers where multiple IKEA Tradfri gateways are in use (#136060)
* Create unique identifiers where multiple gateways are in use

Resolving issue https://github.com/home-assistant/core/issues/134497

* Added migration function to __init__.py

Added migration function to execute upon initialisation, to:
a) remove the erroneously-added config)_entry added to the device (gateway B gets added as a config_entry to a device associated to gateway A), and
b) swap out the non-unique identifiers for genuinely unique identifiers.

* Added tests to simulate migration from bad data scenario (i.e. explicitly executing migrate_entity_unique_ids() from __init__.py)

* Ammendments suggested in first review

* Changes after second review

* Rewrite of test_migrate_config_entry_and_identifiers after feedback

* Converted migrate function into major version, updated tests

* Finalised variable naming convention per feedback, added test to validate config entry migrated to v2

* Hopefully final changes for cosmetic / comment stucture

* Further code-coverage in test_migrate_config_entry_and_identifiers()

* Minor test corrections

* Added test for non-tradfri identifiers
2025-03-03 14:06:25 +01:00
Paulus Schoutsen aee891434f Avoid duplicate chat log content (#139679) 2025-03-03 11:46:40 +01:00
Brett Adams 5472345f45 Add additional garage door code to Advantage Air (#139687)
add Garage door
2025-03-03 11:45:04 +01:00
Norbert Rittel 572534b306 Fix missing camel-case in one "ElevenLabs" string (#139686) 2025-03-03 10:18:30 +01:00
Erik Montnemery 3c363eb5ce Adjust type hints in update entity (#129387)
* Adjust type hints in update entity

* Update allowed return type of update_percentage

---------

Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-03-03 10:17:13 +01:00
andresb5555 7e4432e321 Do not force logfile to roll over when using TimedRotatingFileHandler (#128301)
Do not force log file to roll over when using TimedRotatingFileHandler
2025-03-02 22:07:35 +01:00
Elias Wernicke 5ae7109561 Increase test coverage for todo intent (#135960)
* move intent tests to file

* add tests for errors
2025-03-02 22:04:25 +01:00
hydazz 4602c0a1c3 Add Night mode and HVACAction to Advantage Air (#137475)
* add night mode toggle

* populate AC's action

* set hvac action on zones

* update tests

* show zones as off if AC is off

---------

Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-03-02 21:59:44 +01:00
Michel van de Wetering 53bc5ff029 Keep entered values in form when connecting to Epson projector fails (#135402)
Add suggested values to form
2025-03-02 21:41:38 +01:00
martin12as c782a6ab63 Improve outlet constant naming for NUT (#139660)
* Update const.py

Fixed to match string.json

* Update const.py
2025-03-02 21:38:12 +01:00
Trevor Warwick 23644a60ac Improve Linkplay device unavailability detection (#138457)
* Dampen reachability changes

Retry a few times before declaring player is unavailable

* Fix ruff-format complaint

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>

* Fix ruff-format complaint

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>

* Fix ruff-format complaint

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>

* Fix duplicated change

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>

---------

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2025-03-02 21:26:54 +01:00
Joost Lekkerkerker 14e66ffef4 Fetch integration list from next branch for analytics insights (#137250)
Fetch integration list from next branch
2025-03-02 21:21:47 +01:00
karwosts fa40d02a07 Add model_id filter to device selector (#135646)
* Add model_id filter to device selector

* Rerun CI
2025-03-02 21:15:37 +01:00
Niklas Neesen 8536f2b4cb Fix vicare exception for specific ventilation device type (#138343)
* fix for exception for specific ventilation device type + tests

* fix for exception for specific ventilation device type + tests

* New Testset just for fan

* update test_sensor.ambr
2025-03-02 20:57:13 +01:00
J. Nick Koston 387bf83ba8 Bump aioesphomeapi to 29.3.2 (#139653)
changelog: https://github.com/esphome/aioesphomeapi/compare/v29.3.1...v29.3.2
2025-03-02 20:53:45 +01:00
Norbert Rittel 18b0f54a3e Fix typo in outlet_2_load_off of NUT integration (#139656)
Fix typo in `outlet_2_load_off`

Fix small copy & paste error in https://github.com/home-assistant/core/pull/139044
2025-03-02 20:49:19 +01:00
Nathan Spencer f76e295204 Add fault event to balboa (#138623)
* Add fault sensor to balboa

* Use an event instead of sensor for faults

* Don't set fault initially in conftest

* Use event type per fault message code

* Set fault to None in conftest
2025-03-02 20:24:27 +01:00
Norbert Rittel e63b17cd58 Make spelling of "All-Link" consistent in Insteon integration (#139651)
"All-Link" is a fixed term in the Insteon integration that should be kept in translations. To clarify that this commit makes all occurrences in the Insteon integration consistent (plus fixing one typo).

On the other end the word "database" is sentence-cased as this can be translated, just as "record" etc.

Finally the description of the "Load All-Link database" action is made consistent using descriptive third-person singular as all other actions do.
2025-03-02 20:04:53 +01:00
martin12as 05e23f0fc7 Add nut commands to turn off/on outlet 1 & 2 (#139044)
* Update const.py

* Update strings.json

* Update homeassistant/components/nut/strings.json

Co-authored-by: tdfountain <174762217+tdfountain@users.noreply.github.com>

* Update homeassistant/components/nut/strings.json

Co-authored-by: tdfountain <174762217+tdfountain@users.noreply.github.com>

---------

Co-authored-by: tdfountain <174762217+tdfountain@users.noreply.github.com>
2025-03-02 20:00:05 +01:00
Joost Lekkerkerker fca4ef3b1e Fix scope comparison in SmartThings (#139652) 2025-03-02 19:52:37 +01:00
mvn23 1226354823 Finish removing import from configuration.yaml support from opentherm_gw (#139643) 2025-03-02 17:37:48 +01:00
Simon Lamon 40099547ef Add typing/async to NMBS (#139002)
* Add typing/async to NMBS

* Fix tests

* Boolean fields

* Update homeassistant/components/nmbs/sensor.py

Co-authored-by: Jorim Tielemans <tielemans.jorim@gmail.com>

---------

Co-authored-by: Shay Levy <levyshay1@gmail.com>
Co-authored-by: Jorim Tielemans <tielemans.jorim@gmail.com>
2025-03-02 17:36:37 +01:00
mvn23 de4540c68e Remove deprecated entity migration from opentherm_gw (#139641) 2025-03-02 17:28:11 +01:00
mvn23 d006d33dc0 Remove deprecated device migration from opentherm_gw (#139612) 2025-03-02 16:52:25 +01:00
J. Nick Koston 4c8a58f7cc Fix broken link in ESPHome BLE repair (#139639)
ESPHome always uses .0 in the URL for the changelog,
and we never had a patch version in the stable
BLE version field so we need to switch it to
.0 for the URL.
2025-03-02 16:50:35 +01:00
MarioZG 8d6178ffa6 Add last updated attribute to UK transport train sensor (#139352)
added last updated attribute to train sensor

Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-03-02 16:48:57 +01:00
Manu 0a3562aca3 Add prefix path support to pyLoad integration (#139139)
* Add prefix path configuration support

* fix typo

* formatting

* uppercase

* changes

* redact host
2025-03-02 16:45:57 +01:00
J. Nick Koston c9abe76023 Use multiple indexed group-by queries to get start time states for MySQL (#138786)
* tweaks

* mysql

* mysql

* Update homeassistant/components/recorder/history/modern.py

* Update homeassistant/components/recorder/history/modern.py

* Update homeassistant/components/recorder/const.py

* Update homeassistant/components/recorder/statistics.py

* Apply suggestions from code review

* mysql

* mysql

* cover

* make sure db is fully init on old schema

* fixes

* fixes

* coverage

* coverage

* coverage

* s/slow_dependant_subquery/slow_dependent_subquery/g

* reword

* comment that callers are responsible for staying under the limit

* comment that callers are responsible for staying under the limit

* switch to kwargs

* reduce branching complexity

* split stats query

* preen

* split tests

* split tests
2025-03-02 15:13:06 +01:00
starkillerOG 0c803520a3 Motion blind type list (#139590)
* Add blind_type_list

* fix

* styling

* fix typing

* Bump motionblinds to 0.6.26
2025-03-02 14:40:28 +01:00
rappenze 5ac3fe6ee1 Fibaro integration refactorings (#139624)
* Fibaro integration refactorings

* Fix execute_action

* Add test

* more tests

* Add tests

* Fix test

* More tests
2025-03-02 14:38:56 +01:00
Martreides b7bedd4b8f Fix Nederlandse Spoorwegen to ignore trains in the past (#138331)
* Update NS integration to show first next train instead of just the first.

* Handle no first or next trip.

* Remove debug statement.

* Remove seconds and revert back to minutes.

* Make use of dt_util.now().

* Fix issue with next train if no first train.
2025-03-02 14:32:10 +01:00
Maghiel Dijksman 0694f9e164 Fix Tuya unsupported Temperature & Humidity Sensors (with or without external probe) (#138542)
* add category qxj for th sensor with external probe. partly fixes #136472

* add TEMP_CURRENT_EXTERNAL for th sensor with external probe. fixes #136472

* ruff format

* add translation_key temperature_external for TEMP_CURRENT_EXTERNAL

---------

Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-03-02 14:25:19 +01:00
J. Nick Koston 5b1f3d3e7f Fix arm vacation mode showing as armed away in elkm1 (#139613)
Add native arm vacation mode support to elkm1

Vacation mode is currently implemented as a custom
service which will be deprecated in a future PR.

Note that the custom service was added long before
HA had a native vacation mode which was added
in #45980
2025-03-02 14:23:40 +01:00
Maciej Bieniek d922c723d4 Add LinkedGo virtual integration (#139625) 2025-03-02 14:19:52 +01:00
Joost Lekkerkerker 3eadfcc01d Still request scopes in SmartThings (#139626)
Still request scopes
2025-03-02 14:17:56 +01:00
Maciej Bieniek 29f680f912 Add FrankEver virtual integration (#139629)
* Add FranvEver virtual integration

* Fix file name
2025-03-02 14:12:54 +01:00
Alexey ALERT Rubashёff ee2b53ed0f Bump pyoverkiz to 1.16.2 (#139623) 2025-03-02 14:10:45 +01:00
Manu b0b5567316 Add update_habit action to Habitica integration (#139311)
* Add update_habit action

* icons
2025-03-02 14:04:13 +01:00
Joost Lekkerkerker e6c946b3f4 Bump pysmartthings to 2.4.1 (#139627) 2025-03-02 13:15:43 +01:00
Norbert Rittel b2c7c5b1aa Treat "Core" as name, fix grammar in reload_core_config action (#139622)
* Treat "Core" as name, fix grammar in `reload_core_config` action

Change three occurrences of "core" to "Core" so they are not translated but kept as a name instead.

Fix singular/plural mismatch in the field description of the `reload_core_config` action.

* Change "us customary" to "US customary"
2025-03-02 11:05:25 +01:00
Jan Bouwhuis 220509fd6c Fix body text of imap message not available in custom event data template (#139609) 2025-03-01 23:00:22 -05:00
Paulus Schoutsen 7293ae5d51 Fix type for ESPHome assist satellite events (#139618) 2025-03-01 22:59:14 -05:00
wittypluck 4a7fd89abd Bump pyopenweathermap to 0.2.2 and remove deprecated API version v2.5 (#139599)
* Bump pyopenweathermap

* Remove deprecated API mode v2.5
2025-03-02 02:32:55 +01:00
J. Nick Koston 077ff63b38 Bump inkbird-ble to 0.7.1 (#139603)
changelog: https://github.com/Bluetooth-Devices/inkbird-ble/compare/v0.7.0...v0.7.1
2025-03-02 00:51:09 +01:00
Shay Levy 55fd5fa869 Bump aioshelly to 13.1.0 (#139601)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-03-02 00:12:19 +01:00
G Johansson e3eb6051de Fix duplicate unique id issue in Sensibo (#139582)
* Fix duplicate unique id issue in Sensibo

* Fixes

* Mods
2025-03-02 00:04:13 +01:00
Brett Adams 3e9304253d Bump Tesla Fleet API to v0.9.12 (#139565)
* bump

* Update manifest.json

* Fix versions

* remove tesla_bluetooth

* Remove mistake
2025-03-01 23:58:15 +01:00
Shay Levy cc8ed2c228 Fix demo valve platform to use AddConfigEntryEntitiesCallback (#139602) 2025-03-01 23:29:42 +01:00
J. Nick Koston 89b655c192 Fix handling of NaN float values for current humidity in ESPHome (#139600)
fixes #131837
2025-03-01 16:13:04 -06:00
Robert Resch 56ddfa9ff8 Bump deebot-client to 12.3.1 (#139598) 2025-03-01 23:05:55 +01:00
Manu a2a11ad02e Update quality scale to platinum 🏆️ for IronOS integration (#138217)
Update status in iron_os quality_scale.yaml
2025-03-01 22:55:49 +01:00
Tatham Oddie f7927f9da1 Introduce demo valve (#138187) 2025-03-01 22:54:48 +01:00
Simone Chemelli 13918f07d8 Switch cleanup for Shelly (part 2) (#138922)
* Switch cleanup for Shelly (part 2)

* apply review comment

* Update tests/components/shelly/test_climate.py

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

* apply review comments

---------

Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com>
2025-03-01 22:39:19 +01:00
Manu 35825be12b Update quality scale to platinum 🏆️ for pyLoad integration (#138891)
* Add quality scale file to pyLoad integration

* set strict-typing to done

* set parallel-updates to done

* docs

* update docs

* flow coverage done

* set platinum quality scale
2025-03-01 22:36:51 +01:00
Denis Shulyaka 1786bb9903 Use model list to check anthropic API key (#139307)
Anthropic model list
2025-03-01 22:28:48 +01:00
Manu 3588784f1e Add create_reward action to Habitica integration (#139304)
Add create_reward action to Habitica
2025-03-01 22:27:31 +01:00
Paulus Schoutsen 2cce1b024e Migrate Assist Pipeline to use TTS stream (#139542)
* Migrate Pipeline to use TTS stream

* Fix tests
2025-03-01 21:43:00 +01:00
peteS-UK c168695323 Clean up squeezebox build_item_response part 1 (#139321)
* initial

* final

* is internal change

* test data coverage

* Review fixes

* final
2025-03-01 14:18:30 -06:00
Jan Bouwhuis 913a4ee9ba Improve certificate handling in MQTT config flow (#137234)
* Improve mqtt broker certificate handling in config flow

* Expand test cases
2025-03-01 21:14:08 +01:00
Markus Adrario dd21d48ae4 Homee: fix watchdog icon (#139577)
fix watchdog icon
2025-03-01 20:53:06 +01:00
Joost Lekkerkerker b3f14d72c0 Don't require not needed scopes in SmartThings (#139576)
* Don't require not needed scopes

* Don't require not needed scopes
2025-03-01 20:47:42 +01:00
Trevor Morgan 51beb1c0a8 Add simplisafe OUTDOOR_ALARM_SECURITY_BELL_BOX device type (#134386)
* Update binary_sensor.py to included OUTDOOR_ALARM_SECURITY_BELL_BOX device type

Add support for DeviceTypes.OUTDOOR_ALARM_SECURITY_BELL_BOX

This is an external siren device in Simplisafe which is not  currently discovered with the HA integration

* Fixed formatting error

---------

Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-03-01 20:26:04 +01:00
peteS-UK 0c5766184b Fix Manufacturer naming for Squeezelite model name for Squeezebox (#139586)
Squeezelite Manufacturer Fix
2025-03-01 20:22:34 +01:00
starkillerOG b1a2b89691 Bump motionblinds to 0.6.26 (#139591) 2025-03-01 20:18:52 +01:00
Norbert Rittel 4813da33d6 Improve field descriptions of zha.permit action (#139584)
Make the field descriptions of `source_ieee` and `install_code` UI-friendly by cross-referencing them using their friendly names to allow matching translations.

Better explain the alternative of using the `qr_code` field by adding that this contains both the IEEE address and the Install code of the joining device.
2025-03-01 20:16:32 +01:00
Simone Chemelli d4099ab917 Bump aiocomelit to 0.11.1 (#139589) 2025-03-01 20:16:11 +01:00
Joris Drenth ee206938d8 Update wallbox to 0.8.0 (#139553)
Update Wallbox dependencies
2025-03-01 19:59:13 +01:00
M-A 9fe08f292d Bump env_canada to 0.8.0 (#138237)
* Bump env_canada to 0.8.0

* Fix requirements*.txt

* Grepped more

---------

Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-03-01 19:58:45 +01:00
mvn23 9a331de878 Remove deprecated import from configuration.yaml from opentherm_gw (#139581)
* Remove deprecated import from configuration.yaml in opentherm_gw

* Remove tests for removed funcionality from opentherm_gw
2025-03-01 19:45:07 +01:00
Jan Bouwhuis 2de941bc11 Fix - Allow brightness only light MQTT json light to be set up using the brightness flag or via supported_color_modes (#139585)
* Fix - Allow brightness only light MQTT json light to be set up using the `brightness` flag or via  `supported_color_modes`

* Improve comment
2025-03-01 19:35:39 +01:00
J. Nick Koston c5e0418f75 Bump aiohomekit to 3.2.8 (#139579)
changelog: https://github.com/Jc2k/aiohomekit/compare/3.2.7...3.2.8
2025-03-01 18:41:11 +02:00
Simone Chemelli 679b57e450 Add strict typing to Vodafone Station (#139573) 2025-03-01 14:22:14 +01:00
Guido Schmitz 91eba0855e Handle IPv6 URLs in devolo Home Network (#139191)
* Handle IPv6 URLs in devolo Home Network

* Use yarl
2025-03-01 13:29:50 +01:00
Josef Zweck 43f48b8562 Bump azure_storage quality to platinum (#139452) 2025-03-01 13:23:27 +01:00
Joost Lekkerkerker df95902004 Only determine SmartThings swing modes if we support it (#139571)
Only determine swing modes if we support it
2025-03-01 13:08:28 +01:00
epenet 3edc7913de Fix blog post link in comment (#139568) 2025-03-01 13:06:10 +01:00
Joost Lekkerkerker 1852052dff Add suggested area to SmartThings (#139570)
* Add suggested area to SmartThings

* Add suggested areas to SmartThings
2025-03-01 13:05:58 +01:00
Joost Lekkerkerker fe5cd5c55c Validate scopes in SmartThings config flow (#139569) 2025-03-01 12:47:58 +01:00
Jan-Philipp Benecke 042e4d20c5 Bump aiowebdav2 to 0.3.1 (#139567) 2025-03-01 12:37:44 +01:00
Norbert Rittel dfe1921737 Improve description of media_content_type in media_extractor.play_media action (#139559)
* Improve `media_content_type` in  `media_extractor.play_media` action

In the UI the `media_content_type` field of the `media_extractor.play_media` action already presents a selector with the choices MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST MUSIC.

Therefore these can be removed from the field description to avoid any over-translation that will create an unnecessary  mismatch in the UI.

* Fix casing of  `media_extractor.play_media` action name
2025-03-01 12:12:58 +01:00
Norbert Rittel 2c620f1f60 Improve description of door field in subaru.unlock_specific_door action (#139558)
* Improve description of `door` field in `subaru.unlock_specific_door` action

In the UI the `door` field of the `subaru.unlock_specific_door` action presents three radio buttons for the three possible choices 'all', 'driver' and 'tailgate'.

Therefore the field description should no longer repeat those options to avoid over-translation that will not match the actual choices.

In addition proper sentence-casing is applied to several title keys.

* Fix sentence-casing in two title keys
2025-03-01 12:12:36 +01:00
Joost Lekkerkerker 66a17bd072 Bump pysmartthings to 2.4.0 (#139564) 2025-03-01 12:06:16 +01:00
Filip Agh 18217a594f Fix update data for multiple Gree devices (#139469)
fix sync date for multiple devices

do not use handler for explicit update devices as internal communication lib do not provide which device is updated
use ha update loop

copy data object to prevent rewrite data from internal lib

allow more time to process response before log warning about long wait for response and make log message more clear
2025-03-01 11:50:24 +01:00
J. Nick Koston a6e2dc485b Bump orjson to 3.10.15 (#135223) 2025-03-01 10:44:04 +01:00
Juan Grande 8e7960fa0e Fix bug in derivative sensor when source sensor's state is constant (#139230)
Previously, when the source sensor's state remains constant, the derivative
sensor repeats its latest value indefinitely.

This patch fixes this bug by consuming the state_reported event and updating
the sensor's output even when the source sensor doesn't change its state.
2025-03-01 09:10:35 +01:00
Daniele Ricci 1dc6a94093 Fix caldav todo list not updating after adding items with Assist (#135980)
caldav: fix todo list not updating after adding items with Assist
2025-02-28 21:15:28 -08:00
LaithBudairi 615d79b429 Add missing 'state_class' attribute for Growatt plant sensors (#132145)
* Add missing 'state_class' attribute for Growatt plant sensors

* Update total.py

* Update total.py 'TOTAL_INCREASING'

* Update total.py "maximum_output" -> 'TOTAL_INCREASING'

* Update homeassistant/components/growatt_server/sensor/total.py

---------

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2025-02-28 23:58:39 +01:00
andylittle ebd6daa31d Tuya tyd fix (#135558)
Add support for tuya tyd light
2025-02-28 23:47:40 +01:00
Joost Lekkerkerker d6750624ce Add SmartThings hub connections (#139549) 2025-02-28 23:32:09 +01:00
J. Nick Koston 577b22374a Revert polling changes to HomeKit Controller (#139550)
This reverts #116200

We changed the polling logic to avoid polling if all chars are marked as watchable
to avoid crashing the firmware on a very limited set of devices as it was
more in line with what iOS does. In the end, the user ended up replacing
the device in #116143 because it turned out to be unreliable in other
ways. The vendor has since issued a firmware update that may resolve
the problem with all of these devices.

In practice it turns out many more devices
report that chars are evented and never send events. After a few months
of data and reports the trade-off does not seem worth it since
users are having to set up manual polling on a wide range of
devices. The amount of devices with evented chars that do not
actually send state vastly exceeds the number of devices that
might crash if they are polled too often so restore the previous
behavior

fixes #138561
fixes #100331
fixes #124529
fixes #123456
fixes #130763
fixes #124099
fixes #124916
fixes #135434
fixes #125273
fixes #124099
fixes #119617
2025-02-28 23:25:50 +01:00
J. Nick Koston ee1fe2cae4 Bump bleak-esphome to 2.9.0 (#139467)
* Bump bleak-esphome to 2.9.0

changelog: https://github.com/Bluetooth-Devices/bleak-esphome/compare/v2.8.0...v2.9.0

* fixes
2025-02-28 16:17:44 -06:00
Joost Lekkerkerker db05aa17d3 Add SmartThings Viper device info (#139548) 2025-02-28 23:03:57 +01:00
Joost Lekkerkerker b1ee019e3a Bump pysmartthings to 2.3.0 (#139546) 2025-02-28 23:02:06 +01:00
Paulus Schoutsen b43a7ff1fe Stream the TTS result from webview (#139543) 2025-02-28 23:01:39 +01:00
Joost Lekkerkerker 2d6068b842 Create device for the hub in SmartThings (#139545)
* Create device for the hub in SmartThings

* Create device for the hub in SmartThings

* Create device for the hub in SmartThings
2025-02-28 22:58:35 +01:00
J. Nick Koston ac4c379a0e Bump PySwitchBot to 0.56.1 (#139544)
changelog: https://github.com/sblibs/pySwitchbot/compare/0.56.0...0.56.1
2025-02-28 15:42:33 -06:00
Joost Lekkerkerker 00b7c4f9ef Improve SmartThings OCF device info (#139547) 2025-02-28 23:30:57 +02:00
Joost Lekkerkerker 3f48826370 Bump pysmartthings to 2.2.0 (#139539) 2025-02-28 21:06:45 +01:00
StaleLoafOfBread ed06831e9d Fix alert not respecting can_acknowledge setting (#139483)
* fix(alert): check can_ack prior to acking

* fix(alert): add test for when can_acknowledge=False

* fix(alert): warn on can_ack blocking an ack

* Raise error when trying to acknowledge alert with can_acknowledge set to False

* Rewrite can_ack check as guard

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

* Make can_ack service error msg human readable because it will show up in the UI

* format with ruff

* Make pytest aware of service error when acking an unackable alert

---------

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2025-02-28 20:59:35 +01:00
Marcel van der Veldt c21234672d Ensure Hue bridge is added first to the device registry (#139438) 2025-02-28 20:56:43 +01:00
G Johansson 32950df0b7 Specify recorder as after dependency in sql integration (#139037)
* Specify recorder as after dependency in sql integration

* Remove hassfest exception

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2025-02-28 20:51:56 +01:00
Cameron Ring 0f615bbe4f Add OptionsFlowHandler test for Lutron (#139463) 2025-02-28 20:50:39 +01:00
J. Nick Koston 5a6ffe1901 Update Bluetooth remote config entries if the MAC is corrected (#139457)
* fix ble mac

* fixes

* fixes

* fixes

* restore deleted test
2025-02-28 20:49:31 +01:00
rappenze 6ce48eab45 Use new pyfibaro library features (#139476) 2025-02-28 20:47:03 +01:00
Simone Chemelli 437e545116 Rework Comelit tests (#139475)
* Rework Comelit tests

* allign

* restore coverage
2025-02-28 20:45:47 +01:00
Paulus Schoutsen 1a80934593 Move TTS entity to own file (#139538)
* Move entity to own file

* Move entity tests
2025-02-28 20:40:13 +01:00
Joost Lekkerkerker 455363871f Use last event as color mode in SmartThings (#139473)
* Use last event as color mode in SmartThings

* Use last event as color mode in SmartThings

* Fix
2025-02-28 20:39:49 +01:00
Joost Lekkerkerker 39bc37d225 Remove orphan devices on startup in SmartThings (#139541) 2025-02-28 20:33:25 +01:00
Paulus Schoutsen 90fc6ffdbf Add support for continue conversation in Assist Pipeline (#139480)
* Add support for continue conversation in Assist Pipeline

* Also forward to ESPHome

* Update snapshot

* And mobile app
2025-02-28 13:15:31 -06:00
Joost Lekkerkerker 086c91485f Set SmartThings delta energy to Total (#139474) 2025-02-28 20:03:24 +01:00
Norbert Rittel bf27ccce17 Clarify description of icloud.update action (#139535)
Currently the description of the `icloud.update` action can be easily misunderstood as just updating the device list or forcing a software update on all devices.

This commit changes the description to make clear that it asks for a state update of all devices.
2025-02-28 19:58:26 +01:00
Paulus Schoutsen 70bb56e0fc Text-to-Speech refactor (#139482)
* Refactor TTS

* More cleanup

* Cleanup

* Consolidate more

* Inline another function

* Inline another function

* Improve cleanup
2025-02-28 12:36:12 -06:00
Michael Hansen 49c27ae7bc Check area temperature sensors in get temperature intent (#139221)
* Check area temperature sensors in get temperature intent

* Fix candidate check

* Add new code back in

* Remove cruft from climate
2025-02-28 13:02:30 -05:00
Nathan Spencer e9bb4625d8 Set device class for wind direction weatherflow entities (#139397)
* Set wind_direction device class in weatherflow

* Remove measurement state_class from wind direction entities
2025-02-28 18:33:58 +01:00
Alexey ALERT Rubashёff 2e077cbf12 Bump pyoverkiz to 1.16.1 (#139532) 2025-02-28 17:32:07 +00:00
Bram Kragten 271d225e51 Update frontend to 20250228.0 (#139531) 2025-02-28 17:05:36 +01:00
Michael Hansen fca19a3ec1 Move climate intent to homeassistant integration (#139371)
* Move climate intent to homeassistant integration

* Move get temperature intent to intent integration

* Clean up old test
2025-02-28 10:25:38 -05:00
Josef Zweck 0681652aec Add diagnostics to onedrive (#139516)
* Add diagnostics to onedrive

* redact PII

* add raw data
2025-02-28 16:18:57 +01:00
Robert Resch 5fa5d08b18 Bump wheels to 2025.02.0 (#139525) 2025-02-28 16:16:23 +01:00
Norbert Rittel 0f0866cd52 Improve description of mode field in geniushub.set_zone_mode action (#139513)
Improve description of `mode` field in 'geniushub.set_zone_mode' action

As the three choices for the `mode` field show up as radio buttons in the UI the description does not need to repeat them.

This improves translations by avoiding any over-translation of these values.
2025-02-28 17:03:47 +02:00
Robert Svensson 1b27365c58 Suppress unsupported event 'EVT_USP_RpsPowerDeniedByPsuOverload' by bumping aiounifi to v83 (#139519)
Bump aiounifi to v83
2025-02-28 17:00:31 +02:00
Joost Lekkerkerker 3cd7f50216 Bump yt-dlp to 2025.02.19 (#139526) 2025-02-28 15:47:51 +01:00
Jeef 40d2d6df2c Bump weatherflow4py to 1.3.1 (#135529)
* version bump of dep

* update requirements
2025-02-28 14:32:52 +01:00
Robert Resch c2a7736417 Don't split wheels builder anymore (#139522) 2025-02-28 14:30:47 +01:00
Brett Adams ac15d9b3d4 Fix shift state in Teslemetry (#139505)
* Fix shift state

* Different fix
2025-02-28 14:26:39 +01:00
Erik Montnemery 228a4eb391 Improve error handling in CoreBackupReaderWriter (#139508) 2025-02-28 14:25:35 +01:00
epenet 030a1460de Log a warning when replacing existing config entry with same unique id (#130567)
* Log a warning when replacing existing config entry with same unique id

* Exclude mobile_app

* Ignore custom integrations

* Apply suggestions from code review

* Apply suggestions from code review

* Update config_entries.py

* Fix handler

* Adjust and add tests

* Apply suggestions from code review

* Apply suggestions from code review

* Update comment

* Update config_entries.py

* Apply suggestions from code review
2025-02-28 14:20:39 +01:00
dependabot[bot] d157919da2 Bump actions/attest-build-provenance from 2.2.1 to 2.2.2 (#139489)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/f9eaf234fc1c2e333c1eca18177db0f44fa6ba52...bd77c077858b8d561b7a36cbe48ef4cc642ca39d)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-28 14:19:18 +01:00
Marcel van der Veldt b79c6e772a Add new mediatypes to Music Assistant integration (#139338)
* Bump Music Assistant client to 1.1.0

* Add some casts to help mypy

* Add handling of the new media types in Music Assistant

* mypy cleanup

* lint

* update snapshot

* Adjust tests

---------

Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-02-28 14:17:02 +01:00
Jan Bouwhuis d6f9040baf Make the Tuya backend library compatible with the newer paho mqtt client. (#139518)
* Make the Tuya backend library compatible with the newer paho mqtt client.

* Improve classnames and docstrings
2025-02-28 14:14:56 +01:00
dependabot[bot] 0310418efc Bump dawidd6/action-download-artifact from 8 to 9 (#139488)
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 8 to 9.
- [Release notes](https://github.com/dawidd6/action-download-artifact/releases)
- [Commits](https://github.com/dawidd6/action-download-artifact/compare/v8...v9)

---
updated-dependencies:
- dependency-name: dawidd6/action-download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-28 13:54:31 +01:00
dependabot[bot] 62dc0ac485 Bump actions/cache from 4.2.1 to 4.2.2 (#139490)
Bumps [actions/cache](https://github.com/actions/cache) from 4.2.1 to 4.2.2.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4.2.1...v4.2.2)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-28 13:38:56 +01:00
Joost Lekkerkerker 9a62b0f245 Enable ASYNC ruff rules (#139507)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-02-28 13:05:30 +01:00
Petro31 a296c5e9ad Add floor_entities function and filter (#136509)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-02-28 11:44:01 +00:00
pglab-electronics 12cb349160 Add Sensor to PG LAB Integration (#138802) 2025-02-28 11:07:01 +00:00
Erik Montnemery 5cf56ec113 Adjust recorder backup platform tests (#139492) 2025-02-28 11:44:58 +01:00
Erik Montnemery 1be9836663 Fail recorder.backup.async_pre_backup if Home Assistant is not running (#139491)
Fail recorder.backup.async_pre_backup if hass is not running
2025-02-28 11:44:16 +01:00
Jan-Philipp Benecke 9d10e0e054 Change webdav namespace to absolut URI (#139456)
* Change webdav namespace to absolut URI

* Add const file
2025-02-28 11:18:16 +01:00
Joost Lekkerkerker 05df572951 Bump pysmartthings to 2.1.0 (#139460) 2025-02-28 10:30:31 +01:00
Joost Lekkerkerker 6953c20a65 Set SmartThings suggested display precision (#139470) 2025-02-28 09:15:13 +01:00
Ivan Lopez Hernandez 4e8186491c Fix Gemini Schema validation for #139416 (#139478)
Fixed Schema validation for issue #139477
2025-02-27 19:10:42 -08:00
rappenze 6fa93edf27 Bump pyfibaro to 0.8.2 (#139471) 2025-02-27 22:27:18 +00:00
Joost Lekkerkerker ef13b35c35 Only lowercase SmartThings media input source if we have it (#139468) 2025-02-27 21:50:34 +00:00
J. Nick Koston 0afdd9556f Bump aioesphomeapi to 29.3.1 (#139465) 2025-02-27 21:45:13 +00:00
J. Nick Koston e11ead410b Add coverage to ensure we do not load base platforms before recorder (#139464) 2025-02-27 20:50:23 +00:00
Norbert Rittel ef7058f703 Improve descriptions of lyric.set_hold_time action and field (#139385)
* Fix misleading descriptions on lyric.set_hold_time action

While on Honeywell Lyric thermostats the user can set a "Hold Until" time of day, the set_hold_time action does define a time period instead (Example: 01:00:00)

Therefore both descriptions are incorrectly using "until" for explaining the purpose of the action itself and the `time_period` field. 

This commit re-words both and adds some additional context that helps users (and translators) better understand this action and its purpose.

In addition the action name is changed to proper sentence-casing.

* Replace "time" with "duration" for additional clarity
2025-02-27 22:47:20 +02:00
Josef Zweck 938855bea3 Improve onedrive migration (#139458) 2025-02-27 20:42:04 +01:00
Joost Lekkerkerker 4c00c56afd Bump pysmartthings to 2.0.1 (#139454) 2025-02-27 21:30:18 +02:00
Simone Chemelli 8cc7e7b76f Full test coverage for Vodafone Station init (#139451)
Full test coverage for Vodafone Station init
2025-02-27 20:07:12 +01:00
J. Diego Rodríguez Royo df006aeade Bump aiohomeconnect to 0.15.1 (#139445) 2025-02-27 19:23:46 +01:00
Joost Lekkerkerker ffac522554 Fix SmartThings diagnostics (#139447) 2025-02-27 19:39:18 +02:00
starkillerOG 9502dbee56 Add more diagnostic info to Reolink (#139436)
* Add diagnostic info

* Bump reolink-aio to 0.12.1

* Add tests
2025-02-27 19:39:01 +02:00
J. Nick Koston a339fbaa82 Bump aioesphomeapi to 29.3.0 (#139441) 2025-02-27 16:56:30 +00:00
Bram Kragten b02eaed6b0 Update frontend to 20250227.0 (#139437) 2025-02-27 16:42:08 +01:00
Joost Lekkerkerker df594748cf Bump ruff to 0.9.8 (#139434) 2025-02-27 15:00:24 +00:00
Paulus Schoutsen 744a7a0e82 Fix conversation agent fallback (#139421) 2025-02-27 15:51:40 +01:00
Joost Lekkerkerker f677b910a6 Add diagnostics to SmartThings (#139423) 2025-02-27 15:23:25 +01:00
Michael Arthur 0da6b28808 Add lawn mower entity id format (#139402)
* add missing entity id format

* use ENTITY_ID_FORMAT in mqtt lawn mower
2025-02-27 15:02:14 +01:00
Marcel van der Veldt f111a2c34a Fix Music Assistant media player entity features (#139428)
* Fix Music Assistant supported media player features

* Update supported features when player config changes

* Add tests
2025-02-27 15:30:29 +02:00
starkillerOG 59eb323f8d Bump reolink-aio to 0.12.1 (#139427) 2025-02-27 15:29:57 +02:00
Joost Lekkerkerker 7ae13a4d72 Bump pysmartthings to 2.0.0 (#139418)
* Bump pysmartthings to 2.0.0

* Fix

* Fix

* Fix

* Fix
2025-02-27 13:25:55 +01:00
J. Nick Koston 735b843f5e Bump bleak-esphome to 2.8.0 (#139426) 2025-02-27 12:22:43 +00:00
J. Nick Koston 5b1783e859 Bump habluetooth to 3.24.1 (#139420) 2025-02-27 11:41:27 +00:00
LG-ThinQ-Integration 7b14b6af0e Add water heater entity to LG ThinQ (#138257)
Co-authored-by: yunseon.park <yunseon.park@lge.com>
2025-02-27 11:03:44 +00:00
J. Diego Rodríguez Royo cc18ec2de8 Fix fetch options error for Home connect (#139392)
* Handle errors when obtaining options definitions

* Don't fetch program options if the program key is unknown

* Test to ensure that available program endpoint is not called on unknown program
2025-02-27 12:00:14 +01:00
Josef Zweck df59adf5d1 Add reconfiguration to azure_storage (#139414)
* Add reauthentication to azure_storage

* Add reconfigure to azure_storage

* iqs

* update string

* ruff
2025-02-27 11:06:03 +01:00
dependabot[bot] 8c98cede60 Bump actions/attest-build-provenance from 2.2.0 to 2.2.1 (#139406)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-27 10:44:50 +01:00
dependabot[bot] b1a70c86c3 Bump docker/build-push-action from 6.14.0 to 6.15.0 (#139407)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-27 10:44:13 +01:00
dependabot[bot] 63daed0ed6 Bump codecov/codecov-action from 5.3.1 to 5.4.0 (#139408)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-27 10:43:13 +01:00
Josef Zweck 2150a668b0 Add reauthentication to azure_storage (#139411)
* Add reauthentication to azure_storage

* update docstring
2025-02-27 10:17:57 +01:00
Josef Zweck b505722f38 Bump onedrive to 0.0.12 (#139410)
* Bump onedrive to 0.0.12

* Add alternative name
2025-02-27 10:00:50 +01:00
puddly 036eef2b6b Bump ZHA to 0.0.51 (#139383)
* Bump ZHA to 0.0.51

* Fix unit tests not accounting for primary entities
2025-02-26 22:22:08 +02:00
Michael Hansen f3fb7cd8e8 Bump intents to 2025.2.26 (#139387) 2025-02-26 20:14:03 +00:00
J. Diego Rodríguez Royo 42f55bf271 Small improvements to Home Connect strings and icons (#139386)
* Small improvements to Home Connect strings and icons

* Fix test
2025-02-26 21:02:00 +01:00
Erik Montnemery 6d7dad41d9 Bump hatasmota to 0.10.0 (#139382) 2025-02-26 21:31:45 +02:00
fwestenberg 9dbce6d904 Bump stookwijzer==1.6.1 (#139380) 2025-02-26 21:31:24 +02:00
Bram Kragten 7f0db3181d Bump version to 2025.4.0 (#139381) 2025-02-26 19:54:29 +01:00
513 changed files with 18547 additions and 4471 deletions
+5 -5
View File
@@ -94,7 +94,7 @@ jobs:
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@v8
uses: dawidd6/action-download-artifact@v9
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/frontend
@@ -105,7 +105,7 @@ jobs:
- name: Download nightly wheels of intents
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@v8
uses: dawidd6/action-download-artifact@v9
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/intents-package
@@ -509,7 +509,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
@@ -522,7 +522,7 @@ jobs:
- name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
@@ -531,7 +531,7 @@ jobs:
- name: Generate artifact attestation
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
uses: actions/attest-build-provenance@520d128f165991a6c774bcb264f323e3d70747f4 # v2.2.0
uses: actions/attest-build-provenance@bd77c077858b8d561b7a36cbe48ef4cc642ca39d # v2.2.2
with:
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
+98 -45
View File
@@ -40,7 +40,7 @@ env:
CACHE_VERSION: 11
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 9
HA_SHORT_VERSION: "2025.3"
HA_SHORT_VERSION: "2025.4"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13']"
# 10.3 is the oldest supported version
@@ -89,6 +89,7 @@ jobs:
test_groups: ${{ steps.info.outputs.test_groups }}
tests_glob: ${{ steps.info.outputs.tests_glob }}
tests: ${{ steps.info.outputs.tests }}
lint_only: ${{ steps.info.outputs.lint_only }}
skip_coverage: ${{ steps.info.outputs.skip_coverage }}
runs-on: ubuntu-24.04
steps:
@@ -142,6 +143,7 @@ jobs:
test_group_count=10
tests="[]"
tests_glob=""
lint_only=""
skip_coverage=""
if [[ "${{ steps.integrations.outputs.changes }}" != "[]" ]];
@@ -192,6 +194,15 @@ jobs:
test_full_suite="true"
fi
if [[ "${{ github.event.inputs.lint-only }}" == "true" ]] \
|| [[ "${{ github.event.inputs.pylint-only }}" == "true" ]] \
|| [[ "${{ github.event.inputs.mypy-only }}" == "true" ]] \
|| [[ "${{ github.event.inputs.audit-licenses-only }}" == "true" ]];
then
lint_only="true"
skip_coverage="true"
fi
if [[ "${{ github.event.inputs.skip-coverage }}" == "true" ]] \
|| [[ "${{ contains(github.event.pull_request.labels.*.name, 'ci-skip-coverage') }}" == "true" ]];
then
@@ -217,6 +228,8 @@ jobs:
echo "tests=${tests}" >> $GITHUB_OUTPUT
echo "tests_glob: ${tests_glob}"
echo "tests_glob=${tests_glob}" >> $GITHUB_OUTPUT
echo "lint_only": ${lint_only}
echo "lint_only=${lint_only}" >> $GITHUB_OUTPUT
echo "skip_coverage: ${skip_coverage}"
echo "skip_coverage=${skip_coverage}" >> $GITHUB_OUTPUT
@@ -240,7 +253,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache@v4.2.1
uses: actions/cache@v4.2.2
with:
path: venv
key: >-
@@ -256,7 +269,7 @@ jobs:
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache@v4.2.1
uses: actions/cache@v4.2.2
with:
path: ${{ env.PRE_COMMIT_CACHE }}
lookup-only: true
@@ -286,7 +299,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.1
uses: actions/cache/restore@v4.2.2
with:
path: venv
fail-on-cache-miss: true
@@ -295,7 +308,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache/restore@v4.2.1
uses: actions/cache/restore@v4.2.2
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -326,7 +339,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.1
uses: actions/cache/restore@v4.2.2
with:
path: venv
fail-on-cache-miss: true
@@ -335,7 +348,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache/restore@v4.2.1
uses: actions/cache/restore@v4.2.2
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -366,7 +379,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.1
uses: actions/cache/restore@v4.2.2
with:
path: venv
fail-on-cache-miss: true
@@ -375,7 +388,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache/restore@v4.2.1
uses: actions/cache/restore@v4.2.2
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -482,7 +495,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache@v4.2.1
uses: actions/cache@v4.2.2
with:
path: venv
key: >-
@@ -490,7 +503,7 @@ jobs:
needs.info.outputs.python_cache_key }}
- name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache@v4.2.1
uses: actions/cache@v4.2.2
with:
path: ${{ env.UV_CACHE_DIR }}
key: >-
@@ -578,7 +591,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.1
uses: actions/cache/restore@v4.2.2
with:
path: venv
fail-on-cache-miss: true
@@ -611,7 +624,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.1
uses: actions/cache/restore@v4.2.2
with:
path: venv
fail-on-cache-miss: true
@@ -649,7 +662,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.1
uses: actions/cache/restore@v4.2.2
with:
path: venv
fail-on-cache-miss: true
@@ -692,7 +705,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.1
uses: actions/cache/restore@v4.2.2
with:
path: venv
fail-on-cache-miss: true
@@ -739,7 +752,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.1
uses: actions/cache/restore@v4.2.2
with:
path: venv
fail-on-cache-miss: true
@@ -791,7 +804,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.1
uses: actions/cache/restore@v4.2.2
with:
path: venv
fail-on-cache-miss: true
@@ -799,7 +812,7 @@ jobs:
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Restore mypy cache
uses: actions/cache@v4.2.1
uses: actions/cache@v4.2.2
with:
path: .mypy_cache
key: >-
@@ -830,10 +843,7 @@ jobs:
runs-on: ubuntu-24.04
if: |
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
&& github.event.inputs.lint-only != 'true'
&& github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
&& needs.info.outputs.lint_only != 'true'
&& needs.info.outputs.test_full_suite == 'true'
needs:
- info
@@ -865,7 +875,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.1
uses: actions/cache/restore@v4.2.2
with:
path: venv
fail-on-cache-miss: true
@@ -887,10 +897,7 @@ jobs:
runs-on: ubuntu-24.04
if: |
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
&& github.event.inputs.lint-only != 'true'
&& github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
&& needs.info.outputs.lint_only != 'true'
&& needs.info.outputs.test_full_suite == 'true'
needs:
- info
@@ -929,7 +936,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.1
uses: actions/cache/restore@v4.2.2
with:
path: venv
fail-on-cache-miss: true
@@ -962,6 +969,7 @@ jobs:
if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then
cov_params+=(--cov="homeassistant")
cov_params+=(--cov-report=xml)
cov_params+=(--junitxml=junit.xml -o junit_family=legacy)
fi
echo "Test group ${{ matrix.group }}: $(sed -n "${{ matrix.group }},1p" pytest_buckets.txt)"
@@ -992,6 +1000,12 @@ jobs:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml
overwrite: true
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.1
with:
name: test-results-full-${{ matrix.python-version }}-${{ matrix.group }}
path: junit.xml
- name: Remove pytest_buckets
run: rm pytest_buckets.txt
- name: Check dirty
@@ -1010,10 +1024,7 @@ jobs:
options: --health-cmd="mysqladmin ping -uroot -ppassword" --health-interval=5s --health-timeout=2s --health-retries=3
if: |
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
&& github.event.inputs.lint-only != 'true'
&& github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
&& needs.info.outputs.lint_only != 'true'
&& needs.info.outputs.mariadb_groups != '[]'
needs:
- info
@@ -1051,7 +1062,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.1
uses: actions/cache/restore@v4.2.2
with:
path: venv
fail-on-cache-miss: true
@@ -1088,6 +1099,7 @@ jobs:
cov_params+=(--cov="homeassistant.components.recorder")
cov_params+=(--cov-report=xml)
cov_params+=(--cov-report=term-missing)
cov_params+=(--junitxml=junit.xml -o junit_family=legacy)
fi
python3 -b -X dev -m pytest \
@@ -1122,6 +1134,13 @@ jobs:
steps.pytest-partial.outputs.mariadb }}
path: coverage.xml
overwrite: true
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.1
with:
name: test-results-mariadb-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }}
path: junit.xml
- name: Check dirty
run: |
./script/check_dirty
@@ -1138,10 +1157,7 @@ jobs:
options: --health-cmd="pg_isready -hlocalhost -Upostgres" --health-interval=5s --health-timeout=2s --health-retries=3
if: |
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
&& github.event.inputs.lint-only != 'true'
&& github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
&& needs.info.outputs.lint_only != 'true'
&& needs.info.outputs.postgresql_groups != '[]'
needs:
- info
@@ -1181,7 +1197,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.1
uses: actions/cache/restore@v4.2.2
with:
path: venv
fail-on-cache-miss: true
@@ -1218,6 +1234,7 @@ jobs:
cov_params+=(--cov="homeassistant.components.recorder")
cov_params+=(--cov-report=xml)
cov_params+=(--cov-report=term-missing)
cov_params+=(--junitxml=junit.xml -o junit_family=legacy)
fi
python3 -b -X dev -m pytest \
@@ -1253,6 +1270,13 @@ jobs:
steps.pytest-partial.outputs.postgresql }}
path: coverage.xml
overwrite: true
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.1
with:
name: test-results-postgres-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }}
path: junit.xml
- name: Check dirty
run: |
./script/check_dirty
@@ -1276,7 +1300,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@v5.3.1
uses: codecov/codecov-action@v5.4.0
with:
fail_ci_if_error: true
flags: full-suite
@@ -1286,10 +1310,7 @@ jobs:
runs-on: ubuntu-24.04
if: |
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
&& github.event.inputs.lint-only != 'true'
&& github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
&& needs.info.outputs.lint_only != 'true'
&& needs.info.outputs.tests_glob
&& needs.info.outputs.test_full_suite == 'false'
needs:
@@ -1328,7 +1349,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.1
uses: actions/cache/restore@v4.2.2
with:
path: venv
fail-on-cache-miss: true
@@ -1365,6 +1386,7 @@ jobs:
cov_params+=(--cov="homeassistant.components.${{ matrix.group }}")
cov_params+=(--cov-report=xml)
cov_params+=(--cov-report=term-missing)
cov_params+=(--junitxml=junit.xml -o junit_family=legacy)
fi
python3 -b -X dev -m pytest \
@@ -1394,6 +1416,12 @@ jobs:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml
overwrite: true
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.1
with:
name: test-results-partial-${{ matrix.python-version }}-${{ matrix.group }}
path: junit.xml
- name: Check dirty
run: |
./script/check_dirty
@@ -1415,7 +1443,32 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@v5.3.1
uses: codecov/codecov-action@v5.4.0
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
upload-test-results:
name: Upload test results to Codecov
# codecov/test-results-action currently doesn't support tokenless uploads
# therefore we can't run it on forks
if: github.repository_owner == 'home-assistant' && needs.info.outputs.skip_coverage != 'true' && !cancelled()
runs-on: ubuntu-24.04
needs:
- info
- pytest-partial
- pytest-full
- pytest-postgres
- pytest-mariadb
timeout-minutes: 10
steps:
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.9
with:
pattern: test-results-*
- name: Upload test results to Codecov
uses: codecov/test-results-action@v1
with:
fail_ci_if_error: true
verbose: true
token: ${{ secrets.CODECOV_TOKEN }}
+4 -40
View File
@@ -159,7 +159,7 @@ jobs:
sed -i "/uv/d" requirements_diff.txt
- name: Build wheels
uses: home-assistant/wheels@2024.11.0
uses: home-assistant/wheels@2025.02.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -218,16 +218,8 @@ jobs:
sed -i "/uv/d" requirements.txt
sed -i "/uv/d" requirements_diff.txt
- name: Split requirements all
run: |
# We split requirements all into multiple files.
# This is to prevent the build from running out of memory when
# resolving packages on 32-bits systems (like armhf, armv7).
split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 3) requirements_all_wheels_${{ matrix.arch }}.txt requirements_all.txt
- name: Build wheels (part 1)
uses: home-assistant/wheels@2024.11.0
- name: Build wheels
uses: home-assistant/wheels@2025.02.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -238,32 +230,4 @@ jobs:
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtaa"
- name: Build wheels (part 2)
uses: home-assistant/wheels@2024.11.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-ng-dev"
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtab"
- name: Build wheels (part 3)
uses: home-assistant/wheels@2024.11.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-ng-dev"
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtac"
requirements: "requirements_all.txt"
+1
View File
@@ -69,6 +69,7 @@ test-reports/
test-results.xml
test-output.xml
pytest-*.txt
junit.xml
# Translations
*.mo
+1 -1
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.7
rev: v0.9.8
hooks:
- id: ruff
args:
+3
View File
@@ -136,6 +136,7 @@ homeassistant.components.clicksend.*
homeassistant.components.climate.*
homeassistant.components.cloud.*
homeassistant.components.co2signal.*
homeassistant.components.comelit.*
homeassistant.components.command_line.*
homeassistant.components.config.*
homeassistant.components.configurator.*
@@ -396,6 +397,7 @@ homeassistant.components.pure_energie.*
homeassistant.components.purpleair.*
homeassistant.components.pushbullet.*
homeassistant.components.pvoutput.*
homeassistant.components.pyload.*
homeassistant.components.python_script.*
homeassistant.components.qbus.*
homeassistant.components.qnap_qsw.*
@@ -528,6 +530,7 @@ homeassistant.components.vallox.*
homeassistant.components.valve.*
homeassistant.components.velbus.*
homeassistant.components.vlc_telnet.*
homeassistant.components.vodafone_station.*
homeassistant.components.wake_on_lan.*
homeassistant.components.wake_word.*
homeassistant.components.wallbox.*
Generated
+2 -2
View File
@@ -1529,8 +1529,8 @@ build.json @home-assistant/supervisor
/tests/components/tedee/ @patrickhilker @zweckj
/homeassistant/components/tellduslive/ @fredrike
/tests/components/tellduslive/ @fredrike
/homeassistant/components/template/ @PhracturedBlue @home-assistant/core
/tests/components/template/ @PhracturedBlue @home-assistant/core
/homeassistant/components/template/ @Petro31 @PhracturedBlue @home-assistant/core
/tests/components/template/ @Petro31 @PhracturedBlue @home-assistant/core
/homeassistant/components/tesla_fleet/ @Bre77
/tests/components/tesla_fleet/ @Bre77
/homeassistant/components/tesla_wall_connector/ @einarhauks
+4 -5
View File
@@ -664,11 +664,10 @@ def _create_log_file(
err_handler = _RotatingFileHandlerWithoutShouldRollOver(
err_log_path, backupCount=1
)
try:
err_handler.doRollover()
except OSError as err:
_LOGGER.error("Error rolling over log file: %s", err)
try:
err_handler.doRollover()
except OSError as err:
_LOGGER.error("Error rolling over log file: %s", err)
return err_handler
@@ -2,6 +2,7 @@
from __future__ import annotations
from decimal import Decimal
import logging
from typing import Any
@@ -14,6 +15,7 @@ from homeassistant.components.climate import (
FAN_MEDIUM,
ClimateEntity,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
@@ -49,6 +51,14 @@ ADVANTAGE_AIR_MYTEMP_ENABLED = "climateControlModeEnabled"
ADVANTAGE_AIR_HEAT_TARGET = "myAutoHeatTargetTemp"
ADVANTAGE_AIR_COOL_TARGET = "myAutoCoolTargetTemp"
ADVANTAGE_AIR_MYFAN = "autoAA"
ADVANTAGE_AIR_MYAUTO_MODE_SET = "myAutoModeCurrentSetMode"
HVAC_ACTIONS = {
"cool": HVACAction.COOLING,
"heat": HVACAction.HEATING,
"vent": HVACAction.FAN,
"dry": HVACAction.DRYING,
}
HVAC_MODES = [
HVACMode.OFF,
@@ -175,6 +185,17 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
return ADVANTAGE_AIR_HVAC_MODES.get(self._ac["mode"])
return HVACMode.OFF
@property
def hvac_action(self) -> HVACAction | None:
"""Return the current running HVAC action."""
if self._ac["state"] == ADVANTAGE_AIR_STATE_OFF:
return HVACAction.OFF
if self._ac["mode"] == "myauto":
return HVAC_ACTIONS.get(
self._ac.get(ADVANTAGE_AIR_MYAUTO_MODE_SET, HVACAction.OFF)
)
return HVAC_ACTIONS.get(self._ac["mode"])
@property
def fan_mode(self) -> str | None:
"""Return the current fan modes."""
@@ -273,6 +294,22 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity):
return HVACMode.HEAT_COOL
return HVACMode.OFF
@property
def hvac_action(self) -> HVACAction | None:
"""Return the HVAC action, inheriting from master AC if zone is open but idle if air is <= 5%."""
if self._ac["state"] == ADVANTAGE_AIR_STATE_OFF:
return HVACAction.OFF
master_action = HVAC_ACTIONS.get(self._ac["mode"], HVACAction.OFF)
if self._ac["mode"] == "myauto":
master_action = HVAC_ACTIONS.get(
str(self._ac.get(ADVANTAGE_AIR_MYAUTO_MODE_SET)), HVACAction.OFF
)
if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN:
if self._zone["value"] <= Decimal(5):
return HVACAction.IDLE
return master_action
return HVACAction.OFF
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
@@ -7,3 +7,4 @@ ADVANTAGE_AIR_STATE_CLOSE = "close"
ADVANTAGE_AIR_STATE_ON = "on"
ADVANTAGE_AIR_STATE_OFF = "off"
ADVANTAGE_AIR_AUTOFAN_ENABLED = "aaAutoFanModeEnabled"
ADVANTAGE_AIR_NIGHT_MODE_ENABLED = "quietNightModeEnabled"
@@ -41,7 +41,7 @@ async def async_setup_entry(
entities.append(
AdvantageAirThingCover(instance, thing, CoverDeviceClass.BLIND)
)
elif thing["channelDipState"] == 3: # 3 = "Garage door"
elif thing["channelDipState"] in [3, 10]: # 3 & 10 = "Garage door"
entities.append(
AdvantageAirThingCover(instance, thing, CoverDeviceClass.GARAGE)
)
@@ -9,6 +9,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AdvantageAirDataConfigEntry
from .const import (
ADVANTAGE_AIR_AUTOFAN_ENABLED,
ADVANTAGE_AIR_NIGHT_MODE_ENABLED,
ADVANTAGE_AIR_STATE_OFF,
ADVANTAGE_AIR_STATE_ON,
)
@@ -32,6 +33,8 @@ async def async_setup_entry(
entities.append(AdvantageAirFreshAir(instance, ac_key))
if ADVANTAGE_AIR_AUTOFAN_ENABLED in ac_device["info"]:
entities.append(AdvantageAirMyFan(instance, ac_key))
if ADVANTAGE_AIR_NIGHT_MODE_ENABLED in ac_device["info"]:
entities.append(AdvantageAirNightMode(instance, ac_key))
if things := instance.coordinator.data.get("myThings"):
entities.extend(
AdvantageAirRelay(instance, thing)
@@ -93,6 +96,32 @@ class AdvantageAirMyFan(AdvantageAirAcEntity, SwitchEntity):
await self.async_update_ac({ADVANTAGE_AIR_AUTOFAN_ENABLED: False})
class AdvantageAirNightMode(AdvantageAirAcEntity, SwitchEntity):
"""Representation of Advantage 'MySleep$aver' Mode control."""
_attr_icon = "mdi:weather-night"
_attr_name = "MySleep$aver"
_attr_device_class = SwitchDeviceClass.SWITCH
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
"""Initialize an Advantage Air Night Mode control."""
super().__init__(instance, ac_key)
self._attr_unique_id += "-nightmode"
@property
def is_on(self) -> bool:
"""Return the Night Mode status."""
return self._ac[ADVANTAGE_AIR_NIGHT_MODE_ENABLED]
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn Night Mode on."""
await self.async_update_ac({ADVANTAGE_AIR_NIGHT_MODE_ENABLED: True})
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn Night Mode off."""
await self.async_update_ac({ADVANTAGE_AIR_NIGHT_MODE_ENABLED: False})
class AdvantageAirRelay(AdvantageAirThingEntity, SwitchEntity):
"""Representation of Advantage Air Thing."""
+3 -2
View File
@@ -14,7 +14,7 @@ from homeassistant.components.notify import (
)
from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_ON
from homeassistant.core import Event, EventStateChangedData, HassJob, HomeAssistant
from homeassistant.exceptions import ServiceNotFound
from homeassistant.exceptions import ServiceNotFound, ServiceValidationError
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import (
async_track_point_in_time,
@@ -195,7 +195,8 @@ class AlertEntity(Entity):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Async Acknowledge alert."""
LOGGER.debug("Acknowledged Alert: %s", self._attr_name)
if not self._can_ack:
raise ServiceValidationError("This alert cannot be acknowledged")
self._ack = True
self.async_write_ha_state()
@@ -8,7 +8,7 @@ from python_homeassistant_analytics import (
HomeassistantAnalyticsClient,
HomeassistantAnalyticsConnectionError,
)
from python_homeassistant_analytics.models import IntegrationType
from python_homeassistant_analytics.models import Environment, IntegrationType
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
@@ -81,7 +81,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
)
try:
addons = await client.get_addons()
integrations = await client.get_integrations()
integrations = await client.get_integrations(Environment.NEXT)
custom_integrations = await client.get_custom_integrations()
except HomeassistantAnalyticsConnectionError:
LOGGER.exception("Error connecting to Home Assistant analytics")
@@ -165,7 +165,7 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlow):
)
try:
addons = await client.get_addons()
integrations = await client.get_integrations()
integrations = await client.get_integrations(Environment.NEXT)
custom_integrations = await client.get_custom_integrations()
except HomeassistantAnalyticsConnectionError:
LOGGER.exception("Error connecting to Home Assistant analytics")
@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from .const import DOMAIN, LOGGER
from .const import CONF_CHAT_MODEL, DOMAIN, LOGGER, RECOMMENDED_CHAT_MODEL
PLATFORMS = (Platform.CONVERSATION,)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -26,12 +26,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
partial(anthropic.AsyncAnthropic, api_key=entry.data[CONF_API_KEY])
)
try:
await client.messages.create(
model="claude-3-haiku-20240307",
max_tokens=1,
messages=[{"role": "user", "content": "Hi"}],
timeout=10.0,
)
model_id = entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
model = await client.models.retrieve(model_id=model_id, timeout=10.0)
LOGGER.debug("Anthropic model: %s", model.display_name)
except anthropic.AuthenticationError as err:
LOGGER.error("Invalid API key: %s", err)
return False
@@ -63,12 +63,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
client = await hass.async_add_executor_job(
partial(anthropic.AsyncAnthropic, api_key=data[CONF_API_KEY])
)
await client.messages.create(
model="claude-3-haiku-20240307",
max_tokens=1,
messages=[{"role": "user", "content": "Hi"}],
timeout=10.0,
)
await client.models.list(timeout=10.0)
class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
@@ -0,0 +1 @@
"""Virtual integration: Apollo Automation."""
@@ -0,0 +1,6 @@
{
"domain": "apollo_automation",
"name": "Apollo Automation",
"integration_type": "virtual",
"supported_by": "esphome"
}
@@ -117,7 +117,7 @@ async def async_pipeline_from_audio_stream(
"""
with chat_session.async_get_chat_session(hass, conversation_id) as session:
pipeline_input = PipelineInput(
conversation_id=session.conversation_id,
session=session,
device_id=device_id,
stt_metadata=stt_metadata,
stt_stream=stt_stream,
@@ -19,14 +19,7 @@ import wave
import hass_nabucasa
import voluptuous as vol
from homeassistant.components import (
conversation,
media_source,
stt,
tts,
wake_word,
websocket_api,
)
from homeassistant.components import conversation, stt, tts, wake_word, websocket_api
from homeassistant.components.tts import (
generate_media_source_id as tts_generate_media_source_id,
)
@@ -96,6 +89,9 @@ ENGINE_LANGUAGE_PAIRS = (
)
KEY_ASSIST_PIPELINE: HassKey[PipelineData] = HassKey(DOMAIN)
KEY_PIPELINE_CONVERSATION_DATA: HassKey[dict[str, PipelineConversationData]] = HassKey(
"pipeline_conversation_data"
)
def validate_language(data: dict[str, Any]) -> Any:
@@ -566,8 +562,7 @@ class PipelineRun:
id: str = field(default_factory=ulid_util.ulid_now)
stt_provider: stt.SpeechToTextEntity | stt.Provider = field(init=False, repr=False)
tts_engine: str = field(init=False, repr=False)
tts_options: dict | None = field(init=False, default=None)
tts_stream: tts.ResultStream | None = field(init=False, default=None)
wake_word_entity_id: str | None = field(init=False, default=None, repr=False)
wake_word_entity: wake_word.WakeWordDetectionEntity = field(init=False, repr=False)
@@ -590,6 +585,12 @@ class PipelineRun:
_device_id: str | None = None
"""Optional device id set during run start."""
_conversation_data: PipelineConversationData | None = None
"""Data tied to the conversation ID."""
_intent_agent_only = False
"""If request should only be handled by agent, ignoring sentence triggers and local processing."""
def __post_init__(self) -> None:
"""Set language for pipeline."""
self.language = self.pipeline.language or self.hass.config.language
@@ -639,13 +640,18 @@ class PipelineRun:
self._device_id = device_id
self._start_debug_recording_thread()
data = {
data: dict[str, Any] = {
"pipeline": self.pipeline.id,
"language": self.language,
"conversation_id": conversation_id,
}
if self.runner_data is not None:
data["runner_data"] = self.runner_data
if self.tts_stream:
data["tts_output"] = {
"url": self.tts_stream.url,
"mime_type": self.tts_stream.content_type,
}
self.process_event(PipelineEvent(PipelineEventType.RUN_START, data))
@@ -1007,19 +1013,36 @@ class PipelineRun:
yield chunk.audio
async def prepare_recognize_intent(self) -> None:
async def prepare_recognize_intent(self, session: chat_session.ChatSession) -> None:
"""Prepare recognizing an intent."""
agent_info = conversation.async_get_agent_info(
self.hass,
self.pipeline.conversation_engine or conversation.HOME_ASSISTANT_AGENT,
self._conversation_data = async_get_pipeline_conversation_data(
self.hass, session
)
if agent_info is None:
engine = self.pipeline.conversation_engine or "default"
raise IntentRecognitionError(
code="intent-not-supported",
message=f"Intent recognition engine {engine} is not found",
if self._conversation_data.continue_conversation_agent is not None:
agent_info = conversation.async_get_agent_info(
self.hass, self._conversation_data.continue_conversation_agent
)
self._conversation_data.continue_conversation_agent = None
if agent_info is None:
raise IntentRecognitionError(
code="intent-agent-not-found",
message=f"Intent recognition engine {self._conversation_data.continue_conversation_agent} asked for follow-up but is no longer found",
)
self._intent_agent_only = True
else:
agent_info = conversation.async_get_agent_info(
self.hass,
self.pipeline.conversation_engine or conversation.HOME_ASSISTANT_AGENT,
)
if agent_info is None:
engine = self.pipeline.conversation_engine or "default"
raise IntentRecognitionError(
code="intent-not-supported",
message=f"Intent recognition engine {engine} is not found",
)
self.intent_agent = agent_info.id
@@ -1031,7 +1054,7 @@ class PipelineRun:
conversation_extra_system_prompt: str | None,
) -> str:
"""Run intent recognition portion of pipeline. Returns text to speak."""
if self.intent_agent is None:
if self.intent_agent is None or self._conversation_data is None:
raise RuntimeError("Recognize intent was not prepared")
if self.pipeline.conversation_language == MATCH_ALL:
@@ -1078,7 +1101,7 @@ class PipelineRun:
agent_id = self.intent_agent
processed_locally = agent_id == conversation.HOME_ASSISTANT_AGENT
intent_response: intent.IntentResponse | None = None
if not processed_locally:
if not processed_locally and not self._intent_agent_only:
# Sentence triggers override conversation agent
if (
trigger_response_text
@@ -1103,12 +1126,16 @@ class PipelineRun:
) & conversation.ConversationEntityFeature.CONTROL:
intent_filter = _async_local_fallback_intent_filter
# Try local intents first, if preferred.
elif self.pipeline.prefer_local_intents and (
intent_response := await conversation.async_handle_intents(
self.hass,
user_input,
intent_filter=intent_filter,
# Try local intents
if (
intent_response is None
and self.pipeline.prefer_local_intents
and (
intent_response := await conversation.async_handle_intents(
self.hass,
user_input,
intent_filter=intent_filter,
)
)
):
# Local intent matched
@@ -1191,6 +1218,9 @@ class PipelineRun:
)
)
if conversation_result.continue_conversation:
self._conversation_data.continue_conversation_agent = agent_id
return speech
async def prepare_text_to_speech(self) -> None:
@@ -1213,36 +1243,31 @@ class PipelineRun:
tts_options[tts.ATTR_PREFERRED_SAMPLE_BYTES] = SAMPLE_WIDTH
try:
options_supported = await tts.async_support_options(
self.hass,
engine,
self.pipeline.tts_language,
tts_options,
self.tts_stream = tts.async_create_stream(
hass=self.hass,
engine=engine,
language=self.pipeline.tts_language,
options=tts_options,
)
except HomeAssistantError as err:
raise TextToSpeechError(
code="tts-not-supported",
message=f"Text-to-speech engine '{engine}' not found",
) from err
if not options_supported:
raise TextToSpeechError(
code="tts-not-supported",
message=(
f"Text-to-speech engine {engine} "
f"does not support language {self.pipeline.tts_language} or options {tts_options}"
f"does not support language {self.pipeline.tts_language} or options {tts_options}:"
f" {err}"
),
)
self.tts_engine = engine
self.tts_options = tts_options
) from err
async def text_to_speech(self, tts_input: str) -> None:
"""Run text-to-speech portion of pipeline."""
assert self.tts_stream is not None
self.process_event(
PipelineEvent(
PipelineEventType.TTS_START,
{
"engine": self.tts_engine,
"engine": self.tts_stream.engine,
"language": self.pipeline.tts_language,
"voice": self.pipeline.tts_voice,
"tts_input": tts_input,
@@ -1255,14 +1280,9 @@ class PipelineRun:
tts_media_id = tts_generate_media_source_id(
self.hass,
tts_input,
engine=self.tts_engine,
language=self.pipeline.tts_language,
options=self.tts_options,
)
tts_media = await media_source.async_resolve_media(
self.hass,
tts_media_id,
None,
engine=self.tts_stream.engine,
language=self.tts_stream.language,
options=self.tts_stream.options,
)
except Exception as src_error:
_LOGGER.exception("Unexpected error during text-to-speech")
@@ -1271,10 +1291,12 @@ class PipelineRun:
message="Unexpected error during text-to-speech",
) from src_error
_LOGGER.debug("TTS result %s", tts_media)
self.tts_stream.async_set_message(tts_input)
tts_output = {
"media_id": tts_media_id,
**asdict(tts_media),
"url": self.tts_stream.url,
"mime_type": self.tts_stream.content_type,
}
self.process_event(
@@ -1454,8 +1476,8 @@ class PipelineInput:
run: PipelineRun
conversation_id: str
"""Identifier for the conversation."""
session: chat_session.ChatSession
"""Session for the conversation."""
stt_metadata: stt.SpeechMetadata | None = None
"""Metadata of stt input audio. Required when start_stage = stt."""
@@ -1480,7 +1502,9 @@ class PipelineInput:
async def execute(self) -> None:
"""Run pipeline."""
self.run.start(conversation_id=self.conversation_id, device_id=self.device_id)
self.run.start(
conversation_id=self.session.conversation_id, device_id=self.device_id
)
current_stage: PipelineStage | None = self.run.start_stage
stt_audio_buffer: list[EnhancedAudioChunk] = []
stt_processed_stream: AsyncIterable[EnhancedAudioChunk] | None = None
@@ -1564,7 +1588,7 @@ class PipelineInput:
assert intent_input is not None
tts_input = await self.run.recognize_intent(
intent_input,
self.conversation_id,
self.session.conversation_id,
self.device_id,
self.conversation_extra_system_prompt,
)
@@ -1648,7 +1672,7 @@ class PipelineInput:
<= PIPELINE_STAGE_ORDER.index(PipelineStage.INTENT)
<= end_stage_index
):
prepare_tasks.append(self.run.prepare_recognize_intent())
prepare_tasks.append(self.run.prepare_recognize_intent(self.session))
if (
start_stage_index
@@ -1927,7 +1951,7 @@ class PipelineRunDebug:
class PipelineStore(Store[SerializedPipelineStorageCollection]):
"""Store entity registry data."""
"""Store pipeline data."""
async def _async_migrate_func(
self,
@@ -2009,3 +2033,37 @@ async def async_run_migrations(hass: HomeAssistant) -> None:
for pipeline, attr_updates in updates:
await async_update_pipeline(hass, pipeline, **attr_updates)
@dataclass
class PipelineConversationData:
"""Hold data for the duration of a conversation."""
continue_conversation_agent: str | None = None
"""The agent that requested the conversation to be continued."""
@callback
def async_get_pipeline_conversation_data(
hass: HomeAssistant, session: chat_session.ChatSession
) -> PipelineConversationData:
"""Get the pipeline data for a specific conversation."""
all_conversation_data = hass.data.get(KEY_PIPELINE_CONVERSATION_DATA)
if all_conversation_data is None:
all_conversation_data = {}
hass.data[KEY_PIPELINE_CONVERSATION_DATA] = all_conversation_data
data = all_conversation_data.get(session.conversation_id)
if data is not None:
return data
@callback
def do_cleanup() -> None:
"""Handle cleanup."""
all_conversation_data.pop(session.conversation_id)
session.async_on_cleanup(do_cleanup)
data = all_conversation_data[session.conversation_id] = PipelineConversationData()
return data
@@ -239,7 +239,7 @@ async def websocket_run(
with chat_session.async_get_chat_session(
hass, msg.get("conversation_id")
) as session:
input_args["conversation_id"] = session.conversation_id
input_args["session"] = session
pipeline_input = PipelineInput(**input_args)
try:
@@ -13,7 +13,11 @@ from azure.storage.blob.aio import ContainerClient
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryError,
ConfigEntryNotReady,
)
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import (
@@ -52,7 +56,7 @@ async def async_setup_entry(
translation_placeholders={CONF_ACCOUNT_NAME: entry.data[CONF_ACCOUNT_NAME]},
) from err
except ClientAuthenticationError as err:
raise ConfigEntryError(
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
translation_placeholders={CONF_ACCOUNT_NAME: entry.data[CONF_ACCOUNT_NAME]},
@@ -141,7 +141,7 @@ class AzureStorageBackupAgent(BackupAgent):
"""Delete a backup file."""
blob = await self._find_blob_by_backup_id(backup_id)
if blob is None:
return
raise BackupNotFound(f"Backup {backup_id} not found")
await self._client.delete_blob(blob.name)
@handle_backup_errors
@@ -163,11 +163,11 @@ class AzureStorageBackupAgent(BackupAgent):
self,
backup_id: str,
**kwargs: Any,
) -> AgentBackup | None:
) -> AgentBackup:
"""Return a backup."""
blob = await self._find_blob_by_backup_id(backup_id)
if blob is None:
return None
raise BackupNotFound(f"Backup {backup_id} not found")
return AgentBackup.from_dict(json.loads(blob.metadata["backup_metadata"]))
@@ -1,5 +1,6 @@
"""Config flow for Azure Storage integration."""
from collections.abc import Mapping
import logging
from typing import Any
@@ -26,6 +27,26 @@ _LOGGER = logging.getLogger(__name__)
class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for azure storage."""
def get_account_url(self, account_name: str) -> str:
"""Get the account URL."""
return f"https://{account_name}.blob.core.windows.net/"
async def validate_config(
self, container_client: ContainerClient
) -> dict[str, str]:
"""Validate the configuration."""
errors: dict[str, str] = {}
try:
await container_client.exists()
except ResourceNotFoundError:
errors["base"] = "cannot_connect"
except ClientAuthenticationError:
errors[CONF_STORAGE_ACCOUNT_KEY] = "invalid_auth"
except Exception:
_LOGGER.exception("Unknown exception occurred")
errors["base"] = "unknown"
return errors
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -38,20 +59,13 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
{CONF_ACCOUNT_NAME: user_input[CONF_ACCOUNT_NAME]}
)
container_client = ContainerClient(
account_url=f"https://{user_input[CONF_ACCOUNT_NAME]}.blob.core.windows.net/",
account_url=self.get_account_url(user_input[CONF_ACCOUNT_NAME]),
container_name=user_input[CONF_CONTAINER_NAME],
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
)
try:
await container_client.exists()
except ResourceNotFoundError:
errors["base"] = "cannot_connect"
except ClientAuthenticationError:
errors[CONF_STORAGE_ACCOUNT_KEY] = "invalid_auth"
except Exception:
_LOGGER.exception("Unknown exception occurred")
errors["base"] = "unknown"
errors = await self.validate_config(container_client)
if not errors:
return self.async_create_entry(
title=f"{user_input[CONF_ACCOUNT_NAME]}/{user_input[CONF_CONTAINER_NAME]}",
@@ -70,3 +84,77 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
),
errors=errors,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauth dialog."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
if user_input is not None:
container_client = ContainerClient(
account_url=self.get_account_url(reauth_entry.data[CONF_ACCOUNT_NAME]),
container_name=reauth_entry.data[CONF_CONTAINER_NAME],
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
)
errors = await self.validate_config(container_client)
if not errors:
return self.async_update_reload_and_abort(
reauth_entry,
data={**reauth_entry.data, **user_input},
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_STORAGE_ACCOUNT_KEY): str,
}
),
errors=errors,
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Reconfigure the entry."""
errors: dict[str, str] = {}
reconfigure_entry = self._get_reconfigure_entry()
if user_input is not None:
container_client = ContainerClient(
account_url=self.get_account_url(
reconfigure_entry.data[CONF_ACCOUNT_NAME]
),
container_name=user_input[CONF_CONTAINER_NAME],
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
)
errors = await self.validate_config(container_client)
if not errors:
return self.async_update_reload_and_abort(
reconfigure_entry,
data={**reconfigure_entry.data, **user_input},
)
return self.async_show_form(
data_schema=vol.Schema(
{
vol.Required(
CONF_CONTAINER_NAME,
default=reconfigure_entry.data[CONF_CONTAINER_NAME],
): str,
vol.Required(
CONF_STORAGE_ACCOUNT_KEY,
default=reconfigure_entry.data[CONF_STORAGE_ACCOUNT_KEY],
): str,
}
),
errors=errors,
)
@@ -7,6 +7,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["azure-storage-blob"],
"quality_scale": "bronze",
"quality_scale": "platinum",
"requirements": ["azure-storage-blob==12.24.0"]
}
@@ -57,7 +57,7 @@ rules:
status: exempt
comment: |
This integration does not have platforms.
reauthentication-flow: todo
reauthentication-flow: done
test-coverage: done
# Gold
@@ -121,7 +121,7 @@ rules:
status: exempt
comment: |
This integration does not have entities.
reconfiguration-flow: todo
reconfiguration-flow: done
repair-issues: done
stale-devices:
status: exempt
@@ -19,10 +19,34 @@
},
"description": "Set up an Azure (Blob) storage account to be used for backups.",
"title": "Add Azure storage account"
},
"reauth_confirm": {
"data": {
"storage_account_key": "[%key:component::azure_storage::config::step::user::data::storage_account_key%]"
},
"data_description": {
"storage_account_key": "[%key:component::azure_storage::config::step::user::data_description::storage_account_key%]"
},
"description": "Provide a new storage account key.",
"title": "Reauthenticate Azure storage account"
},
"reconfigure": {
"data": {
"container_name": "[%key:component::azure_storage::config::step::user::data::container_name%]",
"storage_account_key": "[%key:component::azure_storage::config::step::user::data::storage_account_key%]"
},
"data_description": {
"container_name": "[%key:component::azure_storage::config::step::user::data_description::container_name%]",
"storage_account_key": "[%key:component::azure_storage::config::step::user::data_description::storage_account_key%]"
},
"description": "Change the settings of the Azure storage integration.",
"title": "Reconfigure Azure storage account"
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
}
},
"issues": {
+8 -1
View File
@@ -41,6 +41,8 @@ class BackupAgent(abc.ABC):
) -> AsyncIterator[bytes]:
"""Download a backup file.
Raises BackupNotFound if the backup does not exist.
:param backup_id: The ID of the backup that was returned in async_list_backups.
:return: An async iterator that yields bytes.
"""
@@ -67,6 +69,8 @@ class BackupAgent(abc.ABC):
) -> None:
"""Delete a backup file.
Raises BackupNotFound if the backup does not exist.
:param backup_id: The ID of the backup that was returned in async_list_backups.
"""
@@ -80,7 +84,10 @@ class BackupAgent(abc.ABC):
backup_id: str,
**kwargs: Any,
) -> AgentBackup | None:
"""Return a backup."""
"""Return a backup.
Raises BackupNotFound if the backup does not exist.
"""
class LocalBackupAgent(BackupAgent):
+4 -7
View File
@@ -88,13 +88,13 @@ class CoreLocalBackupAgent(LocalBackupAgent):
self,
backup_id: str,
**kwargs: Any,
) -> AgentBackup | None:
) -> AgentBackup:
"""Return a backup."""
if not self._loaded_backups:
await self._load_backups()
if backup_id not in self._backups:
return None
raise BackupNotFound(f"Backup {backup_id} not found")
backup, backup_path = self._backups[backup_id]
if not await self._hass.async_add_executor_job(backup_path.exists):
@@ -107,7 +107,7 @@ class CoreLocalBackupAgent(LocalBackupAgent):
backup_path,
)
self._backups.pop(backup_id)
return None
raise BackupNotFound(f"Backup {backup_id} not found")
return backup
@@ -130,10 +130,7 @@ class CoreLocalBackupAgent(LocalBackupAgent):
if not self._loaded_backups:
await self._load_backups()
try:
backup_path = self.get_backup_path(backup_id)
except BackupNotFound:
return
backup_path = self.get_backup_path(backup_id)
await self._hass.async_add_executor_job(backup_path.unlink, True)
LOGGER.debug("Deleted backup located at %s", backup_path)
self._backups.pop(backup_id)
+8 -3
View File
@@ -59,10 +59,13 @@ class DownloadBackupView(HomeAssistantView):
if agent_id not in manager.backup_agents:
return Response(status=HTTPStatus.BAD_REQUEST)
agent = manager.backup_agents[agent_id]
backup = await agent.async_get_backup(backup_id)
try:
backup = await agent.async_get_backup(backup_id)
except BackupNotFound:
return Response(status=HTTPStatus.NOT_FOUND)
# We don't need to check if the path exists, aiohttp.FileResponse will handle
# that
# Check for None to be backwards compatible with the old BackupAgent API,
# this can be removed in HA Core 2025.10
if backup is None:
return Response(status=HTTPStatus.NOT_FOUND)
@@ -92,6 +95,8 @@ class DownloadBackupView(HomeAssistantView):
) -> StreamResponse | FileResponse | Response:
if agent_id in manager.local_backup_agents:
local_agent = manager.local_backup_agents[agent_id]
# We don't need to check if the path exists, aiohttp.FileResponse will
# handle that
path = local_agent.get_backup_path(backup_id)
return FileResponse(path=path.as_posix(), headers=headers)
+50 -6
View File
@@ -14,6 +14,7 @@ from itertools import chain
import json
from pathlib import Path, PurePath
import shutil
import sys
import tarfile
import time
from typing import IO, TYPE_CHECKING, Any, Protocol, TypedDict, cast
@@ -63,6 +64,7 @@ from .models import (
AgentBackup,
BackupError,
BackupManagerError,
BackupNotFound,
BackupReaderWriterError,
BaseBackup,
Folder,
@@ -308,6 +310,12 @@ class DecryptOnDowloadNotSupported(BackupManagerError):
_message = "On-the-fly decryption is not supported for this backup."
class BackupManagerExceptionGroup(BackupManagerError, ExceptionGroup):
"""Raised when multiple exceptions occur."""
error_code = "multiple_errors"
class BackupManager:
"""Define the format that backup managers can have."""
@@ -641,6 +649,8 @@ class BackupManager:
)
for idx, result in enumerate(get_backup_results):
agent_id = agent_ids[idx]
if isinstance(result, BackupNotFound):
continue
if isinstance(result, BackupAgentError):
agent_errors[agent_id] = result
continue
@@ -652,6 +662,8 @@ class BackupManager:
continue
if isinstance(result, BaseException):
raise result # unexpected error
# Check for None to be backwards compatible with the old BackupAgent API,
# this can be removed in HA Core 2025.10
if not result:
continue
if backup is None:
@@ -716,6 +728,8 @@ class BackupManager:
)
for idx, result in enumerate(delete_backup_results):
agent_id = agent_ids[idx]
if isinstance(result, BackupNotFound):
continue
if isinstance(result, BackupAgentError):
agent_errors[agent_id] = result
continue
@@ -825,7 +839,7 @@ class BackupManager:
agent_errors = {
backup_id: error
for backup_id, error in zip(backup_ids, delete_results, strict=True)
if error
if error and not isinstance(error, BackupNotFound)
}
if agent_errors:
LOGGER.error(
@@ -1257,7 +1271,15 @@ class BackupManager:
) -> None:
"""Initiate restoring a backup."""
agent = self.backup_agents[agent_id]
if not await agent.async_get_backup(backup_id):
try:
backup = await agent.async_get_backup(backup_id)
except BackupNotFound as err:
raise BackupManagerError(
f"Backup {backup_id} not found in agent {agent_id}"
) from err
# Check for None to be backwards compatible with the old BackupAgent API,
# this can be removed in HA Core 2025.10
if not backup:
raise BackupManagerError(
f"Backup {backup_id} not found in agent {agent_id}"
)
@@ -1345,7 +1367,15 @@ class BackupManager:
agent = self.backup_agents[agent_id]
except KeyError as err:
raise BackupManagerError(f"Invalid agent selected: {agent_id}") from err
if not await agent.async_get_backup(backup_id):
try:
backup = await agent.async_get_backup(backup_id)
except BackupNotFound as err:
raise BackupManagerError(
f"Backup {backup_id} not found in agent {agent_id}"
) from err
# Check for None to be backwards compatible with the old BackupAgent API,
# this can be removed in HA Core 2025.10
if not backup:
raise BackupManagerError(
f"Backup {backup_id} not found in agent {agent_id}"
)
@@ -1605,10 +1635,24 @@ class CoreBackupReaderWriter(BackupReaderWriter):
)
finally:
# Inform integrations the backup is done
# If there's an unhandled exception, we keep it so we can rethrow it in case
# the post backup actions also fail.
unhandled_exc = sys.exception()
try:
await manager.async_post_backup_actions()
except BackupManagerError as err:
raise BackupReaderWriterError(str(err)) from err
try:
await manager.async_post_backup_actions()
except BackupManagerError as err:
raise BackupReaderWriterError(str(err)) from err
except Exception as err:
if not unhandled_exc:
raise
# If there's an unhandled exception, we wrap both that and the exception
# from the post backup actions in an ExceptionGroup so the caller is
# aware of both exceptions.
raise BackupManagerExceptionGroup(
f"Multiple errors when creating backup: {unhandled_exc}, {err}",
[unhandled_exc, err],
) from None
def _mkdir_and_generate_backup_contents(
self,
+1 -1
View File
@@ -21,6 +21,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.CLIMATE,
Platform.EVENT,
Platform.FAN,
Platform.LIGHT,
Platform.SELECT,
@@ -28,7 +29,6 @@ PLATFORMS = [
Platform.TIME,
]
KEEP_ALIVE_INTERVAL = timedelta(minutes=1)
SYNC_TIME_INTERVAL = timedelta(hours=1)
+91
View File
@@ -0,0 +1,91 @@
"""Support for Balboa events."""
from __future__ import annotations
from datetime import datetime, timedelta
from pybalboa import EVENT_UPDATE, SpaClient
from homeassistant.components.event import EventEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from . import BalboaConfigEntry
from .entity import BalboaEntity
FAULT = "fault"
FAULT_DATE = "fault_date"
REQUEST_FAULT_LOG_INTERVAL = timedelta(minutes=5)
FAULT_MESSAGE_CODE_MAP: dict[int, str] = {
15: "sensor_out_of_sync",
16: "low_flow",
17: "flow_failed",
18: "settings_reset",
19: "priming_mode",
20: "clock_failed",
21: "settings_reset",
22: "memory_failure",
26: "service_sensor_sync",
27: "heater_dry",
28: "heater_may_be_dry",
29: "water_too_hot",
30: "heater_too_hot",
31: "sensor_a_fault",
32: "sensor_b_fault",
34: "pump_stuck",
35: "hot_fault",
36: "gfci_test_failed",
37: "standby_mode",
}
FAULT_EVENT_TYPES = sorted(set(FAULT_MESSAGE_CODE_MAP.values()))
async def async_setup_entry(
hass: HomeAssistant,
entry: BalboaConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the spa's events."""
async_add_entities([BalboaEventEntity(entry.runtime_data)])
class BalboaEventEntity(BalboaEntity, EventEntity):
"""Representation of a Balboa event entity."""
_attr_event_types = FAULT_EVENT_TYPES
_attr_translation_key = FAULT
def __init__(self, spa: SpaClient) -> None:
"""Initialize a Balboa event entity."""
super().__init__(spa, FAULT)
@callback
def _async_handle_event(self) -> None:
"""Handle the fault event."""
if not (fault := self._client.fault):
return
fault_date = fault.fault_datetime.isoformat()
if self.state_attributes.get(FAULT_DATE) != fault_date:
self._trigger_event(
FAULT_MESSAGE_CODE_MAP.get(fault.message_code, fault.message),
{FAULT_DATE: fault_date, "code": fault.message_code},
)
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
await super().async_added_to_hass()
self.async_on_remove(self._client.on(EVENT_UPDATE, self._async_handle_event))
async def request_fault_log(now: datetime | None = None) -> None:
"""Request the most recent fault log."""
await self._client.request_fault_log()
await request_fault_log()
self.async_on_remove(
async_track_time_interval(
self.hass, request_fault_log, REQUEST_FAULT_LOG_INTERVAL
)
)
@@ -57,6 +57,35 @@
}
}
},
"event": {
"fault": {
"name": "Fault",
"state_attributes": {
"event_type": {
"state": {
"sensor_out_of_sync": "Sensors are out of sync",
"low_flow": "The water flow is low",
"flow_failed": "The water flow has failed",
"settings_reset": "The settings have been reset",
"priming_mode": "Priming mode",
"clock_failed": "The clock has failed",
"memory_failure": "Program memory failure",
"service_sensor_sync": "Sensors are out of sync -- call for service",
"heater_dry": "The heater is dry",
"heater_may_be_dry": "The heater may be dry",
"water_too_hot": "The water is too hot",
"heater_too_hot": "The heater is too hot",
"sensor_a_fault": "Sensor A fault",
"sensor_b_fault": "Sensor B fault",
"pump_stuck": "A pump may be stuck on",
"hot_fault": "Hot fault",
"gfci_test_failed": "The GFCI test failed",
"standby_mode": "Standby mode (hold mode)"
}
}
}
}
},
"fan": {
"pump": {
"name": "Pump {index}"
+16 -3
View File
@@ -311,11 +311,24 @@ async def async_update_device(
update the device with the new location so they can
figure out where the adapter is.
"""
address = details[ADAPTER_ADDRESS]
connections = {(dr.CONNECTION_BLUETOOTH, address)}
device_registry = dr.async_get(hass)
# We only have one device for the config entry
# so if the address has been corrected, make
# sure the device entry reflects the correct
# address
for device in dr.async_entries_for_config_entry(device_registry, entry.entry_id):
for conn_type, conn_value in device.connections:
if conn_type == dr.CONNECTION_BLUETOOTH and conn_value != address:
device_registry.async_update_device(
device.id, new_connections=connections
)
break
device_entry = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
name=adapter_human_name(adapter, details[ADAPTER_ADDRESS]),
connections={(dr.CONNECTION_BLUETOOTH, details[ADAPTER_ADDRESS])},
name=adapter_human_name(adapter, address),
connections=connections,
manufacturer=details[ADAPTER_MANUFACTURER],
model=adapter_model(details),
sw_version=details.get(ADAPTER_SW_VERSION),
@@ -342,9 +355,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
)
)
return True
address = entry.unique_id
assert address is not None
assert source_entry is not None
source_domain = entry.data[CONF_SOURCE_DOMAIN]
if mac_manufacturer := await get_manufacturer_from_mac(address):
manufacturer = f"{mac_manufacturer} ({source_domain})"
@@ -186,16 +186,28 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a flow initialized by an external scanner."""
source = user_input[CONF_SOURCE]
await self.async_set_unique_id(source)
source_config_entry_id = user_input[CONF_SOURCE_CONFIG_ENTRY_ID]
data = {
CONF_SOURCE: source,
CONF_SOURCE_MODEL: user_input[CONF_SOURCE_MODEL],
CONF_SOURCE_DOMAIN: user_input[CONF_SOURCE_DOMAIN],
CONF_SOURCE_CONFIG_ENTRY_ID: user_input[CONF_SOURCE_CONFIG_ENTRY_ID],
CONF_SOURCE_CONFIG_ENTRY_ID: source_config_entry_id,
CONF_SOURCE_DEVICE_ID: user_input[CONF_SOURCE_DEVICE_ID],
}
self._abort_if_unique_id_configured(updates=data)
manager = get_manager()
scanner = manager.async_scanner_by_source(source)
for entry in self._async_current_entries(include_ignore=False):
# If the mac address needs to be corrected, migrate
# the config entry to the new mac address
if (
entry.data.get(CONF_SOURCE_CONFIG_ENTRY_ID) == source_config_entry_id
and entry.unique_id != source
):
self.hass.config_entries.async_update_entry(
entry, unique_id=source, data={**entry.data, **data}
)
self.hass.config_entries.async_schedule_reload(entry.entry_id)
return self.async_abort(reason="already_configured")
scanner = get_manager().async_scanner_by_source(source)
assert scanner is not None
return self.async_create_entry(title=scanner.name, data=data)
@@ -19,8 +19,8 @@
"bleak-retry-connector==3.9.0",
"bluetooth-adapters==0.21.4",
"bluetooth-auto-recovery==1.4.4",
"bluetooth-data-tools==1.23.4",
"dbus-fast==2.33.0",
"habluetooth==3.24.0"
"bluetooth-data-tools==1.25.0",
"dbus-fast==2.35.1",
"habluetooth==3.25.0"
]
}
+6
View File
@@ -138,6 +138,8 @@ class WebDavTodoListEntity(TodoListEntity):
await self.hass.async_add_executor_job(
partial(self._calendar.save_todo, **item_data),
)
# refreshing async otherwise it would take too much time
self.hass.async_create_task(self.async_update_ha_state(force_refresh=True))
except (requests.ConnectionError, DAVError) as err:
raise HomeAssistantError(f"CalDAV save error: {err}") from err
@@ -172,6 +174,8 @@ class WebDavTodoListEntity(TodoListEntity):
obj_type="todo",
),
)
# refreshing async otherwise it would take too much time
self.hass.async_create_task(self.async_update_ha_state(force_refresh=True))
except (requests.ConnectionError, DAVError) as err:
raise HomeAssistantError(f"CalDAV save error: {err}") from err
@@ -195,3 +199,5 @@ class WebDavTodoListEntity(TodoListEntity):
await self.hass.async_add_executor_job(item.delete)
except (requests.ConnectionError, DAVError) as err:
raise HomeAssistantError(f"CalDAV delete error: {err}") from err
# refreshing async otherwise it would take too much time
self.hass.async_create_task(self.async_update_ha_state(force_refresh=True))
@@ -153,6 +153,27 @@ def _has_min_duration(
return validate
def _has_positive_interval(
start_key: str, end_key: str, duration_key: str
) -> Callable[[dict[str, Any]], dict[str, Any]]:
"""Verify that the time span between start and end is greater than zero."""
def validate(obj: dict[str, Any]) -> dict[str, Any]:
if (duration := obj.get(duration_key)) is not None:
if duration <= datetime.timedelta(seconds=0):
raise vol.Invalid(f"Expected positive duration ({duration})")
return obj
if (start := obj.get(start_key)) and (end := obj.get(end_key)):
if start >= end:
raise vol.Invalid(
f"Expected end time to be after start time ({start}, {end})"
)
return obj
return validate
def _has_same_type(*keys: Any) -> Callable[[dict[str, Any]], dict[str, Any]]:
"""Verify that all values are of the same type."""
@@ -281,6 +302,7 @@ SERVICE_GET_EVENTS_SCHEMA: Final = vol.All(
),
}
),
_has_positive_interval(EVENT_START_DATETIME, EVENT_END_DATETIME, EVENT_DURATION),
)
@@ -870,6 +892,7 @@ async def async_get_events_service(
end = start + service_call.data[EVENT_DURATION]
else:
end = service_call.data[EVENT_END_DATETIME]
calendar_event_list = await calendar.async_get_events(
calendar.hass, dt_util.as_local(start), dt_util.as_local(end)
)
@@ -68,7 +68,6 @@ from .const import ( # noqa: F401
FAN_ON,
FAN_TOP,
HVAC_MODES,
INTENT_GET_TEMPERATURE,
INTENT_SET_TEMPERATURE,
PRESET_ACTIVITY,
PRESET_AWAY,
@@ -126,7 +126,6 @@ DEFAULT_MAX_HUMIDITY = 99
DOMAIN = "climate"
INTENT_GET_TEMPERATURE = "HassClimateGetTemperature"
INTENT_SET_TEMPERATURE = "HassClimateSetTemperature"
SERVICE_SET_AUX_HEAT = "set_aux_heat"
+1 -42
View File
@@ -1,4 +1,4 @@
"""Intents for the client integration."""
"""Intents for the climate integration."""
from __future__ import annotations
@@ -11,7 +11,6 @@ from homeassistant.helpers import config_validation as cv, intent
from . import (
ATTR_TEMPERATURE,
DOMAIN,
INTENT_GET_TEMPERATURE,
INTENT_SET_TEMPERATURE,
SERVICE_SET_TEMPERATURE,
ClimateEntityFeature,
@@ -20,49 +19,9 @@ from . import (
async def async_setup_intents(hass: HomeAssistant) -> None:
"""Set up the climate intents."""
intent.async_register(hass, GetTemperatureIntent())
intent.async_register(hass, SetTemperatureIntent())
class GetTemperatureIntent(intent.IntentHandler):
"""Handle GetTemperature intents."""
intent_type = INTENT_GET_TEMPERATURE
description = "Gets the current temperature of a climate device or entity"
slot_schema = {
vol.Optional("area"): intent.non_empty_string,
vol.Optional("name"): intent.non_empty_string,
}
platforms = {DOMAIN}
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
"""Handle the intent."""
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
name: str | None = None
if "name" in slots:
name = slots["name"]["value"]
area: str | None = None
if "area" in slots:
area = slots["area"]["value"]
match_constraints = intent.MatchTargetsConstraints(
name=name, area_name=area, domains=[DOMAIN], assistant=intent_obj.assistant
)
match_result = intent.async_match_targets(hass, match_constraints)
if not match_result.is_match:
raise intent.MatchFailedError(
result=match_result, constraints=match_constraints
)
response = intent_obj.create_response()
response.response_type = intent.IntentResponseType.QUERY_ANSWER
response.async_set_states(matched_states=match_result.states)
return response
class SetTemperatureIntent(intent.IntentHandler):
"""Handle SetTemperature intents."""
+12 -15
View File
@@ -18,7 +18,12 @@ from hass_nabucasa.cloud_api import (
)
from hass_nabucasa.files import FilesError, StorageType, calculate_b64md5
from homeassistant.components.backup import AgentBackup, BackupAgent, BackupAgentError
from homeassistant.components.backup import (
AgentBackup,
BackupAgent,
BackupAgentError,
BackupNotFound,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import ChunkAsyncStreamIterator
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -90,9 +95,7 @@ class CloudBackupAgent(BackupAgent):
:param backup_id: The ID of the backup that was returned in async_list_backups.
:return: An async iterator that yields bytes.
"""
if not (backup := await self._async_get_backup(backup_id)):
raise BackupAgentError("Backup not found")
backup = await self._async_get_backup(backup_id)
try:
content = await self._cloud.files.download(
storage_type=StorageType.BACKUP,
@@ -171,9 +174,7 @@ class CloudBackupAgent(BackupAgent):
:param backup_id: The ID of the backup that was returned in async_list_backups.
"""
if not (backup := await self._async_get_backup(backup_id)):
return
backup = await self._async_get_backup(backup_id)
try:
await async_files_delete_file(
self._cloud,
@@ -204,16 +205,12 @@ class CloudBackupAgent(BackupAgent):
self,
backup_id: str,
**kwargs: Any,
) -> AgentBackup | None:
) -> AgentBackup:
"""Return a backup."""
if not (backup := await self._async_get_backup(backup_id)):
return None
backup = await self._async_get_backup(backup_id)
return AgentBackup.from_dict(backup["Metadata"])
async def _async_get_backup(
self,
backup_id: str,
) -> FilesHandlerListEntry | None:
async def _async_get_backup(self, backup_id: str) -> FilesHandlerListEntry:
"""Return a backup."""
backups = await self._async_list_backups()
@@ -221,4 +218,4 @@ class CloudBackupAgent(BackupAgent):
if backup["Metadata"]["backup_id"] == backup_id:
return backup
return None
raise BackupNotFound(f"Backup {backup_id} not found")
+1 -1
View File
@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==0.92.0"],
"requirements": ["hass-nabucasa==0.94.0"],
"single_config_entry": true
}
@@ -6,7 +6,7 @@ import logging
from typing import cast
from aiocomelit.api import ComelitVedoAreaObject
from aiocomelit.const import ALARM_AREAS, AlarmAreaState
from aiocomelit.const import AlarmAreaState
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
@@ -56,7 +56,7 @@ async def async_setup_entry(
async_add_entities(
ComelitAlarmEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[ALARM_AREAS].values()
for device in coordinator.data["alarm_areas"].values()
)
@@ -92,7 +92,7 @@ class ComelitAlarmEntity(CoordinatorEntity[ComelitVedoSystem], AlarmControlPanel
@property
def _area(self) -> ComelitVedoAreaObject:
"""Return area object."""
return self.coordinator.data[ALARM_AREAS][self._area_index]
return self.coordinator.data["alarm_areas"][self._area_index]
@property
def available(self) -> bool:
@@ -5,7 +5,6 @@ from __future__ import annotations
from typing import cast
from aiocomelit import ComelitVedoZoneObject
from aiocomelit.const import ALARM_ZONES
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
@@ -29,7 +28,7 @@ async def async_setup_entry(
async_add_entities(
ComelitVedoBinarySensorEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[ALARM_ZONES].values()
for device in coordinator.data["alarm_zones"].values()
)
@@ -49,7 +48,7 @@ class ComelitVedoBinarySensorEntity(
) -> None:
"""Init sensor entity."""
self._api = coordinator.api
self._zone = zone
self._zone_index = zone.index
super().__init__(coordinator)
# Use config_entry.entry_id as base for unique_id
# because no serial number or mac is available
@@ -59,4 +58,6 @@ class ComelitVedoBinarySensorEntity(
@property
def is_on(self) -> bool:
"""Presence detected."""
return self.coordinator.data[ALARM_ZONES][self._zone.index].status_api == "0001"
return (
self.coordinator.data["alarm_zones"][self._zone_index].status_api == "0001"
)
+47 -73
View File
@@ -3,7 +3,7 @@
from __future__ import annotations
from enum import StrEnum
from typing import Any, cast
from typing import Any, TypedDict, cast
from aiocomelit import ComelitSerialBridgeObject
from aiocomelit.const import CLIMATE
@@ -16,7 +16,8 @@ from homeassistant.components.climate import (
UnitOfTemperature,
)
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -42,22 +43,23 @@ class ClimaComelitCommand(StrEnum):
AUTO = "auto"
API_STATUS: dict[str, dict[str, Any]] = {
ClimaComelitMode.OFF: {
"action": "off",
"hvac_mode": HVACMode.OFF,
"hvac_action": HVACAction.OFF,
},
ClimaComelitMode.LOWER: {
"action": "lower",
"hvac_mode": HVACMode.COOL,
"hvac_action": HVACAction.COOLING,
},
ClimaComelitMode.UPPER: {
"action": "upper",
"hvac_mode": HVACMode.HEAT,
"hvac_action": HVACAction.HEATING,
},
class ClimaComelitApiStatus(TypedDict):
"""Comelit Clima API status."""
hvac_mode: HVACMode
hvac_action: HVACAction
API_STATUS: dict[str, ClimaComelitApiStatus] = {
ClimaComelitMode.OFF: ClimaComelitApiStatus(
hvac_mode=HVACMode.OFF, hvac_action=HVACAction.OFF
),
ClimaComelitMode.LOWER: ClimaComelitApiStatus(
hvac_mode=HVACMode.COOL, hvac_action=HVACAction.COOLING
),
ClimaComelitMode.UPPER: ClimaComelitApiStatus(
hvac_mode=HVACMode.HEAT, hvac_action=HVACAction.HEATING
),
}
MODE_TO_ACTION: dict[HVACMode, ClimaComelitCommand] = {
@@ -114,69 +116,41 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity
self._attr_unique_id = f"{config_entry_entry_id}-{device.index}"
self._attr_device_info = coordinator.platform_device_info(device, device.type)
@property
def _clima(self) -> list[Any]:
"""Return clima device data."""
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
device = self.coordinator.data[CLIMATE][self._device.index]
if not isinstance(device.val, list):
raise HomeAssistantError("Invalid clima data")
# CLIMATE has a 2 item tuple:
# - first for Clima
# - second for Humidifier
return self.coordinator.data[CLIMATE][self._device.index].val[0]
values = device.val[0]
@property
def _api_mode(self) -> str:
"""Return device mode."""
# Values from API: "O", "L", "U"
return self._clima[2]
_active = values[1]
_mode = values[2] # Values from API: "O", "L", "U"
_automatic = values[3] == ClimaComelitMode.AUTO
@property
def _api_active(self) -> bool:
"Return device active/idle."
return self._clima[1]
self._attr_current_temperature = values[0] / 10
@property
def _api_automatic(self) -> bool:
"""Return device in automatic/manual mode."""
return self._clima[3] == ClimaComelitMode.AUTO
self._attr_hvac_action = None
if _mode == ClimaComelitMode.OFF:
self._attr_hvac_action = HVACAction.OFF
if not _active:
self._attr_hvac_action = HVACAction.IDLE
if _mode in API_STATUS:
self._attr_hvac_action = API_STATUS[_mode]["hvac_action"]
@property
def target_temperature(self) -> float:
"""Set target temperature."""
return self._clima[4] / 10
self._attr_hvac_mode = None
if _mode == ClimaComelitMode.OFF:
self._attr_hvac_mode = HVACMode.OFF
if _automatic:
self._attr_hvac_mode = HVACMode.AUTO
if _mode in API_STATUS:
self._attr_hvac_mode = API_STATUS[_mode]["hvac_mode"]
@property
def current_temperature(self) -> float:
"""Return current temperature."""
return self._clima[0] / 10
@property
def hvac_mode(self) -> HVACMode | None:
"""HVAC current mode."""
if self._api_mode == ClimaComelitMode.OFF:
return HVACMode.OFF
if self._api_automatic:
return HVACMode.AUTO
if self._api_mode in API_STATUS:
return API_STATUS[self._api_mode]["hvac_mode"]
return None
@property
def hvac_action(self) -> HVACAction | None:
"""HVAC current action."""
if self._api_mode == ClimaComelitMode.OFF:
return HVACAction.OFF
if not self._api_active:
return HVACAction.IDLE
if self._api_mode in API_STATUS:
return API_STATUS[self._api_mode]["hvac_action"]
return None
self._attr_target_temperature = values[4] / 10
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
+26 -13
View File
@@ -2,18 +2,19 @@
from abc import abstractmethod
from datetime import timedelta
from typing import Any
from typing import TypeVar
from aiocomelit import (
from aiocomelit.api import (
AlarmDataObject,
ComelitCommonApi,
ComeliteSerialBridgeApi,
ComelitSerialBridgeObject,
ComelitVedoApi,
ComelitVedoAreaObject,
ComelitVedoZoneObject,
exceptions,
)
from aiocomelit.api import ComelitCommonApi
from aiocomelit.const import BRIDGE, VEDO
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -26,7 +27,13 @@ from .const import _LOGGER, DOMAIN
type ComelitConfigEntry = ConfigEntry[ComelitBaseCoordinator]
class ComelitBaseCoordinator(DataUpdateCoordinator[dict[str, Any]]):
T = TypeVar(
"T",
bound=dict[str, dict[int, ComelitSerialBridgeObject]] | AlarmDataObject,
)
class ComelitBaseCoordinator(DataUpdateCoordinator[T]):
"""Base coordinator for Comelit Devices."""
_hw_version: str
@@ -81,23 +88,25 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[dict[str, Any]]):
hw_version=self._hw_version,
)
async def _async_update_data(self) -> dict[str, Any]:
async def _async_update_data(self) -> T:
"""Update device data."""
_LOGGER.debug("Polling Comelit %s host: %s", self._device, self._host)
try:
await self.api.login()
return await self._async_update_system_data()
except (exceptions.CannotConnect, exceptions.CannotRetrieveData) as err:
except (CannotConnect, CannotRetrieveData) as err:
raise UpdateFailed(repr(err)) from err
except exceptions.CannotAuthenticate as err:
except CannotAuthenticate as err:
raise ConfigEntryAuthFailed from err
@abstractmethod
async def _async_update_system_data(self) -> dict[str, Any]:
async def _async_update_system_data(self) -> T:
"""Class method for updating data."""
class ComelitSerialBridge(ComelitBaseCoordinator):
class ComelitSerialBridge(
ComelitBaseCoordinator[dict[str, dict[int, ComelitSerialBridgeObject]]]
):
"""Queries Comelit Serial Bridge."""
_hw_version = "20003101"
@@ -115,12 +124,14 @@ class ComelitSerialBridge(ComelitBaseCoordinator):
self.api = ComeliteSerialBridgeApi(host, port, pin)
super().__init__(hass, entry, BRIDGE, host)
async def _async_update_system_data(self) -> dict[str, Any]:
async def _async_update_system_data(
self,
) -> dict[str, dict[int, ComelitSerialBridgeObject]]:
"""Specific method for updating data."""
return await self.api.get_all_devices()
class ComelitVedoSystem(ComelitBaseCoordinator):
class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
"""Queries Comelit VEDO system."""
_hw_version = "VEDO IP"
@@ -138,6 +149,8 @@ class ComelitVedoSystem(ComelitBaseCoordinator):
self.api = ComelitVedoApi(host, port, pin)
super().__init__(hass, entry, VEDO, host)
async def _async_update_system_data(self) -> dict[str, Any]:
async def _async_update_system_data(
self,
) -> AlarmDataObject:
"""Specific method for updating data."""
return await self.api.get_all_areas_and_zones()
+22 -51
View File
@@ -16,8 +16,8 @@ from homeassistant.components.humidifier import (
HumidifierEntity,
HumidifierEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -122,61 +122,32 @@ class ComelitHumidifierEntity(CoordinatorEntity[ComelitSerialBridge], Humidifier
self._active_action = active_action
self._set_command = set_command
@property
def _humidifier(self) -> list[Any]:
"""Return humidifier device data."""
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
device = self.coordinator.data[CLIMATE][self._device.index]
if not isinstance(device.val, list):
raise HomeAssistantError("Invalid clima data")
# CLIMATE has a 2 item tuple:
# - first for Clima
# - second for Humidifier
return self.coordinator.data[CLIMATE][self._device.index].val[1]
values = device.val[1]
@property
def _api_mode(self) -> str:
"""Return device mode."""
# Values from API: "O", "L", "U"
return self._humidifier[2]
_active = values[1]
_mode = values[2] # Values from API: "O", "L", "U"
_automatic = values[3] == HumidifierComelitMode.AUTO
@property
def _api_active(self) -> bool:
"Return device active/idle."
return self._humidifier[1]
self._attr_action = HumidifierAction.IDLE
if _mode == HumidifierComelitMode.OFF:
self._attr_action = HumidifierAction.OFF
if _active and _mode == self._active_mode:
self._attr_action = self._active_action
@property
def _api_automatic(self) -> bool:
"""Return device in automatic/manual mode."""
return self._humidifier[3] == HumidifierComelitMode.AUTO
@property
def target_humidity(self) -> float:
"""Set target humidity."""
return self._humidifier[4] / 10
@property
def current_humidity(self) -> float:
"""Return current humidity."""
return self._humidifier[0] / 10
@property
def is_on(self) -> bool | None:
"""Return true is humidifier is on."""
return self._api_mode == self._active_mode
@property
def mode(self) -> str | None:
"""Return current mode."""
return MODE_AUTO if self._api_automatic else MODE_NORMAL
@property
def action(self) -> HumidifierAction | None:
"""Return current action."""
if self._api_mode == HumidifierComelitMode.OFF:
return HumidifierAction.OFF
if self._api_active and self._api_mode == self._active_mode:
return self._active_action
return HumidifierAction.IDLE
self._attr_current_humidity = values[0] / 10
self._attr_is_on = _mode == self._active_mode
self._attr_mode = MODE_AUTO if _automatic else MODE_NORMAL
self._attr_target_humidity = values[4] / 10
async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"requirements": ["aiocomelit==0.10.1"]
"requirements": ["aiocomelit==0.11.2"]
}
+11 -8
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
from typing import Final, cast
from aiocomelit import ComelitSerialBridgeObject, ComelitVedoZoneObject
from aiocomelit.const import ALARM_ZONES, BRIDGE, OTHER, AlarmZoneState
from aiocomelit.const import BRIDGE, OTHER, AlarmZoneState
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -82,7 +82,7 @@ async def async_setup_vedo_entry(
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
entities: list[ComelitVedoSensorEntity] = []
for device in coordinator.data[ALARM_ZONES].values():
for device in coordinator.data["alarm_zones"].values():
entities.extend(
ComelitVedoSensorEntity(
coordinator, device, config_entry.entry_id, sensor_desc
@@ -119,9 +119,12 @@ class ComelitBridgeSensorEntity(CoordinatorEntity[ComelitSerialBridge], SensorEn
@property
def native_value(self) -> StateType:
"""Sensor value."""
return getattr(
self.coordinator.data[OTHER][self._device.index],
self.entity_description.key,
return cast(
StateType,
getattr(
self.coordinator.data[OTHER][self._device.index],
self.entity_description.key,
),
)
@@ -139,7 +142,7 @@ class ComelitVedoSensorEntity(CoordinatorEntity[ComelitVedoSystem], SensorEntity
) -> None:
"""Init sensor entity."""
self._api = coordinator.api
self._zone = zone
self._zone_index = zone.index
super().__init__(coordinator)
# Use config_entry.entry_id as base for unique_id
# because no serial number or mac is available
@@ -151,7 +154,7 @@ class ComelitVedoSensorEntity(CoordinatorEntity[ComelitVedoSystem], SensorEntity
@property
def _zone_object(self) -> ComelitVedoZoneObject:
"""Zone object."""
return self.coordinator.data[ALARM_ZONES][self._zone.index]
return self.coordinator.data["alarm_zones"][self._zone_index]
@property
def available(self) -> bool:
@@ -164,4 +167,4 @@ class ComelitVedoSensorEntity(CoordinatorEntity[ComelitVedoSystem], SensorEntity
if (status := self._zone_object.human_status) == AlarmZoneState.UNKNOWN:
return None
return status.value
return cast(str, status.value)
+1 -4
View File
@@ -77,7 +77,4 @@ class ComelitSwitchEntity(CoordinatorEntity[ComelitSerialBridge], SwitchEntity):
@property
def is_on(self) -> bool:
"""Return True if switch is on."""
return (
self.coordinator.data[self._device.type][self._device.index].status
== STATE_ON
)
return self.coordinator.data[OTHER][self._device.index].status == STATE_ON
@@ -49,7 +49,11 @@ def async_get_chat_log(
raise RuntimeError(
"Cannot attach chat log delta listener unless initial caller"
)
if user_input is not None:
if user_input is not None and (
(content := chat_log.content[-1]).role != "user"
# MyPy doesn't understand that content is a UserContent here
or content.content != user_input.text # type: ignore[union-attr]
):
chat_log.async_add_user_content(UserContent(content=user_input.text))
yield chat_log
@@ -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.2.5"]
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.2.26"]
}
@@ -62,12 +62,14 @@ class ConversationResult:
response: intent.IntentResponse
conversation_id: str | None = None
continue_conversation: bool = False
def as_dict(self) -> dict[str, Any]:
"""Return result as a dict."""
return {
"response": self.response.as_dict(),
"conversation_id": self.conversation_id,
"continue_conversation": self.continue_conversation,
}
@@ -48,6 +48,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [
Platform.TIME,
Platform.UPDATE,
Platform.VACUUM,
Platform.VALVE,
Platform.WATER_HEATER,
Platform.WEATHER,
]
+89
View File
@@ -0,0 +1,89 @@
"""Demo valve platform that implements valves."""
from __future__ import annotations
import asyncio
from typing import Any
from homeassistant.components.valve import ValveEntity, ValveEntityFeature, ValveState
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
OPEN_CLOSE_DELAY = 2 # Used to give a realistic open/close experience in frontend
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Demo config entry."""
async_add_entities(
[
DemoValve("Front Garden", ValveState.OPEN),
DemoValve("Orchard", ValveState.CLOSED),
]
)
class DemoValve(ValveEntity):
"""Representation of a Demo valve."""
_attr_should_poll = False
def __init__(
self,
name: str,
state: str,
moveable: bool = True,
) -> None:
"""Initialize the valve."""
self._attr_name = name
if moveable:
self._attr_supported_features = (
ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE
)
self._state = state
self._moveable = moveable
@property
def is_open(self) -> bool:
"""Return true if valve is open."""
return self._state == ValveState.OPEN
@property
def is_opening(self) -> bool:
"""Return true if valve is opening."""
return self._state == ValveState.OPENING
@property
def is_closing(self) -> bool:
"""Return true if valve is closing."""
return self._state == ValveState.CLOSING
@property
def is_closed(self) -> bool:
"""Return true if valve is closed."""
return self._state == ValveState.CLOSED
@property
def reports_position(self) -> bool:
"""Return True if entity reports position, False otherwise."""
return False
async def async_open_valve(self, **kwargs: Any) -> None:
"""Open the valve."""
self._state = ValveState.OPENING
self.async_write_ha_state()
await asyncio.sleep(OPEN_CLOSE_DELAY)
self._state = ValveState.OPEN
self.async_write_ha_state()
async def async_close_valve(self, **kwargs: Any) -> None:
"""Close the valve."""
self._state = ValveState.CLOSING
self.async_write_ha_state()
await asyncio.sleep(OPEN_CLOSE_DELAY)
self._state = ValveState.CLOSED
self.async_write_ha_state()
+51 -15
View File
@@ -24,7 +24,14 @@ from homeassistant.const import (
STATE_UNKNOWN,
UnitOfTime,
)
from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback
from homeassistant.core import (
Event,
EventStateChangedData,
EventStateReportedData,
HomeAssistant,
State,
callback,
)
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.device import async_device_info_to_link_from_entity
from homeassistant.helpers.device_registry import DeviceInfo
@@ -32,7 +39,10 @@ from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.event import (
async_track_state_change_event,
async_track_state_report_event,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import (
@@ -200,13 +210,33 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
_LOGGER.warning("Could not restore last state: %s", err)
@callback
def calc_derivative(event: Event[EventStateChangedData]) -> None:
def on_state_reported(event: Event[EventStateReportedData]) -> None:
"""Handle constant sensor state."""
if self._attr_native_value == Decimal(0):
# If the derivative is zero, and the source sensor hasn't
# changed state, then we know it will still be zero.
return
new_state = event.data["new_state"]
if new_state is not None:
calc_derivative(
new_state, new_state.state, event.data["old_last_reported"]
)
@callback
def on_state_changed(event: Event[EventStateChangedData]) -> None:
"""Handle changed sensor state."""
new_state = event.data["new_state"]
old_state = event.data["old_state"]
if new_state is not None and old_state is not None:
calc_derivative(new_state, old_state.state, old_state.last_reported)
def calc_derivative(
new_state: State, old_value: str, old_last_reported: datetime
) -> None:
"""Handle the sensor state changes."""
if (
(old_state := event.data["old_state"]) is None
or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)
or (new_state := event.data["new_state"]) is None
or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)
if old_value in (STATE_UNKNOWN, STATE_UNAVAILABLE) or new_state.state in (
STATE_UNKNOWN,
STATE_UNAVAILABLE,
):
return
@@ -220,15 +250,15 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
self._state_list = [
(time_start, time_end, state)
for time_start, time_end, state in self._state_list
if (new_state.last_updated - time_end).total_seconds()
if (new_state.last_reported - time_end).total_seconds()
< self._time_window
]
try:
elapsed_time = (
new_state.last_updated - old_state.last_updated
new_state.last_reported - old_last_reported
).total_seconds()
delta_value = Decimal(new_state.state) - Decimal(old_state.state)
delta_value = Decimal(new_state.state) - Decimal(old_value)
new_derivative = (
delta_value
/ Decimal(elapsed_time)
@@ -240,7 +270,7 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
_LOGGER.warning("While calculating derivative: %s", err)
except DecimalException as err:
_LOGGER.warning(
"Invalid state (%s > %s): %s", old_state.state, new_state.state, err
"Invalid state (%s > %s): %s", old_value, new_state.state, err
)
except AssertionError as err:
_LOGGER.error("Could not calculate derivative: %s", err)
@@ -257,7 +287,7 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
# add latest derivative to the window list
self._state_list.append(
(old_state.last_updated, new_state.last_updated, new_derivative)
(old_last_reported, new_state.last_reported, new_derivative)
)
def calculate_weight(
@@ -277,13 +307,19 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
else:
derivative = Decimal("0.00")
for start, end, value in self._state_list:
weight = calculate_weight(start, end, new_state.last_updated)
weight = calculate_weight(start, end, new_state.last_reported)
derivative = derivative + (value * Decimal(weight))
self._attr_native_value = round(derivative, self._round_digits)
self.async_write_ha_state()
self.async_on_remove(
async_track_state_change_event(
self.hass, self._sensor_source_id, calc_derivative
self.hass, self._sensor_source_id, on_state_changed
)
)
self.async_on_remove(
async_track_state_report_event(
self.hass, self._sensor_source_id, on_state_reported
)
)
@@ -8,6 +8,7 @@ from devolo_plc_api.device_api import (
WifiGuestAccessGet,
)
from devolo_plc_api.plcnet_api import DataRate, LogicalNetwork
from yarl import URL
from homeassistant.const import ATTR_CONNECTIONS
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
@@ -43,7 +44,7 @@ class DevoloEntity(Entity):
self.entry = entry
self._attr_device_info = DeviceInfo(
configuration_url=f"http://{self.device.ip}",
configuration_url=URL.build(scheme="http", host=self.device.ip),
identifiers={(DOMAIN, str(self.device.serial_number))},
manufacturer="devolo",
model=self.device.product,
+1 -1
View File
@@ -16,6 +16,6 @@
"requirements": [
"aiodhcpwatcher==1.1.1",
"aiodiscover==2.6.1",
"cached-ipaddress==0.9.2"
"cached-ipaddress==0.10.0"
]
}
@@ -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.2.0"]
"requirements": ["py-sucks==0.9.10", "deebot-client==12.3.1"]
}
@@ -6,7 +6,7 @@
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "Your Elevenlabs API key."
"api_key": "Your ElevenLabs API key."
}
}
},
@@ -105,6 +105,7 @@ class ElkArea(ElkAttachedEntity, AlarmControlPanelEntity, RestoreEntity):
AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY
| AlarmControlPanelEntityFeature.ARM_NIGHT
| AlarmControlPanelEntityFeature.ARM_VACATION
)
_element: Area
@@ -204,7 +205,7 @@ class ElkArea(ElkAttachedEntity, AlarmControlPanelEntity, RestoreEntity):
ArmedStatus.ARMED_STAY_INSTANT: AlarmControlPanelState.ARMED_HOME,
ArmedStatus.ARMED_TO_NIGHT: AlarmControlPanelState.ARMED_NIGHT,
ArmedStatus.ARMED_TO_NIGHT_INSTANT: AlarmControlPanelState.ARMED_NIGHT,
ArmedStatus.ARMED_TO_VACATION: AlarmControlPanelState.ARMED_AWAY,
ArmedStatus.ARMED_TO_VACATION: AlarmControlPanelState.ARMED_VACATION,
}
if self._element.alarm_state is None:
@@ -6,5 +6,5 @@
"iot_class": "local_push",
"loggers": ["sense_energy"],
"quality_scale": "internal",
"requirements": ["sense-energy==0.13.5"]
"requirements": ["sense-energy==0.13.6"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env-canada==0.7.2"]
"requirements": ["env-canada==0.8.0"]
}
@@ -72,5 +72,7 @@ class EpsonConfigFlow(ConfigFlow, domain=DOMAIN):
if projector:
projector.close()
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
step_id="user",
data_schema=self.add_suggested_values_to_schema(DATA_SCHEMA, user_input),
errors=errors,
)
@@ -22,5 +22,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["eq3btsmart"],
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.7.1"]
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.11.0"]
}
+3 -3
View File
@@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import CONF_NOISE_PSK, DATA_FFMPEG_PROXY, DOMAIN
from .const import CONF_BLUETOOTH_MAC_ADDRESS, CONF_NOISE_PSK, DATA_FFMPEG_PROXY, DOMAIN
from .dashboard import async_setup as async_setup_dashboard
from .domain_data import DomainData
@@ -87,6 +87,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) ->
async def async_remove_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> None:
"""Remove an esphome config entry."""
if mac_address := entry.unique_id:
async_remove_scanner(hass, mac_address.upper())
if bluetooth_mac_address := entry.data.get(CONF_BLUETOOTH_MAC_ADDRESS):
async_remove_scanner(hass, bluetooth_mac_address.upper())
await DomainData.get(hass).get_or_create_store(hass, entry).async_remove()
@@ -284,7 +284,10 @@ class EsphomeAssistSatellite(
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_END:
assert event.data is not None
data_to_send = {
"conversation_id": event.data["intent_output"]["conversation_id"] or "",
"conversation_id": event.data["intent_output"]["conversation_id"],
"continue_conversation": str(
int(event.data["intent_output"]["continue_conversation"])
),
}
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_TTS_START:
assert event.data is not None
+7 -2
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
from functools import partial
from math import isfinite
from typing import Any, cast
from aioesphomeapi import (
@@ -238,9 +239,13 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
@esphome_state_property
def current_humidity(self) -> int | None:
"""Return the current humidity."""
if not self._static_info.supports_current_humidity:
if (
not self._static_info.supports_current_humidity
or (val := self._state.current_humidity) is None
or not isfinite(val)
):
return None
return round(self._state.current_humidity)
return round(val)
@property
@esphome_float_state_property
+5 -2
View File
@@ -8,16 +8,19 @@ CONF_ALLOW_SERVICE_CALLS = "allow_service_calls"
CONF_SUBSCRIBE_LOGS = "subscribe_logs"
CONF_DEVICE_NAME = "device_name"
CONF_NOISE_PSK = "noise_psk"
CONF_BLUETOOTH_MAC_ADDRESS = "bluetooth_mac_address"
DEFAULT_ALLOW_SERVICE_CALLS = True
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False
STABLE_BLE_VERSION_STR = "2025.2.1"
STABLE_BLE_VERSION_STR = "2025.2.2"
STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR)
PROJECT_URLS = {
"esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/",
}
DEFAULT_URL = f"https://esphome.io/changelog/{STABLE_BLE_VERSION_STR}.html"
# ESPHome always uses .0 for the changelog URL
STABLE_BLE_URL_VERSION = f"{STABLE_BLE_VERSION.major}.{STABLE_BLE_VERSION.minor}.0"
DEFAULT_URL = f"https://esphome.io/changelog/{STABLE_BLE_URL_VERSION}.html"
DATA_FFMPEG_PROXY = f"{DOMAIN}.ffmpeg_proxy"
@@ -13,9 +13,7 @@ from . import CONF_NOISE_PSK
from .dashboard import async_get_dashboard
from .entry_data import ESPHomeConfigEntry
CONF_MAC_ADDRESS = "mac_address"
REDACT_KEYS = {CONF_NOISE_PSK, CONF_PASSWORD, CONF_MAC_ADDRESS}
REDACT_KEYS = {CONF_NOISE_PSK, CONF_PASSWORD, "mac_address", "bluetooth_mac_address"}
async def async_get_config_entry_diagnostics(
@@ -27,13 +25,17 @@ async def async_get_config_entry_diagnostics(
diag["config"] = config_entry.as_dict()
entry_data = config_entry.runtime_data
device_info = entry_data.device_info
if (storage_data := await entry_data.store.async_load()) is not None:
diag["storage_data"] = storage_data
if (
config_entry.unique_id
and (scanner := async_scanner_by_source(hass, config_entry.unique_id.upper()))
device_info
and (
scanner_mac := device_info.bluetooth_mac_address or device_info.mac_address
)
and (scanner := async_scanner_by_source(hass, scanner_mac.upper()))
and (bluetooth_device := entry_data.bluetooth_device)
):
diag["bluetooth"] = {
+25 -4
View File
@@ -63,6 +63,7 @@ from homeassistant.util.async_ import create_eager_task
from .bluetooth import async_connect_scanner
from .const import (
CONF_ALLOW_SERVICE_CALLS,
CONF_BLUETOOTH_MAC_ADDRESS,
CONF_DEVICE_NAME,
CONF_SUBSCRIBE_LOGS,
DEFAULT_ALLOW_SERVICE_CALLS,
@@ -431,6 +432,13 @@ class ESPHomeManager:
device_mac = format_mac(device_info.mac_address)
mac_address_matches = unique_id == device_mac
if (
bluetooth_mac_address := device_info.bluetooth_mac_address
) and entry.data.get(CONF_BLUETOOTH_MAC_ADDRESS) != bluetooth_mac_address:
hass.config_entries.async_update_entry(
entry,
data={**entry.data, CONF_BLUETOOTH_MAC_ADDRESS: bluetooth_mac_address},
)
#
# Migrate config entry to new unique ID if the current
# unique id is not a mac address.
@@ -498,7 +506,9 @@ class ESPHomeManager:
)
)
else:
bluetooth.async_remove_scanner(hass, device_info.mac_address)
bluetooth.async_remove_scanner(
hass, device_info.bluetooth_mac_address or device_info.mac_address
)
if device_info.voice_assistant_feature_flags_compat(api_version) and (
Platform.ASSIST_SATELLITE not in entry_data.loaded_platforms
@@ -617,11 +627,22 @@ class ESPHomeManager:
)
_setup_services(hass, entry_data, services)
if entry_data.device_info is not None and entry_data.device_info.name:
reconnect_logic.name = entry_data.device_info.name
if (device_info := entry_data.device_info) is not None:
if device_info.name:
reconnect_logic.name = device_info.name
if (
bluetooth_mac_address := device_info.bluetooth_mac_address
) and entry.data.get(CONF_BLUETOOTH_MAC_ADDRESS) != bluetooth_mac_address:
hass.config_entries.async_update_entry(
entry,
data={
**entry.data,
CONF_BLUETOOTH_MAC_ADDRESS: bluetooth_mac_address,
},
)
if entry.unique_id is None:
hass.config_entries.async_update_entry(
entry, unique_id=format_mac(entry_data.device_info.mac_address)
entry, unique_id=format_mac(device_info.mac_address)
)
await reconnect_logic.start()
@@ -16,9 +16,9 @@
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
"mqtt": ["esphome/discover/#"],
"requirements": [
"aioesphomeapi==29.2.0",
"aioesphomeapi==29.4.0",
"esphome-dashboard-api==1.2.3",
"bleak-esphome==2.7.1"
"bleak-esphome==2.11.0"
],
"zeroconf": ["_esphomelib._tcp.local."]
}
+50 -79
View File
@@ -7,21 +7,21 @@ from collections.abc import Callable, Mapping
import logging
from typing import Any
from pyfibaro.fibaro_client import FibaroClient
from pyfibaro.fibaro_client import (
FibaroAuthenticationFailed,
FibaroClient,
FibaroConnectFailed,
)
from pyfibaro.fibaro_data_helper import read_rooms
from pyfibaro.fibaro_device import DeviceModel
from pyfibaro.fibaro_room import RoomModel
from pyfibaro.fibaro_info import InfoModel
from pyfibaro.fibaro_scene import SceneModel
from pyfibaro.fibaro_state_resolver import FibaroEvent, FibaroStateResolver
from requests.exceptions import HTTPError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceEntry, DeviceInfo
from homeassistant.util import slugify
@@ -74,63 +74,31 @@ FIBARO_TYPEMAP = {
class FibaroController:
"""Initiate Fibaro Controller Class."""
def __init__(self, config: Mapping[str, Any]) -> None:
def __init__(
self, fibaro_client: FibaroClient, info: InfoModel, import_plugins: bool
) -> None:
"""Initialize the Fibaro controller."""
# The FibaroClient uses the correct API version automatically
self._client = FibaroClient(config[CONF_URL])
self._client.set_authentication(config[CONF_USERNAME], config[CONF_PASSWORD])
self._client = fibaro_client
self._fibaro_info = info
# Whether to import devices from plugins
self._import_plugins = config[CONF_IMPORT_PLUGINS]
self._room_map: dict[int, RoomModel] # Mapping roomId to room object
self._import_plugins = import_plugins
# Mapping roomId to room object
self._room_map = read_rooms(fibaro_client)
self._device_map: dict[int, DeviceModel] # Mapping deviceId to device object
self.fibaro_devices: dict[Platform, list[DeviceModel]] = defaultdict(
list
) # List of devices by entity platform
# All scenes
self._scenes: list[SceneModel] = []
self._scenes = self._client.read_scenes()
self._callbacks: dict[int, list[Any]] = {} # Update value callbacks by deviceId
# Event callbacks by device id
self._event_callbacks: dict[int, list[Callable[[FibaroEvent], None]]] = {}
self.hub_serial: str # Unique serial number of the hub
self.hub_name: str # The friendly name of the hub
self.hub_model: str
self.hub_software_version: str
self.hub_api_url: str = config[CONF_URL]
# Unique serial number of the hub
self.hub_serial = info.serial_number
# Device infos by fibaro device id
self._device_infos: dict[int, DeviceInfo] = {}
def connect(self) -> None:
"""Start the communication with the Fibaro controller."""
# Return value doesn't need to be checked,
# it is only relevant when connecting without credentials
self._client.connect()
info = self._client.read_info()
self.hub_serial = info.serial_number
self.hub_name = info.hc_name
self.hub_model = info.platform
self.hub_software_version = info.current_version
self._room_map = {room.fibaro_id: room for room in self._client.read_rooms()}
self._read_devices()
self._scenes = self._client.read_scenes()
def connect_with_error_handling(self) -> None:
"""Translate connect errors to easily differentiate auth and connect failures.
When there is a better error handling in the used library this can be improved.
"""
try:
self.connect()
except HTTPError as http_ex:
if http_ex.response.status_code == 403:
raise FibaroAuthFailed from http_ex
raise FibaroConnectFailed from http_ex
except Exception as ex:
raise FibaroConnectFailed from ex
def enable_state_handler(self) -> None:
"""Start StateHandler thread for monitoring updates."""
@@ -302,14 +270,20 @@ class FibaroController:
def get_room_name(self, room_id: int) -> str | None:
"""Get the room name by room id."""
assert self._room_map
room = self._room_map.get(room_id)
return room.name if room else None
return self._room_map.get(room_id)
def read_scenes(self) -> list[SceneModel]:
"""Return list of scenes."""
return self._scenes
def read_fibaro_info(self) -> InfoModel:
"""Return the general info about the hub."""
return self._fibaro_info
def get_frontend_url(self) -> str:
"""Return the url to the Fibaro hub web UI."""
return self._client.frontend_url()
def _read_devices(self) -> None:
"""Read and process the device list."""
devices = self._client.read_devices()
@@ -319,20 +293,17 @@ class FibaroController:
for device in devices:
try:
device.fibaro_controller = self
if device.room_id == 0:
room_name = self.get_room_name(device.room_id)
if not room_name:
room_name = "Unknown"
else:
room_name = self._room_map[device.room_id].name
device.room_name = room_name
device.friendly_name = f"{room_name} {device.name}"
device.ha_id = (
f"{slugify(room_name)}_{slugify(device.name)}_{device.fibaro_id}"
)
if device.enabled and (not device.is_plugin or self._import_plugins):
device.mapped_platform = self._map_device_to_platform(device)
else:
device.mapped_platform = None
if (platform := device.mapped_platform) is None:
platform = self._map_device_to_platform(device)
if platform is None:
continue
device.unique_id_str = f"{slugify(self.hub_serial)}.{device.fibaro_id}"
self._create_device_info(device, devices)
@@ -375,11 +346,17 @@ class FibaroController:
pass
def connect_fibaro_client(data: Mapping[str, Any]) -> tuple[InfoModel, FibaroClient]:
"""Connect to the fibaro hub and read some basic data."""
client = FibaroClient(data[CONF_URL])
info = client.connect_with_credentials(data[CONF_USERNAME], data[CONF_PASSWORD])
return (info, client)
def init_controller(data: Mapping[str, Any]) -> FibaroController:
"""Validate the user input allows us to connect to fibaro."""
controller = FibaroController(data)
controller.connect_with_error_handling()
return controller
"""Connect to the fibaro hub and init the controller."""
info, client = connect_fibaro_client(data)
return FibaroController(client, info, data[CONF_IMPORT_PLUGINS])
async def async_setup_entry(hass: HomeAssistant, entry: FibaroConfigEntry) -> bool:
@@ -393,22 +370,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: FibaroConfigEntry) -> bo
raise ConfigEntryNotReady(
f"Could not connect to controller at {entry.data[CONF_URL]}"
) from connect_ex
except FibaroAuthFailed as auth_ex:
except FibaroAuthenticationFailed as auth_ex:
raise ConfigEntryAuthFailed from auth_ex
entry.runtime_data = controller
# register the hub device info separately as the hub has sometimes no entities
fibaro_info = controller.read_fibaro_info()
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, controller.hub_serial)},
serial_number=controller.hub_serial,
manufacturer="Fibaro",
name=controller.hub_name,
model=controller.hub_model,
sw_version=controller.hub_software_version,
configuration_url=controller.hub_api_url.removesuffix("/api/"),
manufacturer=fibaro_info.manufacturer_name,
name=fibaro_info.hc_name,
model=fibaro_info.model_name,
sw_version=fibaro_info.current_version,
configuration_url=controller.get_frontend_url(),
connections={(dr.CONNECTION_NETWORK_MAC, fibaro_info.mac_address)},
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -443,11 +422,3 @@ async def async_remove_config_entry_device(
return False
return True
class FibaroConnectFailed(HomeAssistantError):
"""Error to indicate we cannot connect to fibaro home center."""
class FibaroAuthFailed(HomeAssistantError):
"""Error to indicate that authentication failed on fibaro home center."""
+50 -46
View File
@@ -129,13 +129,13 @@ class FibaroThermostat(FibaroEntity, ClimateEntity):
def __init__(self, fibaro_device: DeviceModel) -> None:
"""Initialize the Fibaro device."""
super().__init__(fibaro_device)
self._temp_sensor_device: FibaroEntity | None = None
self._target_temp_device: FibaroEntity | None = None
self._op_mode_device: FibaroEntity | None = None
self._fan_mode_device: FibaroEntity | None = None
self._temp_sensor_device: DeviceModel | None = None
self._target_temp_device: DeviceModel | None = None
self._op_mode_device: DeviceModel | None = None
self._fan_mode_device: DeviceModel | None = None
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
siblings = fibaro_device.fibaro_controller.get_siblings(fibaro_device)
siblings = self.controller.get_siblings(fibaro_device)
_LOGGER.debug("%s siblings: %s", fibaro_device.ha_id, siblings)
tempunit = "C"
for device in siblings:
@@ -147,23 +147,23 @@ class FibaroThermostat(FibaroEntity, ClimateEntity):
and (device.value.has_value or device.has_heating_thermostat_setpoint)
and device.unit in ("C", "F")
):
self._temp_sensor_device = FibaroEntity(device)
self._temp_sensor_device = device
tempunit = device.unit
if any(
action for action in TARGET_TEMP_ACTIONS if action in device.actions
):
self._target_temp_device = FibaroEntity(device)
self._target_temp_device = device
self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
if device.has_unit:
tempunit = device.unit
if any(action for action in OP_MODE_ACTIONS if action in device.actions):
self._op_mode_device = FibaroEntity(device)
self._op_mode_device = device
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
if "setFanMode" in device.actions:
self._fan_mode_device = FibaroEntity(device)
self._fan_mode_device = device
self._attr_supported_features |= ClimateEntityFeature.FAN_MODE
if tempunit == "F":
@@ -172,7 +172,7 @@ class FibaroThermostat(FibaroEntity, ClimateEntity):
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
if self._fan_mode_device:
fan_modes = self._fan_mode_device.fibaro_device.supported_modes
fan_modes = self._fan_mode_device.supported_modes
self._attr_fan_modes = []
for mode in fan_modes:
if mode not in FANMODES:
@@ -184,7 +184,7 @@ class FibaroThermostat(FibaroEntity, ClimateEntity):
if self._op_mode_device:
self._attr_preset_modes = []
self._attr_hvac_modes: list[HVACMode] = []
device = self._op_mode_device.fibaro_device
device = self._op_mode_device
if device.has_supported_thermostat_modes:
for mode in device.supported_thermostat_modes:
try:
@@ -222,15 +222,15 @@ class FibaroThermostat(FibaroEntity, ClimateEntity):
"- _fan_mode_device %s"
),
self.ha_id,
self._temp_sensor_device.ha_id if self._temp_sensor_device else "None",
self._target_temp_device.ha_id if self._target_temp_device else "None",
self._op_mode_device.ha_id if self._op_mode_device else "None",
self._fan_mode_device.ha_id if self._fan_mode_device else "None",
self._temp_sensor_device.fibaro_id if self._temp_sensor_device else "None",
self._target_temp_device.fibaro_id if self._target_temp_device else "None",
self._op_mode_device.fibaro_id if self._op_mode_device else "None",
self._fan_mode_device.fibaro_id if self._fan_mode_device else "None",
)
await super().async_added_to_hass()
# Register update callback for child devices
siblings = self.fibaro_device.fibaro_controller.get_siblings(self.fibaro_device)
siblings = self.controller.get_siblings(self.fibaro_device)
for device in siblings:
if device != self.fibaro_device:
self.controller.register(device.fibaro_id, self._update_callback)
@@ -240,14 +240,14 @@ class FibaroThermostat(FibaroEntity, ClimateEntity):
"""Return the fan setting."""
if not self._fan_mode_device:
return None
mode = self._fan_mode_device.fibaro_device.mode
mode = self._fan_mode_device.mode
return FANMODES[mode]
def set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode."""
if not self._fan_mode_device:
return
self._fan_mode_device.action("setFanMode", HA_FANMODES[fan_mode])
self._fan_mode_device.execute_action("setFanMode", [HA_FANMODES[fan_mode]])
@property
def fibaro_op_mode(self) -> str | int:
@@ -255,7 +255,7 @@ class FibaroThermostat(FibaroEntity, ClimateEntity):
if not self._op_mode_device:
return HA_OPMODES_HVAC[HVACMode.AUTO]
device = self._op_mode_device.fibaro_device
device = self._op_mode_device
if device.has_operating_mode:
return device.operating_mode
@@ -281,17 +281,17 @@ class FibaroThermostat(FibaroEntity, ClimateEntity):
if not self._op_mode_device:
return
if "setOperatingMode" in self._op_mode_device.fibaro_device.actions:
self._op_mode_device.action("setOperatingMode", HA_OPMODES_HVAC[hvac_mode])
elif "setThermostatMode" in self._op_mode_device.fibaro_device.actions:
device = self._op_mode_device.fibaro_device
device = self._op_mode_device
if "setOperatingMode" in device.actions:
device.execute_action("setOperatingMode", [HA_OPMODES_HVAC[hvac_mode]])
elif "setThermostatMode" in device.actions:
if device.has_supported_thermostat_modes:
for mode in device.supported_thermostat_modes:
if mode.lower() == hvac_mode:
self._op_mode_device.action("setThermostatMode", mode)
device.execute_action("setThermostatMode", [mode])
break
elif "setMode" in self._op_mode_device.fibaro_device.actions:
self._op_mode_device.action("setMode", HA_OPMODES_HVAC[hvac_mode])
elif "setMode" in device.actions:
device.execute_action("setMode", [HA_OPMODES_HVAC[hvac_mode]])
@property
def hvac_action(self) -> HVACAction | None:
@@ -299,7 +299,7 @@ class FibaroThermostat(FibaroEntity, ClimateEntity):
if not self._op_mode_device:
return None
device = self._op_mode_device.fibaro_device
device = self._op_mode_device
if device.has_thermostat_operating_state:
with suppress(ValueError):
return HVACAction(device.thermostat_operating_state.lower())
@@ -315,15 +315,15 @@ class FibaroThermostat(FibaroEntity, ClimateEntity):
if not self._op_mode_device:
return None
if self._op_mode_device.fibaro_device.has_thermostat_mode:
mode = self._op_mode_device.fibaro_device.thermostat_mode
if self._op_mode_device.has_thermostat_mode:
mode = self._op_mode_device.thermostat_mode
if self.preset_modes is not None and mode in self.preset_modes:
return mode
return None
if self._op_mode_device.fibaro_device.has_operating_mode:
mode = self._op_mode_device.fibaro_device.operating_mode
if self._op_mode_device.has_operating_mode:
mode = self._op_mode_device.operating_mode
else:
mode = self._op_mode_device.fibaro_device.mode
mode = self._op_mode_device.mode
if mode not in OPMODES_PRESET:
return None
@@ -334,20 +334,22 @@ class FibaroThermostat(FibaroEntity, ClimateEntity):
if self._op_mode_device is None:
return
if "setThermostatMode" in self._op_mode_device.fibaro_device.actions:
self._op_mode_device.action("setThermostatMode", preset_mode)
elif "setOperatingMode" in self._op_mode_device.fibaro_device.actions:
self._op_mode_device.action(
"setOperatingMode", HA_OPMODES_PRESET[preset_mode]
if "setThermostatMode" in self._op_mode_device.actions:
self._op_mode_device.execute_action("setThermostatMode", [preset_mode])
elif "setOperatingMode" in self._op_mode_device.actions:
self._op_mode_device.execute_action(
"setOperatingMode", [HA_OPMODES_PRESET[preset_mode]]
)
elif "setMode" in self._op_mode_device.actions:
self._op_mode_device.execute_action(
"setMode", [HA_OPMODES_PRESET[preset_mode]]
)
elif "setMode" in self._op_mode_device.fibaro_device.actions:
self._op_mode_device.action("setMode", HA_OPMODES_PRESET[preset_mode])
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
if self._temp_sensor_device:
device = self._temp_sensor_device.fibaro_device
device = self._temp_sensor_device
if device.has_heating_thermostat_setpoint:
return device.heating_thermostat_setpoint
return device.value.float_value()
@@ -357,7 +359,7 @@ class FibaroThermostat(FibaroEntity, ClimateEntity):
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
if self._target_temp_device:
device = self._target_temp_device.fibaro_device
device = self._target_temp_device
if device.has_heating_thermostat_setpoint_future:
return device.heating_thermostat_setpoint_future
return device.target_level
@@ -368,9 +370,11 @@ class FibaroThermostat(FibaroEntity, ClimateEntity):
temperature = kwargs.get(ATTR_TEMPERATURE)
target = self._target_temp_device
if target is not None and temperature is not None:
if "setThermostatSetpoint" in target.fibaro_device.actions:
target.action("setThermostatSetpoint", self.fibaro_op_mode, temperature)
elif "setHeatingThermostatSetpoint" in target.fibaro_device.actions:
target.action("setHeatingThermostatSetpoint", temperature)
if "setThermostatSetpoint" in target.actions:
target.execute_action(
"setThermostatSetpoint", [self.fibaro_op_mode, temperature]
)
elif "setHeatingThermostatSetpoint" in target.actions:
target.execute_action("setHeatingThermostatSetpoint", [temperature])
else:
target.action("setTargetLevel", temperature)
target.execute_action("setTargetLevel", [temperature])
@@ -6,6 +6,7 @@ from collections.abc import Mapping
import logging
from typing import Any
from pyfibaro.fibaro_client import FibaroAuthenticationFailed, FibaroConnectFailed
from slugify import slugify
import voluptuous as vol
@@ -13,7 +14,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME
from homeassistant.core import HomeAssistant
from . import FibaroAuthFailed, FibaroConnectFailed, init_controller
from . import connect_fibaro_client
from .const import CONF_IMPORT_PLUGINS, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -33,16 +34,16 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
controller = await hass.async_add_executor_job(init_controller, data)
info, _ = await hass.async_add_executor_job(connect_fibaro_client, data)
_LOGGER.debug(
"Successfully connected to fibaro home center %s with name %s",
controller.hub_serial,
controller.hub_name,
info.serial_number,
info.hc_name,
)
return {
"serial_number": slugify(controller.hub_serial),
"name": controller.hub_name,
"serial_number": slugify(info.serial_number),
"name": info.hc_name,
}
@@ -75,7 +76,7 @@ class FibaroConfigFlow(ConfigFlow, domain=DOMAIN):
info = await _validate_input(self.hass, user_input)
except FibaroConnectFailed:
errors["base"] = "cannot_connect"
except FibaroAuthFailed:
except FibaroAuthenticationFailed:
errors["base"] = "invalid_auth"
else:
await self.async_set_unique_id(info["serial_number"])
@@ -106,7 +107,7 @@ class FibaroConfigFlow(ConfigFlow, domain=DOMAIN):
await _validate_input(self.hass, new_data)
except FibaroConnectFailed:
errors["base"] = "cannot_connect"
except FibaroAuthFailed:
except FibaroAuthenticationFailed:
errors["base"] = "invalid_auth"
else:
return self.async_update_reload_and_abort(
+4 -15
View File
@@ -11,6 +11,8 @@ from pyfibaro.fibaro_device import DeviceModel
from homeassistant.const import ATTR_ARMED, ATTR_BATTERY_LEVEL
from homeassistant.helpers.entity import Entity
from . import FibaroController
_LOGGER = logging.getLogger(__name__)
@@ -22,7 +24,7 @@ class FibaroEntity(Entity):
def __init__(self, fibaro_device: DeviceModel) -> None:
"""Initialize the device."""
self.fibaro_device = fibaro_device
self.controller = fibaro_device.fibaro_controller
self.controller: FibaroController = fibaro_device.fibaro_controller
self.ha_id = fibaro_device.ha_id
self._attr_name = fibaro_device.friendly_name
self._attr_unique_id = fibaro_device.unique_id_str
@@ -54,15 +56,6 @@ class FibaroEntity(Entity):
return self.fibaro_device.value_2.int_value()
return None
def dont_know_message(self, cmd: str) -> None:
"""Make a warning in case we don't know how to perform an action."""
_LOGGER.warning(
"Not sure how to %s: %s (available actions: %s)",
cmd,
str(self.ha_id),
str(self.fibaro_device.actions),
)
def set_level(self, level: int) -> None:
"""Set the level of Fibaro device."""
self.action("setValue", level)
@@ -97,11 +90,7 @@ class FibaroEntity(Entity):
def action(self, cmd: str, *args: Any) -> None:
"""Perform an action on the Fibaro HC."""
if cmd in self.fibaro_device.actions:
self.fibaro_device.execute_action(cmd, args)
_LOGGER.debug("-> %s.%s%s called", str(self.ha_id), str(cmd), str(args))
else:
self.dont_know_message(cmd)
self.fibaro_device.execute_action(cmd, args)
@property
def current_binary_state(self) -> bool:
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["pyfibaro"],
"requirements": ["pyfibaro==0.8.0"]
"requirements": ["pyfibaro==0.8.2"]
}
@@ -1,16 +1,32 @@
"""The forked_daapd component."""
from pyforked_daapd import ForkedDaapdAPI
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, HASS_DATA_UPDATER_KEY
from .coordinator import ForkedDaapdUpdater
PLATFORMS = [Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up forked-daapd from a config entry by forwarding to platform."""
host: str = entry.data[CONF_HOST]
port: int = entry.data[CONF_PORT]
password: str = entry.data[CONF_PASSWORD]
forked_daapd_api = ForkedDaapdAPI(
async_get_clientsession(hass), host, port, password
)
forked_daapd_updater = ForkedDaapdUpdater(hass, forked_daapd_api, entry.entry_id)
if not hass.data.get(DOMAIN):
hass.data[DOMAIN] = {entry.entry_id: {}}
hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {})[
HASS_DATA_UPDATER_KEY
] = forked_daapd_updater
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -39,6 +39,11 @@ class ForkedDaapdUpdater:
self._all_output_ids: set[str] = set()
self._entry_id = entry_id
@property
def api(self) -> ForkedDaapdAPI:
"""Return the API object."""
return self._api
async def async_init(self) -> None:
"""Perform async portion of class initialization."""
if not (server_config := await self._api.get_request("config")):
@@ -7,7 +7,6 @@ from collections import defaultdict
import logging
from typing import Any
from pyforked_daapd import ForkedDaapdAPI
from pylibrespot_java import LibrespotJavaAPI
from homeassistant.components import media_source
@@ -29,7 +28,7 @@ from homeassistant.components.spotify import (
spotify_uri_from_media_browser_url,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import (
@@ -85,12 +84,12 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up forked-daapd from a config entry."""
forked_daapd_updater: ForkedDaapdUpdater = hass.data[DOMAIN][config_entry.entry_id][
HASS_DATA_UPDATER_KEY
]
host: str = config_entry.data[CONF_HOST]
port: int = config_entry.data[CONF_PORT]
password: str = config_entry.data[CONF_PASSWORD]
forked_daapd_api = ForkedDaapdAPI(
async_get_clientsession(hass), host, port, password
)
forked_daapd_api = forked_daapd_updater.api
forked_daapd_master = ForkedDaapdMaster(
clientsession=async_get_clientsession(hass),
api=forked_daapd_api,
@@ -111,16 +110,8 @@ async def async_setup_entry(
)
config_entry.async_on_unload(config_entry.add_update_listener(update_listener))
if not hass.data.get(DOMAIN):
hass.data[DOMAIN] = {config_entry.entry_id: {}}
async_add_entities([forked_daapd_master], False)
forked_daapd_updater = ForkedDaapdUpdater(
hass, forked_daapd_api, config_entry.entry_id
)
hass.data[DOMAIN][config_entry.entry_id][HASS_DATA_UPDATER_KEY] = (
forked_daapd_updater
)
await forked_daapd_updater.async_init()
@@ -0,0 +1 @@
"""FrankEver virtual integration."""
@@ -0,0 +1,6 @@
{
"domain": "frankever",
"name": "FrankEver",
"integration_type": "virtual",
"supported_by": "shelly"
}
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250226.0"]
"requirements": ["home-assistant-frontend==20250228.0"]
}
@@ -1,6 +1,6 @@
{
"common": {
"data_description_password": "The Remote Admin Password from the Fully Kiosk Browser app settings.",
"data_description_password": "The Remote Admin password from the Fully Kiosk Browser app settings.",
"data_description_ssl": "Is the Fully Kiosk app configured to require SSL for the connection?",
"data_description_verify_ssl": "Should SSL certificartes be verified? This should be off for self-signed certificates."
},
@@ -151,7 +151,7 @@
}
},
"set_config": {
"name": "Set Configuration",
"name": "Set configuration",
"description": "Sets a configuration parameter on Fully Kiosk Browser.",
"fields": {
"key": {
@@ -165,7 +165,7 @@
}
},
"start_application": {
"name": "Start Application",
"name": "Start application",
"description": "Starts an application on the device running Fully Kiosk Browser.",
"fields": {
"application": {
@@ -45,7 +45,7 @@
},
"mode": {
"name": "[%key:common::config_flow::data::mode%]",
"description": "One of: off, timer or footprint."
"description": "The zone's operating mode."
}
}
},
@@ -8,7 +8,12 @@ from typing import Any
from google_drive_api.exceptions import GoogleDriveApiError
from homeassistant.components.backup import AgentBackup, BackupAgent, BackupAgentError
from homeassistant.components.backup import (
AgentBackup,
BackupAgent,
BackupAgentError,
BackupNotFound,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import ChunkAsyncStreamIterator
@@ -93,13 +98,13 @@ class GoogleDriveBackupAgent(BackupAgent):
self,
backup_id: str,
**kwargs: Any,
) -> AgentBackup | None:
) -> AgentBackup:
"""Return a backup."""
backups = await self.async_list_backups()
for backup in backups:
if backup.backup_id == backup_id:
return backup
return None
raise BackupNotFound(f"Backup {backup_id} not found")
async def async_download_backup(
self,
@@ -120,7 +125,7 @@ class GoogleDriveBackupAgent(BackupAgent):
return ChunkAsyncStreamIterator(stream)
except (GoogleDriveApiError, HomeAssistantError, TimeoutError) as err:
raise BackupAgentError(f"Failed to download backup: {err}") from err
raise BackupAgentError("Backup not found")
raise BackupNotFound(f"Backup {backup_id} not found")
async def async_delete_backup(
self,
@@ -138,5 +143,7 @@ class GoogleDriveBackupAgent(BackupAgent):
_LOGGER.debug("Deleting file_id: %s", file_id)
await self._client.async_delete(file_id)
_LOGGER.debug("Deleted backup_id: %s", backup_id)
return
except (GoogleDriveApiError, HomeAssistantError, TimeoutError) as err:
raise BackupAgentError(f"Failed to delete backup: {err}") from err
raise BackupNotFound(f"Backup {backup_id} not found")
@@ -111,9 +111,20 @@ def _format_schema(schema: dict[str, Any]) -> Schema:
continue
if key == "any_of":
val = [_format_schema(subschema) for subschema in val]
if key == "type":
elif key == "type":
val = val.upper()
if key == "items":
elif key == "format":
# Gemini API does not support all formats, see: https://ai.google.dev/api/caching#Schema
# formats that are not supported are ignored
if schema.get("type") == "string" and val not in ("enum", "date-time"):
continue
if schema.get("type") == "number" and val not in ("float", "double"):
continue
if schema.get("type") == "integer" and val not in ("int32", "int64"):
continue
if schema.get("type") not in ("string", "number", "integer"):
continue
elif key == "items":
val = _format_schema(val)
elif key == "properties":
val = {k: _format_schema(v) for k, v in val.items()}
+1
View File
@@ -20,3 +20,4 @@ MAX_ERRORS = 2
TARGET_TEMPERATURE_STEP = 1
UPDATE_INTERVAL = 60
MAX_EXPECTED_RESPONSE_TIME_INTERVAL = UPDATE_INTERVAL * 2
+10 -4
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import copy
from datetime import datetime, timedelta
import logging
from typing import Any
@@ -24,6 +25,7 @@ from .const import (
DISPATCH_DEVICE_DISCOVERED,
DOMAIN,
MAX_ERRORS,
MAX_EXPECTED_RESPONSE_TIME_INTERVAL,
UPDATE_INTERVAL,
)
@@ -48,7 +50,6 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
always_update=False,
)
self.device = device
self.device.add_handler(Response.DATA, self.device_state_updated)
self.device.add_handler(Response.RESULT, self.device_state_updated)
self._error_count: int = 0
@@ -88,7 +89,9 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
# raise update failed if time for more than MAX_ERRORS has passed since last update
now = utcnow()
elapsed_success = now - self._last_response_time
if self.update_interval and elapsed_success >= self.update_interval:
if self.update_interval and elapsed_success >= timedelta(
seconds=MAX_EXPECTED_RESPONSE_TIME_INTERVAL
):
if not self._last_error_time or (
(now - self.update_interval) >= self._last_error_time
):
@@ -96,16 +99,19 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self._error_count += 1
_LOGGER.warning(
"Device %s is unresponsive for %s seconds",
"Device %s took an unusually long time to respond, %s seconds",
self.name,
elapsed_success,
)
else:
self._error_count = 0
if self.last_update_success and self._error_count >= MAX_ERRORS:
raise UpdateFailed(
f"Device {self.name} is unresponsive for too long and now unavailable"
)
return self.device.raw_properties
self._last_response_time = utcnow()
return copy.deepcopy(self.device.raw_properties)
async def push_state_update(self):
"""Send state updates to the physical device."""
@@ -26,6 +26,7 @@ TOTAL_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="todayEnergy",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
GrowattSensorEntityDescription(
key="total_output_power",
@@ -33,6 +34,7 @@ TOTAL_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="invTodayPpv",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="total_energy_output",
@@ -40,6 +40,10 @@ ATTR_ALIAS = "alias"
ATTR_PRIORITY = "priority"
ATTR_COST = "cost"
ATTR_NOTES = "notes"
ATTR_UP_DOWN = "up_down"
ATTR_FREQUENCY = "frequency"
ATTR_COUNTER_UP = "counter_up"
ATTR_COUNTER_DOWN = "counter_down"
SERVICE_CAST_SKILL = "cast_skill"
SERVICE_START_QUEST = "start_quest"
@@ -56,6 +60,9 @@ SERVICE_SCORE_REWARD = "score_reward"
SERVICE_TRANSFORMATION = "transformation"
SERVICE_UPDATE_REWARD = "update_reward"
SERVICE_CREATE_REWARD = "create_reward"
SERVICE_UPDATE_HABIT = "update_habit"
SERVICE_CREATE_HABIT = "create_habit"
DEVELOPER_ID = "4c4ca53f-c059-4ffa-966e-9d29dd405daf"
X_CLIENT = f"{DEVELOPER_ID} - {APPLICATION_NAME} {__version__}"
@@ -224,6 +224,25 @@
"tag_options": "mdi:tag",
"developer_options": "mdi:test-tube"
}
},
"create_reward": {
"service": "mdi:treasure-chest-outline",
"sections": {
"developer_options": "mdi:test-tube"
}
},
"update_habit": {
"service": "mdi:contrast-box",
"sections": {
"tag_options": "mdi:tag",
"developer_options": "mdi:test-tube"
}
},
"create_habit": {
"service": "mdi:contrast-box",
"sections": {
"developer_options": "mdi:test-tube"
}
}
}
}
+106 -27
View File
@@ -10,6 +10,7 @@ from uuid import UUID
from aiohttp import ClientError
from habiticalib import (
Direction,
Frequency,
HabiticaException,
NotAuthorizedError,
NotFoundError,
@@ -41,8 +42,11 @@ from .const import (
ATTR_ARGS,
ATTR_CONFIG_ENTRY,
ATTR_COST,
ATTR_COUNTER_DOWN,
ATTR_COUNTER_UP,
ATTR_DATA,
ATTR_DIRECTION,
ATTR_FREQUENCY,
ATTR_ITEM,
ATTR_KEYWORD,
ATTR_NOTES,
@@ -54,6 +58,7 @@ from .const import (
ATTR_TARGET,
ATTR_TASK,
ATTR_TYPE,
ATTR_UP_DOWN,
DOMAIN,
EVENT_API_CALL_SUCCESS,
SERVICE_ABORT_QUEST,
@@ -61,6 +66,8 @@ from .const import (
SERVICE_API_CALL,
SERVICE_CANCEL_QUEST,
SERVICE_CAST_SKILL,
SERVICE_CREATE_HABIT,
SERVICE_CREATE_REWARD,
SERVICE_GET_TASKS,
SERVICE_LEAVE_QUEST,
SERVICE_REJECT_QUEST,
@@ -68,6 +75,7 @@ from .const import (
SERVICE_SCORE_REWARD,
SERVICE_START_QUEST,
SERVICE_TRANSFORMATION,
SERVICE_UPDATE_HABIT,
SERVICE_UPDATE_REWARD,
)
from .coordinator import HabiticaConfigEntry
@@ -112,18 +120,36 @@ SERVICE_TRANSFORMATION_SCHEMA = vol.Schema(
}
)
SERVICE_UPDATE_TASK_SCHEMA = vol.Schema(
BASE_TASK_SCHEMA = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(),
vol.Required(ATTR_TASK): cv.string,
vol.Optional(ATTR_RENAME): cv.string,
vol.Optional(ATTR_NOTES): cv.string,
vol.Optional(ATTR_TAG): vol.All(cv.ensure_list, [str]),
vol.Optional(ATTR_REMOVE_TAG): vol.All(cv.ensure_list, [str]),
vol.Optional(ATTR_ALIAS): vol.All(
cv.string, cv.matches_regex("^[a-zA-Z0-9-_]*$")
),
vol.Optional(ATTR_COST): vol.Coerce(float),
vol.Optional(ATTR_COST): vol.All(vol.Coerce(float), vol.Range(0)),
vol.Optional(ATTR_PRIORITY): vol.All(
vol.Upper, vol.In(TaskPriority._member_names_)
),
vol.Optional(ATTR_UP_DOWN): vol.All(cv.ensure_list, [str]),
vol.Optional(ATTR_COUNTER_UP): vol.All(int, vol.Range(0)),
vol.Optional(ATTR_COUNTER_DOWN): vol.All(int, vol.Range(0)),
vol.Optional(ATTR_FREQUENCY): vol.Coerce(Frequency),
}
)
SERVICE_UPDATE_TASK_SCHEMA = BASE_TASK_SCHEMA.extend(
{
vol.Required(ATTR_TASK): cv.string,
vol.Optional(ATTR_REMOVE_TAG): vol.All(cv.ensure_list, [str]),
}
)
SERVICE_CREATE_TASK_SCHEMA = BASE_TASK_SCHEMA.extend(
{
vol.Required(ATTR_NAME): cv.string,
}
)
@@ -161,6 +187,13 @@ ITEMID_MAP = {
"shiny_seed": Skill.SHINY_SEED,
}
SERVICE_TASK_TYPE_MAP = {
SERVICE_UPDATE_REWARD: TaskType.REWARD,
SERVICE_CREATE_REWARD: TaskType.REWARD,
SERVICE_UPDATE_HABIT: TaskType.HABIT,
SERVICE_CREATE_HABIT: TaskType.HABIT,
}
def get_config_entry(hass: HomeAssistant, entry_id: str) -> HabiticaConfigEntry:
"""Return config entry or raise if not found or not loaded."""
@@ -539,33 +572,36 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
return result
async def update_task(call: ServiceCall) -> ServiceResponse:
"""Update task action."""
async def create_or_update_task(call: ServiceCall) -> ServiceResponse: # noqa: C901
"""Create or update task action."""
entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
coordinator = entry.runtime_data
await coordinator.async_refresh()
is_update = call.service in (SERVICE_UPDATE_REWARD, SERVICE_UPDATE_HABIT)
current_task = None
try:
current_task = next(
task
for task in coordinator.data.tasks
if call.data[ATTR_TASK] in (str(task.id), task.alias, task.text)
and task.Type is TaskType.REWARD
)
except StopIteration as e:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="task_not_found",
translation_placeholders={"task": f"'{call.data[ATTR_TASK]}'"},
) from e
if is_update:
try:
current_task = next(
task
for task in coordinator.data.tasks
if call.data[ATTR_TASK] in (str(task.id), task.alias, task.text)
and task.Type is SERVICE_TASK_TYPE_MAP[call.service]
)
except StopIteration as e:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="task_not_found",
translation_placeholders={"task": f"'{call.data[ATTR_TASK]}'"},
) from e
task_id = current_task.id
if TYPE_CHECKING:
assert task_id
data = Task()
if rename := call.data.get(ATTR_RENAME):
data["text"] = rename
if not is_update:
data["type"] = SERVICE_TASK_TYPE_MAP[call.service]
if (text := call.data.get(ATTR_RENAME)) or (text := call.data.get(ATTR_NAME)):
data["text"] = text
if (notes := call.data.get(ATTR_NOTES)) is not None:
data["notes"] = notes
@@ -574,7 +610,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
remove_tags = cast(list[str], call.data.get(ATTR_REMOVE_TAG))
if tags or remove_tags:
update_tags = set(current_task.tags)
update_tags = set(current_task.tags) if current_task else set()
user_tags = {
tag.name.lower(): tag.id
for tag in coordinator.data.user.tags
@@ -633,8 +669,30 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
if (cost := call.data.get(ATTR_COST)) is not None:
data["value"] = cost
if priority := call.data.get(ATTR_PRIORITY):
data["priority"] = TaskPriority[priority]
if frequency := call.data.get(ATTR_FREQUENCY):
data["frequency"] = frequency
if up_down := call.data.get(ATTR_UP_DOWN):
data["up"] = "up" in up_down
data["down"] = "down" in up_down
if counter_up := call.data.get(ATTR_COUNTER_UP):
data["counterUp"] = counter_up
if counter_down := call.data.get(ATTR_COUNTER_DOWN):
data["counterDown"] = counter_down
try:
response = await coordinator.habitica.update_task(task_id, data)
if is_update:
if TYPE_CHECKING:
assert current_task
assert current_task.id
response = await coordinator.habitica.update_task(current_task.id, data)
else:
response = await coordinator.habitica.create_task(data)
except TooManyRequestsError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
@@ -659,10 +717,31 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
hass.services.async_register(
DOMAIN,
SERVICE_UPDATE_REWARD,
update_task,
create_or_update_task,
schema=SERVICE_UPDATE_TASK_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
SERVICE_UPDATE_HABIT,
create_or_update_task,
schema=SERVICE_UPDATE_TASK_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
SERVICE_CREATE_REWARD,
create_or_update_task,
schema=SERVICE_CREATE_TASK_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
SERVICE_CREATE_HABIT,
create_or_update_task,
schema=SERVICE_CREATE_TASK_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
SERVICE_API_CALL,
@@ -144,26 +144,26 @@ update_reward:
fields:
config_entry: *config_entry
task: *task
rename:
rename: &rename
selector:
text:
notes:
notes: &notes
required: false
selector:
text:
multiline: true
cost:
required: false
selector:
selector: &cost_selector
number:
min: 0
step: 0.01
unit_of_measurement: "🪙"
mode: box
tag_options:
tag_options: &tag_options
collapsed: true
fields:
tag:
tag: &tag
required: false
selector:
text:
@@ -173,10 +173,92 @@ update_reward:
selector:
text:
multiple: true
developer_options:
developer_options: &developer_options
collapsed: true
fields:
alias:
alias: &alias
required: false
selector:
text:
create_reward:
fields:
config_entry: *config_entry
name: &name
required: true
selector:
text:
notes: *notes
cost:
required: true
selector: *cost_selector
tag: *tag
developer_options: *developer_options
update_habit:
fields:
config_entry: *config_entry
task: *task
rename: *rename
notes: *notes
up_down: &up_down
required: false
selector:
select:
options:
- value: up
label: ""
- value: down
label: ""
multiple: true
mode: list
priority: &priority
required: false
selector:
select:
options:
- "trivial"
- "easy"
- "medium"
- "hard"
mode: dropdown
translation_key: "priority"
frequency: &frequency
required: false
selector:
select:
options:
- "daily"
- "weekly"
- "monthly"
translation_key: "frequency"
mode: dropdown
tag_options: *tag_options
developer_options:
collapsed: true
fields:
counter_up:
required: false
selector:
number:
min: 0
step: 1
unit_of_measurement: ""
mode: box
counter_down:
required: false
selector:
number:
min: 0
step: 1
unit_of_measurement: ""
mode: box
alias: *alias
create_habit:
fields:
config_entry: *config_entry
name: *name
notes: *notes
up_down: *up_down
priority: *priority
frequency: *frequency
tag: *tag
developer_options: *developer_options
+165 -5
View File
@@ -11,9 +11,9 @@
"config_entry_description": "Select the Habitica account to update a task.",
"task_description": "The name (or task ID) of the task you want to update.",
"rename_name": "Rename",
"rename_description": "The new title for the Habitica task.",
"notes_name": "Update notes",
"notes_description": "The new notes for the Habitica task.",
"rename_description": "The title for the Habitica task.",
"notes_name": "Notes",
"notes_description": "The notes for the Habitica task.",
"tag_name": "Add tags",
"tag_description": "Add tags to the Habitica task. If a tag does not already exist, a new one will be created.",
"remove_tag_name": "Remove tags",
@@ -23,7 +23,15 @@
"developer_options_name": "Advanced settings",
"developer_options_description": "Additional features available in developer mode.",
"tag_options_name": "Tags",
"tag_options_description": "Add or remove tags from a task."
"tag_options_description": "Add or remove tags from a task.",
"name_description": "The title for the Habitica task.",
"cost_name": "Cost",
"difficulty_name": "Difficulty",
"difficulty_description": "The difficulty of the task.",
"frequency_name": "Counter reset",
"frequency_description": "The frequency at which the habit's counter resets: daily at the start of a new day, weekly after Sunday night, or monthly at the beginning of a new month.",
"up_down_name": "Rewards or losses",
"up_down_description": "Whether the habit is good and rewarding (positive), bad and penalizing (negative), or both."
},
"config": {
"abort": {
@@ -707,7 +715,7 @@
"description": "[%key:component::habitica::common::alias_description%]"
},
"cost": {
"name": "Cost",
"name": "[%key:component::habitica::common::cost_name%]",
"description": "Update the cost of a reward."
}
},
@@ -721,6 +729,150 @@
"description": "[%key:component::habitica::common::developer_options_description%]"
}
}
},
"create_reward": {
"name": "Create reward",
"description": "Adds a new custom reward.",
"fields": {
"config_entry": {
"name": "[%key:component::habitica::common::config_entry_name%]",
"description": "Select the Habitica account to create a reward."
},
"name": {
"name": "[%key:component::habitica::common::task_name%]",
"description": "[%key:component::habitica::common::name_description%]"
},
"notes": {
"name": "[%key:component::habitica::common::notes_name%]",
"description": "[%key:component::habitica::common::notes_description%]"
},
"tag": {
"name": "[%key:component::habitica::common::tag_name%]",
"description": "[%key:component::habitica::common::tag_description%]"
},
"alias": {
"name": "[%key:component::habitica::common::alias_name%]",
"description": "[%key:component::habitica::common::alias_description%]"
},
"cost": {
"name": "[%key:component::habitica::common::cost_name%]",
"description": "The cost of the reward."
}
},
"sections": {
"developer_options": {
"name": "[%key:component::habitica::common::developer_options_name%]",
"description": "[%key:component::habitica::common::developer_options_description%]"
}
}
},
"update_habit": {
"name": "Update a habit",
"description": "Updates a specific habit for the selected Habitica character",
"fields": {
"config_entry": {
"name": "[%key:component::habitica::common::config_entry_name%]",
"description": "Select the Habitica account to update a habit."
},
"task": {
"name": "[%key:component::habitica::common::task_name%]",
"description": "[%key:component::habitica::common::task_description%]"
},
"rename": {
"name": "[%key:component::habitica::common::rename_name%]",
"description": "[%key:component::habitica::common::rename_description%]"
},
"notes": {
"name": "[%key:component::habitica::common::notes_name%]",
"description": "[%key:component::habitica::common::notes_description%]"
},
"tag": {
"name": "[%key:component::habitica::common::tag_name%]",
"description": "[%key:component::habitica::common::tag_description%]"
},
"remove_tag": {
"name": "[%key:component::habitica::common::remove_tag_name%]",
"description": "[%key:component::habitica::common::remove_tag_description%]"
},
"alias": {
"name": "[%key:component::habitica::common::alias_name%]",
"description": "[%key:component::habitica::common::alias_description%]"
},
"priority": {
"name": "[%key:component::habitica::common::difficulty_name%]",
"description": "[%key:component::habitica::common::difficulty_description%]"
},
"frequency": {
"name": "[%key:component::habitica::common::frequency_name%]",
"description": "[%key:component::habitica::common::frequency_description%]"
},
"up_down": {
"name": "[%key:component::habitica::common::up_down_name%]",
"description": "[%key:component::habitica::common::up_down_description%]"
},
"counter_up": {
"name": "Adjust positive counter",
"description": "Update the up counter of a positive habit."
},
"counter_down": {
"name": "Adjust negative counter",
"description": "Update the down counter of a negative habit."
}
},
"sections": {
"tag_options": {
"name": "[%key:component::habitica::common::tag_options_name%]",
"description": "[%key:component::habitica::common::tag_options_description%]"
},
"developer_options": {
"name": "[%key:component::habitica::common::developer_options_name%]",
"description": "[%key:component::habitica::common::developer_options_description%]"
}
}
},
"create_habit": {
"name": "Create habit",
"description": "Adds a new habit.",
"fields": {
"config_entry": {
"name": "[%key:component::habitica::common::config_entry_name%]",
"description": "Select the Habitica account to create a habit."
},
"name": {
"name": "[%key:component::habitica::common::task_name%]",
"description": "[%key:component::habitica::common::name_description%]"
},
"notes": {
"name": "[%key:component::habitica::common::notes_name%]",
"description": "[%key:component::habitica::common::notes_description%]"
},
"tag": {
"name": "[%key:component::habitica::common::tag_name%]",
"description": "[%key:component::habitica::common::tag_description%]"
},
"alias": {
"name": "[%key:component::habitica::common::alias_name%]",
"description": "[%key:component::habitica::common::alias_description%]"
},
"priority": {
"name": "[%key:component::habitica::common::difficulty_name%]",
"description": "[%key:component::habitica::common::difficulty_description%]"
},
"frequency": {
"name": "[%key:component::habitica::common::frequency_name%]",
"description": "[%key:component::habitica::common::frequency_description%]"
},
"up_down": {
"name": "[%key:component::habitica::common::up_down_name%]",
"description": "[%key:component::habitica::common::up_down_description%]"
}
},
"sections": {
"developer_options": {
"name": "[%key:component::habitica::common::developer_options_name%]",
"description": "[%key:component::habitica::common::developer_options_description%]"
}
}
}
},
"selector": {
@@ -755,6 +907,14 @@
"medium": "Medium",
"hard": "Hard"
}
},
"frequency": {
"options": {
"daily": "Daily",
"weekly": "Weekly",
"monthly": "Monthly",
"yearly": "Yearly"
}
}
}
}
+20 -16
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
from contextlib import suppress
import logging
import os
from pathlib import Path, PurePath
@@ -173,7 +174,7 @@ class SupervisorBackupAgent(BackupAgent):
),
)
except SupervisorNotFoundError as err:
raise BackupNotFound from err
raise BackupNotFound(f"Backup {backup_id} not found") from err
async def async_upload_backup(
self,
@@ -186,13 +187,14 @@ class SupervisorBackupAgent(BackupAgent):
The upload will be skipped if the backup already exists in the agent's location.
"""
if await self.async_get_backup(backup.backup_id):
_LOGGER.debug(
"Backup %s already exists in location %s",
backup.backup_id,
self.location,
)
return
with suppress(BackupNotFound):
if await self.async_get_backup(backup.backup_id):
_LOGGER.debug(
"Backup %s already exists in location %s",
backup.backup_id,
self.location,
)
return
stream = await open_stream()
upload_options = supervisor_backups.UploadBackupOptions(
location={self.location},
@@ -218,14 +220,14 @@ class SupervisorBackupAgent(BackupAgent):
self,
backup_id: str,
**kwargs: Any,
) -> AgentBackup | None:
) -> AgentBackup:
"""Return a backup."""
try:
details = await self._client.backups.backup_info(backup_id)
except SupervisorNotFoundError:
return None
except SupervisorNotFoundError as err:
raise BackupNotFound(f"Backup {backup_id} not found") from err
if self.location not in details.location_attributes:
return None
raise BackupNotFound(f"Backup {backup_id} not found")
return _backup_details_to_agent_backup(details, self.location)
async def async_delete_backup(self, backup_id: str, **kwargs: Any) -> None:
@@ -237,8 +239,8 @@ class SupervisorBackupAgent(BackupAgent):
location={self.location}
),
)
except SupervisorNotFoundError:
_LOGGER.debug("Backup %s does not exist", backup_id)
except SupervisorNotFoundError as err:
raise BackupNotFound(f"Backup {backup_id} not found") from err
class SupervisorBackupReaderWriter(BackupReaderWriter):
@@ -492,10 +494,12 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
) -> None:
"""Restore a backup."""
manager = self._hass.data[DATA_MANAGER]
# The backup manager has already checked that the backup exists so we don't need to
# check that here.
# The backup manager has already checked that the backup exists so we don't
# need to catch BackupNotFound here.
backup = await manager.backup_agents[agent_id].async_get_backup(backup_id)
if (
# Check for None to be backwards compatible with the old BackupAgent API,
# this can be removed in HA Core 2025.10
backup
and restore_homeassistant
and restore_database != backup.database_included
+1 -2
View File
@@ -11,7 +11,6 @@ from hko import HKO, HKOError
from homeassistant.components.weather import (
ATTR_CONDITION_CLOUDY,
ATTR_CONDITION_FOG,
ATTR_CONDITION_HAIL,
ATTR_CONDITION_LIGHTNING_RAINY,
ATTR_CONDITION_PARTLYCLOUDY,
ATTR_CONDITION_POURING,
@@ -145,7 +144,7 @@ class HKOUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Return the condition corresponding to the weather info."""
info = info.lower()
if WEATHER_INFO_RAIN in info:
return ATTR_CONDITION_HAIL
return ATTR_CONDITION_RAINY
if WEATHER_INFO_SNOW in info and WEATHER_INFO_RAIN in info:
return ATTR_CONDITION_SNOWY_RAINY
if WEATHER_INFO_SNOW in info:
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.67", "babel==2.15.0"]
"requirements": ["holidays==0.68", "babel==2.15.0"]
}

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