Compare commits

...

239 Commits

Author SHA1 Message Date
Franck Nijhof
635669278c Merge branch 'dev' into manual_trigger_entity-fix-availability 2025-03-02 21:03:23 +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](f9eaf234fc...bd77c07785)

---
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
Joost Lekkerkerker
2e972422c2 Fix typo in SmartThing string (#139373) 2025-02-26 18:19:45 +01:00
Joost Lekkerkerker
3a21c36173 Don't create entities for disabled capabilities in SmartThings (#139343)
* Don't create entities for disabled capabilities in SmartThings

* Fix

* fix

* fix
2025-02-26 18:19:28 +01:00
Joost Lekkerkerker
25ee2e58a5 Add translatable states to dryer job state in SmartThings (#139370)
* Add translatable states to washer job state in SmartThings

* Add translatable states to dryer job state in Smartthings

* fix

* fix
2025-02-26 18:15:14 +01:00
Joost Lekkerkerker
561b3ae21b Add translatable states to dryer machine state in Smartthings (#139369) 2025-02-26 18:14:59 +01:00
J. Diego Rodríguez Royo
5be7f49146 Improve Home Connect oven cavity temperature sensor (#139355)
* Improve oven cavity temperature translation

* Fetch cavity temperature unit

* Handle generic Home Connect error

* Improve test clarity
2025-02-26 18:11:40 +01:00
Joost Lekkerkerker
2694828451 Add translatable states to washer job state in SmartThings (#139368)
* Add translatable states to washer job state in SmartThings

* fix

* Update homeassistant/components/smartthings/sensor.py
2025-02-26 18:07:56 +01:00
Joost Lekkerkerker
3eea932b24 Add translatable states to robot cleaner turbo mode in SmartThings (#139364) 2025-02-26 17:53:16 +01:00
Joost Lekkerkerker
468208502f Add translatable states to smoke detector in SmartThings (#139365) 2025-02-26 17:52:57 +01:00
Joost Lekkerkerker
92268f894a Add translatable states to washer machine state in SmartThings (#139366) 2025-02-26 17:34:29 +01:00
Joost Lekkerkerker
5e5fd6a2f2 Add translatable states to robot cleaner cleaning mode in SmartThings (#139362)
* Add translatable states to robot cleaner cleaning mode in SmartThings

* Update homeassistant/components/smartthings/strings.json

* Update homeassistant/components/smartthings/strings.json

---------

Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-02-26 17:33:13 +01:00
Joost Lekkerkerker
cadee73da8 Add translatable states to robot cleaner movement in SmartThings (#139363) 2025-02-26 17:25:50 +01:00
Joost Lekkerkerker
51099ae7d6 Add translatable states to oven machine state (#139358) 2025-02-26 17:13:02 +01:00
Joost Lekkerkerker
b777c29bab Add translatable states to oven job state in SmartThings (#139361) 2025-02-26 17:12:27 +01:00
Joost Lekkerkerker
fc1190dafd Add translatable states to oven mode in SmartThings (#139356) 2025-02-26 16:59:20 +01:00
Joost Lekkerkerker
775a81829b Add translatable states to SmartThings media playback (#139354)
Add translatable states to media playback
2025-02-26 16:49:00 +01:00
Joost Lekkerkerker
998757f09e Add translatable states to SmartThings media source input (#139353)
Add translatable states to media source input
2025-02-26 16:40:34 +01:00
Artur Pragacz
b964bc58be Fix variable scopes in scripts (#138883)
Co-authored-by: Erik <erik@montnemery.com>
2025-02-26 16:19:19 +01:00
Joost Lekkerkerker
bd80a78848 Set options for alarm sensor in SmartThings (#139345)
* Set options for alarm sensor in SmartThings

* Set options for alarm sensor in SmartThings

* Fix
2025-02-26 17:18:59 +02:00
Joost Lekkerkerker
37c8764426 Set options for dishwasher machine state sensor in SmartThings (#139347)
* Set options for dishwasher machine state sensor in SmartThings

* Fix
2025-02-26 17:18:37 +02:00
Joost Lekkerkerker
9262dec444 Set options for dishwasher job state sensor in SmartThings (#139349) 2025-02-26 17:18:14 +02:00
Joost Lekkerkerker
3c3c4d2641 Use particulate matter device class in SmartThings (#139351)
Use particule matter device class in SmartThings
2025-02-26 17:17:55 +02:00
Bram Kragten
c1898ece80 Update frontend to 20250226.0 (#139340)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-02-26 16:13:45 +01:00
Jan Bouwhuis
fdf69fcd7d Improve calculating supported features in template light (#139339) 2025-02-26 15:09:20 +00:00
Joost Lekkerkerker
e403bee95b Set options for carbon monoxide detector sensor in SmartThings (#139346) 2025-02-26 16:05:59 +01:00
Joost Lekkerkerker
9be8fd4eac Change no fixtures comment in SmartThings (#139344) 2025-02-26 16:59:23 +02:00
Artur Pragacz
e09b40c2bd Improve logging for selected options in Onkyo (#139279)
Different error for not selected option
2025-02-26 15:51:16 +01:00
Joost Lekkerkerker
2826198d5d Add entity translations to SmartThings (#139342)
* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* Refactor SmartThings

* fix

* fix

* Add AC tests

* Add thermostat tests

* Add cover tests

* Add device tests

* Add light tests

* Add rest of the tests

* Add oauth

* Add oauth tests

* Add oauth tests

* Add oauth tests

* Add oauth tests

* Bump version

* Add rest of the tests

* Finalize

* Finalize

* Finalize

* Finalize

* Finalize

* Finalize

* Finalize

* Finalize

* Finalize

* Finalize

* Iterate over entities instead

* use set

* use const

* uncomment

* fix handler

* Fix device info

* Fix device info

* Fix lib

* Fix lib

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Add fake fan

* Fix

* Add entity translations to SmartThings

* Fix
2025-02-26 15:48:51 +01:00
Jan Bouwhuis
5324f3e542 Add support for swing horizontal mode for mqtt climate (#139303)
* Add support for swing horizontal mode for mqtt climate

* Fix import
2025-02-26 15:44:16 +01:00
Erik Montnemery
7e97ef588b Add keys initiate_flow and entry_type to data entry translations (#138882) 2025-02-26 15:27:52 +01:00
Joost Lekkerkerker
bb120020a8 Refactor SmartThings (#137940) 2025-02-26 15:14:04 +01:00
Marcel van der Veldt
bb9aba2a7d Bump Music Assistant client to 1.1.1 (#139331) 2025-02-26 14:48:18 +01:00
Norbert Rittel
b676c2f61b Improve action descriptions of LIFX integration (#139329)
Improve action description of lifx integration

- fix sentence-casing on two action names
- change "Kelvin" unit name to proper uppercase
- reference 'Theme' and 'Palette' fields by their friendly names for matching translations
- change paint_theme action description to match HA style
2025-02-26 15:24:19 +02:00
Erik Montnemery
0c092f80c7 Add default_db_url flag to WS command recorder/info (#139333) 2025-02-26 14:09:38 +01:00
J. Nick Koston
2bf592d8aa Bump recommended ESPHome Bluetooth proxy version to 2025.2.1 (#139196) 2025-02-26 12:55:03 +00:00
Paul Bottein
e591157e37 Add translations and icon for Twinkly select entity (#139336)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-02-26 13:44:43 +01:00
Erik Montnemery
ee01aa73b8 Improve error message when failing to create backups (#139262)
* Improve error message when failing to create backups

* Check for expected error message in tests
2025-02-26 13:44:09 +01:00
fwestenberg
0f827fbf22 Bump stookwijzer==1.6.0 (#139332) 2025-02-26 13:31:07 +01:00
Ben Bridts
4dca4a64b5 Bump pybotvac to 0.0.26 (#139330) 2025-02-26 13:26:12 +01:00
Denis Shulyaka
b82886a3e1 Fix anthropic blocking call (#139299) 2025-02-26 12:25:59 +00:00
Matt Zimmerman
fe396cdf4b Update python-smarttub dependency to 0.0.39 (#139313) 2025-02-26 11:59:13 +01:00
Christophe Gagnier
5895245a31 Bump pytechnove to 2.0.0 (#139314) 2025-02-26 11:57:54 +01:00
TheJulianJES
861ba0ee5e Bump ZHA to 0.0.50 (#139318) 2025-02-26 11:52:57 +01:00
Maciej Bieniek
d15f9edc57 Bump accuweather to version 4.1.0 (#139320) 2025-02-26 11:51:35 +01:00
Erik Montnemery
cab6ec0363 Fix homeassistant/expose_entity/list (#138872)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-02-26 09:02:17 +01:00
J. Nick Koston
eb26a2124b Adjust remote ESPHome log subscription level on logging change (#139308) 2025-02-26 08:58:13 +01:00
dependabot[bot]
4530fe4bf7 Bump home-assistant/builder from 2024.08.2 to 2025.02.0 (#139316)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-26 08:48:25 +01:00
dependabot[bot]
b1865de58f Bump actions/download-artifact from 4.1.8 to 4.1.9 (#139317)
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-02-26 08:13:25 +01:00
J. Nick Koston
3ff04d6d04 Bump aioesphomeapi to 29.2.0 (#139309) 2025-02-26 03:14:58 +01:00
peteS-UK
bd306abace Add album artist media browser category to Squeezebox (#139210) 2025-02-25 17:55:53 -06:00
Michael
412ceca6f7 Sort common translation strings (#139300)
sort common strings
2025-02-25 23:22:02 +01:00
J. Diego Rodríguez Royo
8644fb1887 Add missing Home Connect context at event listener registration for appliance options (#139292)
* Add missing context at event listener registration for appliance options

* Add tests
2025-02-25 23:05:52 +01:00
Abílio Costa
622be70fee Remove timeout from vscode test launch configuration (#139288) 2025-02-25 23:02:49 +01:00
Maciej Bieniek
7bc0c1b912 Bump aioshelly to version 13.0.0 (#139294)
* Bump aioshelly to version 13.0.0

* MODEL_BLU_GATEWAY_GEN3 -> MODEL_BLU_GATEWAY_G3
2025-02-25 23:52:44 +02:00
G Johansson
3230e741e9 Remove not used constants in smhi (#139298) 2025-02-25 22:49:41 +01:00
J. Nick Koston
81db3dea41 Add option to ESPHome to subscribe to logs (#139073) 2025-02-25 21:56:39 +01:00
J. Nick Koston
fe348e17a3 Revert "Bump stookwijzer==1.5.8" (#139287) 2025-02-25 21:43:06 +01:00
Pierre Ståhl
03f6508bd8 Fix re-connect logic in Apple TV integration (#139289) 2025-02-25 20:37:01 +00:00
G Johansson
1c5eb92c9c Clean test 2025-01-13 18:26:36 +00:00
G Johansson
3337dd4ed7 Add state template test 2025-01-13 18:21:31 +00:00
G Johansson
f1d21685e6 Remove availability from complex 2025-01-13 17:59:02 +00:00
G Johansson
73f27549e4 Error better name 2025-01-13 17:58:40 +00:00
G Johansson
1882b914dc render templates calls availability render 2025-01-13 17:54:19 +00:00
G Johansson
06f99dc9ba Mods 2025-01-07 23:07:03 +00:00
G Johansson
2e2c718d94 Remove render complex for availability 2025-01-07 21:18:12 +00:00
G Johansson
b8f56a6ed6 Fix 2025-01-07 21:18:12 +00:00
G Johansson
db37dbec03 Fix 2025-01-07 21:18:12 +00:00
G Johansson
579f44468e Only render templates if availability is true 2025-01-07 21:18:12 +00:00
G Johansson
d452e957c9 Last fix 2025-01-07 21:18:12 +00:00
G Johansson
5f9bcd583b Mod 2025-01-07 21:18:12 +00:00
G Johansson
c0c508c7a2 Fix 2025-01-07 21:18:11 +00:00
Erik
13f5adfa84 Try a different approach 2025-01-07 21:18:11 +00:00
G Johansson
a07a3a61bf Add test 2025-01-07 21:18:11 +00:00
G Johansson
848162debd Fix when state breaks to stringify 2025-01-07 21:18:11 +00:00
G Johansson
07cd669bc1 Fix availability for manual trigger entities 2025-01-07 21:18:11 +00:00
502 changed files with 40621 additions and 9490 deletions

View File

@@ -94,7 +94,7 @@ jobs:
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@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
@@ -175,7 +175,7 @@ jobs:
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
- name: Download translations
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.9
with:
name: translations
@@ -197,7 +197,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2024.08.2
uses: home-assistant/builder@2025.02.0
with:
args: |
$BUILD_ARGS \
@@ -263,7 +263,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2024.08.2
uses: home-assistant/builder@2025.02.0
with:
args: |
$BUILD_ARGS \
@@ -462,7 +462,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download translations
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.9
with:
name: translations
@@ -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 }}

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
@@ -240,7 +240,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 +256,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 +286,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 +295,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 +326,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 +335,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 +366,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 +375,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 +482,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 +490,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 +578,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 +611,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 +649,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 +692,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 +739,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 +791,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 +799,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: >-
@@ -865,7 +865,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
@@ -929,7 +929,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
@@ -942,7 +942,7 @@ jobs:
run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Download pytest_buckets
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.9
with:
name: pytest_buckets
- name: Compile English translations
@@ -1051,7 +1051,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
@@ -1181,7 +1181,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
@@ -1271,12 +1271,12 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.9
with:
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
@@ -1328,7 +1328,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
@@ -1410,12 +1410,12 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.9
with:
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 }}

View File

@@ -138,17 +138,17 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Download env_file
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.9
with:
name: env_file
- name: Download build_constraints
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.9
with:
name: build_constraints
- name: Download requirements_diff
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.9
with:
name: requirements_diff
@@ -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
@@ -187,22 +187,22 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Download env_file
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.9
with:
name: env_file
- name: Download build_constraints
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.9
with:
name: build_constraints
- name: Download requirements_diff
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.9
with:
name: requirements_diff
- name: Download requirements_all_wheels
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.9
with:
name: requirements_all_wheels
@@ -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"

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:

View File

@@ -396,6 +396,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 +529,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.*

1
.vscode/launch.json vendored
View File

@@ -38,7 +38,6 @@
"module": "pytest",
"justMyCode": false,
"args": [
"--timeout=10",
"--picked"
],
},

2
CODEOWNERS generated
View File

@@ -1401,6 +1401,8 @@ build.json @home-assistant/supervisor
/tests/components/smappee/ @bsmappee
/homeassistant/components/smart_meter_texas/ @grahamwetzler
/tests/components/smart_meter_texas/ @grahamwetzler
/homeassistant/components/smartthings/ @joostlek
/tests/components/smartthings/ @joostlek
/homeassistant/components/smarttub/ @mdz
/tests/components/smarttub/ @mdz
/homeassistant/components/smarty/ @z0mbieprocess

View File

@@ -7,6 +7,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["accuweather"],
"requirements": ["accuweather==4.0.0"],
"requirements": ["accuweather==4.1.0"],
"single_config_entry": true
}

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()

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
from functools import partial
import anthropic
from homeassistant.config_entries import ConfigEntry
@@ -10,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)
@@ -20,14 +22,13 @@ type AnthropicConfigEntry = ConfigEntry[anthropic.AsyncClient]
async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) -> bool:
"""Set up Anthropic from a config entry."""
client = anthropic.AsyncAnthropic(api_key=entry.data[CONF_API_KEY])
client = await hass.async_add_executor_job(
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

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from functools import partial
import logging
from types import MappingProxyType
from typing import Any
@@ -59,13 +60,10 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
client = 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,
client = await hass.async_add_executor_job(
partial(anthropic.AsyncAnthropic, api_key=data[CONF_API_KEY])
)
await client.models.list(timeout=10.0)
class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):

View File

@@ -233,7 +233,6 @@ class AppleTVManager(DeviceListener):
pass
except Exception:
_LOGGER.exception("Failed to connect")
await self.disconnect()
async def _connect_loop(self) -> None:
"""Connect loop background task function."""

View File

@@ -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,

View File

@@ -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

View File

@@ -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:

View File

@@ -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]},

View File

@@ -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,
)

View File

@@ -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"]
}

View File

@@ -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

View File

@@ -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": {

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
@@ -308,6 +309,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."""
@@ -1605,10 +1612,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,
@@ -1620,7 +1641,13 @@ class CoreBackupReaderWriter(BackupReaderWriter):
"""Generate backup contents and return the size."""
if not tar_file_path:
tar_file_path = self.temp_backup_dir / f"{backup_data['slug']}.tar"
make_backup_dir(tar_file_path.parent)
try:
make_backup_dir(tar_file_path.parent)
except OSError as err:
raise BackupReaderWriterError(
f"Failed to create dir {tar_file_path.parent}: "
f"{err} ({err.__class__.__name__})"
) from err
excludes = EXCLUDE_FROM_BACKUP
if not database_included:
@@ -1658,7 +1685,14 @@ class CoreBackupReaderWriter(BackupReaderWriter):
file_filter=is_excluded_by_filter,
arcname="data",
)
return (tar_file_path, tar_file_path.stat().st_size)
try:
stat_result = tar_file_path.stat()
except OSError as err:
raise BackupReaderWriterError(
f"Error getting size of {tar_file_path}: "
f"{err} ({err.__class__.__name__})"
) from err
return (tar_file_path, stat_result.st_size)
async def async_receive_backup(
self,

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)

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
)
)

View File

@@ -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}"

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})"

View File

@@ -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)

View File

@@ -21,6 +21,6 @@
"bluetooth-auto-recovery==1.4.4",
"bluetooth-data-tools==1.23.4",
"dbus-fast==2.33.0",
"habluetooth==3.24.0"
"habluetooth==3.24.1"
]
}

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))

View File

@@ -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,

View File

@@ -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"

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."""

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"requirements": ["aiocomelit==0.10.1"]
"requirements": ["aiocomelit==0.11.1"]
}

View File

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

View File

@@ -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,
}

View File

@@ -48,6 +48,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [
Platform.TIME,
Platform.UPDATE,
Platform.VACUUM,
Platform.VALVE,
Platform.WATER_HEATER,
Platform.WEATHER,
]

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()

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
)
)

View File

@@ -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,

View File

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

View File

@@ -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:

View File

@@ -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"]
}

View File

@@ -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.9.0"]
}

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()

View File

@@ -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

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

View File

@@ -41,6 +41,7 @@ from .const import (
CONF_ALLOW_SERVICE_CALLS,
CONF_DEVICE_NAME,
CONF_NOISE_PSK,
CONF_SUBSCRIBE_LOGS,
DEFAULT_ALLOW_SERVICE_CALLS,
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
DOMAIN,
@@ -508,6 +509,10 @@ class OptionsFlowHandler(OptionsFlow):
CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS
),
): bool,
vol.Required(
CONF_SUBSCRIBE_LOGS,
default=self.config_entry.options.get(CONF_SUBSCRIBE_LOGS, False),
): bool,
}
)
return self.async_show_form(step_id="init", data_schema=data_schema)

View File

@@ -5,18 +5,22 @@ from awesomeversion import AwesomeVersion
DOMAIN = "esphome"
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 = "2023.8.0"
STABLE_BLE_VERSION_STR = "2025.2.1"
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"

View File

@@ -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"] = {

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import asyncio
from functools import partial
import logging
import re
from typing import TYPE_CHECKING, Any, NamedTuple
from aioesphomeapi import (
@@ -16,6 +17,7 @@ from aioesphomeapi import (
HomeassistantServiceCall,
InvalidAuthAPIError,
InvalidEncryptionKeyAPIError,
LogLevel,
ReconnectLogic,
RequiresEncryptionAPIError,
UserService,
@@ -33,6 +35,7 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import (
CALLBACK_TYPE,
Event,
EventStateChangedData,
HomeAssistant,
@@ -60,7 +63,9 @@ 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,
DEFAULT_URL,
DOMAIN,
@@ -74,8 +79,38 @@ from .domain_data import DomainData
# Import config flow so that it's added to the registry
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
if TYPE_CHECKING:
from aioesphomeapi.api_pb2 import ( # type: ignore[attr-defined]
SubscribeLogsResponse,
)
_LOGGER = logging.getLogger(__name__)
LOG_LEVEL_TO_LOGGER = {
LogLevel.LOG_LEVEL_NONE: logging.DEBUG,
LogLevel.LOG_LEVEL_ERROR: logging.ERROR,
LogLevel.LOG_LEVEL_WARN: logging.WARNING,
LogLevel.LOG_LEVEL_INFO: logging.INFO,
LogLevel.LOG_LEVEL_CONFIG: logging.INFO,
LogLevel.LOG_LEVEL_DEBUG: logging.DEBUG,
LogLevel.LOG_LEVEL_VERBOSE: logging.DEBUG,
LogLevel.LOG_LEVEL_VERY_VERBOSE: logging.DEBUG,
}
LOGGER_TO_LOG_LEVEL = {
logging.NOTSET: LogLevel.LOG_LEVEL_VERY_VERBOSE,
logging.DEBUG: LogLevel.LOG_LEVEL_VERY_VERBOSE,
logging.INFO: LogLevel.LOG_LEVEL_CONFIG,
logging.WARNING: LogLevel.LOG_LEVEL_WARN,
logging.ERROR: LogLevel.LOG_LEVEL_ERROR,
logging.CRITICAL: LogLevel.LOG_LEVEL_ERROR,
}
# 7-bit and 8-bit C1 ANSI sequences
# https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python
ANSI_ESCAPE_78BIT = re.compile(
rb"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])"
)
@callback
def _async_check_firmware_version(
@@ -136,6 +171,8 @@ class ESPHomeManager:
"""Class to manage an ESPHome connection."""
__slots__ = (
"_cancel_subscribe_logs",
"_log_level",
"cli",
"device_id",
"domain_data",
@@ -169,6 +206,8 @@ class ESPHomeManager:
self.reconnect_logic: ReconnectLogic | None = None
self.zeroconf_instance = zeroconf_instance
self.entry_data = entry.runtime_data
self._cancel_subscribe_logs: CALLBACK_TYPE | None = None
self._log_level = LogLevel.LOG_LEVEL_NONE
async def on_stop(self, event: Event) -> None:
"""Cleanup the socket client on HA close."""
@@ -341,6 +380,34 @@ class ESPHomeManager:
# Re-connection logic will trigger after this
await self.cli.disconnect()
def _async_on_log(self, msg: SubscribeLogsResponse) -> None:
"""Handle a log message from the API."""
log: bytes = msg.message
_LOGGER.log(
LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG),
"%s: %s",
self.entry.title,
ANSI_ESCAPE_78BIT.sub(b"", log).decode("utf-8", "backslashreplace"),
)
@callback
def _async_get_equivalent_log_level(self) -> LogLevel:
"""Get the equivalent ESPHome log level for the current logger."""
return LOGGER_TO_LOG_LEVEL.get(
_LOGGER.getEffectiveLevel(), LogLevel.LOG_LEVEL_VERY_VERBOSE
)
@callback
def _async_subscribe_logs(self, log_level: LogLevel) -> None:
"""Subscribe to logs."""
if self._cancel_subscribe_logs is not None:
self._cancel_subscribe_logs()
self._cancel_subscribe_logs = None
self._log_level = log_level
self._cancel_subscribe_logs = self.cli.subscribe_logs(
self._async_on_log, self._log_level
)
async def _on_connnect(self) -> None:
"""Subscribe to states and list entities on successful API login."""
entry = self.entry
@@ -352,6 +419,8 @@ class ESPHomeManager:
cli = self.cli
stored_device_name = entry.data.get(CONF_DEVICE_NAME)
unique_id_is_mac_address = unique_id and ":" in unique_id
if entry.options.get(CONF_SUBSCRIBE_LOGS):
self._async_subscribe_logs(self._async_get_equivalent_log_level())
results = await asyncio.gather(
create_eager_task(cli.device_info()),
create_eager_task(cli.list_entities_services()),
@@ -363,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.
@@ -430,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
@@ -503,6 +581,10 @@ class ESPHomeManager:
def _async_handle_logging_changed(self, _event: Event) -> None:
"""Handle when the logging level changes."""
self.cli.set_debug(_LOGGER.isEnabledFor(logging.DEBUG))
if self.entry.options.get(CONF_SUBSCRIBE_LOGS) and self._log_level != (
new_log_level := self._async_get_equivalent_log_level()
):
self._async_subscribe_logs(new_log_level)
async def async_start(self) -> None:
"""Start the esphome connection manager."""
@@ -545,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()

View File

@@ -16,9 +16,9 @@
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
"mqtt": ["esphome/discover/#"],
"requirements": [
"aioesphomeapi==29.1.1",
"aioesphomeapi==29.3.2",
"esphome-dashboard-api==1.2.3",
"bleak-esphome==2.7.1"
"bleak-esphome==2.9.0"
],
"zeroconf": ["_esphomelib._tcp.local."]
}

View File

@@ -54,7 +54,8 @@
"step": {
"init": {
"data": {
"allow_service_calls": "Allow the device to perform Home Assistant actions."
"allow_service_calls": "Allow the device to perform Home Assistant actions.",
"subscribe_logs": "Subscribe to logs from the device. When enabled, the device will send logs to Home Assistant and you can view them in the logs panel."
}
}
}

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."""

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])

View File

@@ -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(

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:

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["pyfibaro"],
"requirements": ["pyfibaro==0.8.0"]
"requirements": ["pyfibaro==0.8.2"]
}

View File

@@ -0,0 +1 @@
"""FrankEver virtual integration."""

View File

@@ -0,0 +1,6 @@
{
"domain": "frankever",
"name": "FrankEver",
"integration_type": "virtual",
"supported_by": "shelly"
}

View File

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

View File

@@ -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."
}
}
},

View File

@@ -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()}

View File

@@ -20,3 +20,4 @@ MAX_ERRORS = 2
TARGET_TEMPERATURE_STEP = 1
UPDATE_INTERVAL = 60
MAX_EXPECTED_RESPONSE_TIME_INTERVAL = UPDATE_INTERVAL * 2

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."""

View File

@@ -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",

View File

@@ -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,8 @@ SERVICE_SCORE_REWARD = "score_reward"
SERVICE_TRANSFORMATION = "transformation"
SERVICE_UPDATE_REWARD = "update_reward"
SERVICE_CREATE_REWARD = "create_reward"
SERVICE_UPDATE_HABIT = "update_habit"
DEVELOPER_ID = "4c4ca53f-c059-4ffa-966e-9d29dd405daf"
X_CLIENT = f"{DEVELOPER_ID} - {APPLICATION_NAME} {__version__}"

View File

@@ -224,6 +224,19 @@
"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"
}
}
}
}

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,7 @@ from .const import (
SERVICE_API_CALL,
SERVICE_CANCEL_QUEST,
SERVICE_CAST_SKILL,
SERVICE_CREATE_REWARD,
SERVICE_GET_TASKS,
SERVICE_LEAVE_QUEST,
SERVICE_REJECT_QUEST,
@@ -68,6 +74,7 @@ from .const import (
SERVICE_SCORE_REWARD,
SERVICE_START_QUEST,
SERVICE_TRANSFORMATION,
SERVICE_UPDATE_HABIT,
SERVICE_UPDATE_REWARD,
)
from .coordinator import HabiticaConfigEntry
@@ -112,18 +119,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 +186,12 @@ 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,
}
def get_config_entry(hass: HomeAssistant, entry_id: str) -> HabiticaConfigEntry:
"""Return config entry or raise if not found or not loaded."""
@@ -539,33 +570,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"] = TaskType.REWARD
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 +608,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 +667,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 +715,24 @@ 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_API_CALL,

View File

@@ -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,82 @@ 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:
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:
required: false
selector:
select:
options:
- value: up
label: ""
- value: down
label: ""
multiple: true
mode: list
priority:
required: false
selector:
select:
options:
- "trivial"
- "easy"
- "medium"
- "hard"
mode: dropdown
translation_key: "priority"
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

View File

@@ -23,7 +23,9 @@
"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"
},
"config": {
"abort": {
@@ -707,7 +709,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 +723,106 @@
"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": "Difficulty",
"description": "Update the difficulty of a task."
},
"frequency": {
"name": "Counter reset",
"description": "Update when a habit's counter resets: daily resets at the start of a new day, weekly after Sunday night, and monthly at the beginning of a new month."
},
"up_down": {
"name": "Rewards or losses",
"description": "Update if the habit is good and rewarding (positive), bad and penalizing (negative), or both."
},
"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%]"
}
}
}
},
"selector": {
@@ -755,6 +857,14 @@
"medium": "Medium",
"hard": "Hard"
}
},
"frequency": {
"options": {
"daily": "Daily",
"weekly": "Weekly",
"monthly": "Monthly",
"yearly": "Yearly"
}
}
}
}

View File

@@ -72,22 +72,27 @@ def _handle_paired_or_connected_appliance(
for entity in get_option_entities_for_appliance(entry, appliance)
if entity.unique_id not in known_entity_unique_ids
)
changed_options_listener_remove_callback = (
entry.runtime_data.async_add_listener(
partial(
_create_option_entities,
entry,
appliance,
known_entity_unique_ids,
get_option_entities_for_appliance,
async_add_entities,
),
for event_key in (
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
):
changed_options_listener_remove_callback = (
entry.runtime_data.async_add_listener(
partial(
_create_option_entities,
entry,
appliance,
known_entity_unique_ids,
get_option_entities_for_appliance,
async_add_entities,
),
(appliance.info.ha_id, event_key),
)
)
entry.async_on_unload(changed_options_listener_remove_callback)
changed_options_listener_remove_callbacks[appliance.info.ha_id].append(
changed_options_listener_remove_callback
)
)
entry.async_on_unload(changed_options_listener_remove_callback)
changed_options_listener_remove_callbacks[appliance.info.ha_id].append(
changed_options_listener_remove_callback
)
known_entity_unique_ids.update(
{
cast(str, entity.unique_id): appliance.info.ha_id

View File

@@ -4,6 +4,8 @@ from typing import cast
from aiohomeconnect.model import EventKey, OptionKey, ProgramKey, SettingKey, StatusKey
from homeassistant.const import UnitOfTemperature, UnitOfTime, UnitOfVolume
from .utils import bsh_key_to_translation_key
DOMAIN = "home_connect"
@@ -21,6 +23,13 @@ APPLIANCES_WITH_PROGRAMS = (
"WasherDryer",
)
UNIT_MAP = {
"seconds": UnitOfTime.SECONDS,
"ml": UnitOfVolume.MILLILITERS,
"°C": UnitOfTemperature.CELSIUS,
"°F": UnitOfTemperature.FAHRENHEIT,
}
BSH_POWER_ON = "BSH.Common.EnumType.PowerState.On"
BSH_POWER_OFF = "BSH.Common.EnumType.PowerState.Off"

View File

@@ -440,13 +440,27 @@ class HomeConnectCoordinator(
self, ha_id: str, program_key: ProgramKey
) -> dict[OptionKey, ProgramDefinitionOption]:
"""Get options with constraints for appliance."""
return {
option.key: option
for option in (
await self.client.get_available_program(ha_id, program_key=program_key)
).options
or []
}
if program_key is ProgramKey.UNKNOWN:
return {}
try:
return {
option.key: option
for option in (
await self.client.get_available_program(
ha_id, program_key=program_key
)
).options
or []
}
except HomeConnectError as error:
_LOGGER.debug(
"Error fetching options for %s: %s",
ha_id,
error
if isinstance(error, HomeConnectApiError)
else type(error).__name__,
)
return {}
async def update_options(
self, ha_id: str, event_key: EventKey, program_key: ProgramKey
@@ -456,8 +470,7 @@ class HomeConnectCoordinator(
events = self.data[ha_id].events
options_to_notify = options.copy()
options.clear()
if program_key is not ProgramKey.UNKNOWN:
options.update(await self.get_options_definitions(ha_id, program_key))
options.update(await self.get_options_definitions(ha_id, program_key))
for option in options.values():
option_value = option.constraints.default if option.constraints else None

View File

@@ -49,6 +49,23 @@
"default": "mdi:map-marker-remove-variant"
}
},
"button": {
"open_door": {
"default": "mdi:door-open"
},
"partly_open_door": {
"default": "mdi:door-open"
},
"pause_program": {
"default": "mdi:pause"
},
"resume_program": {
"default": "mdi:play"
},
"stop_program": {
"default": "mdi:stop"
}
},
"sensor": {
"operation_state": {
"default": "mdi:state-machine",

View File

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

View File

@@ -11,7 +11,6 @@ from homeassistant.components.number import (
NumberEntity,
NumberEntityDescription,
)
from homeassistant.const import UnitOfTemperature, UnitOfTime, UnitOfVolume
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -23,6 +22,7 @@ from .const import (
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID,
SVE_TRANSLATION_PLACEHOLDER_KEY,
SVE_TRANSLATION_PLACEHOLDER_VALUE,
UNIT_MAP,
)
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
from .entity import HomeConnectEntity, HomeConnectOptionEntity
@@ -32,13 +32,6 @@ _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
UNIT_MAP = {
"seconds": UnitOfTime.SECONDS,
"ml": UnitOfVolume.MILLILITERS,
"°C": UnitOfTemperature.CELSIUS,
"°F": UnitOfTemperature.FAHRENHEIT,
}
NUMBERS = (
NumberEntityDescription(
key=SettingKey.REFRIGERATION_FRIDGE_FREEZER_SETPOINT_TEMPERATURE_REFRIGERATOR,

View File

@@ -1,10 +1,12 @@
"""Provides a sensor for Home Connect."""
import contextlib
from dataclasses import dataclass
from datetime import timedelta
from typing import cast
from aiohomeconnect.model import EventKey, StatusKey
from aiohomeconnect.model.error import HomeConnectError
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -23,6 +25,7 @@ from .const import (
BSH_OPERATION_STATE_FINISHED,
BSH_OPERATION_STATE_PAUSE,
BSH_OPERATION_STATE_RUN,
UNIT_MAP,
)
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
from .entity import HomeConnectEntity
@@ -40,6 +43,7 @@ class HomeConnectSensorEntityDescription(
default_value: str | None = None
appliance_types: tuple[str, ...] | None = None
fetch_unit: bool = False
BSH_PROGRAM_SENSORS = (
@@ -183,7 +187,8 @@ SENSORS = (
key=StatusKey.COOKING_OVEN_CURRENT_CAVITY_TEMPERATURE,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="current_cavity_temperature",
translation_key="oven_current_cavity_temperature",
fetch_unit=True,
),
)
@@ -318,6 +323,29 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity):
case _:
self._attr_native_value = status
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
if self.entity_description.fetch_unit:
data = self.appliance.status[cast(StatusKey, self.bsh_key)]
if data.unit:
self._attr_native_unit_of_measurement = UNIT_MAP.get(
data.unit, data.unit
)
else:
await self.fetch_unit()
async def fetch_unit(self) -> None:
"""Fetch the unit of measurement."""
with contextlib.suppress(HomeConnectError):
data = await self.coordinator.client.get_status_value(
self.appliance.info.ha_id, status_key=cast(StatusKey, self.bsh_key)
)
if data.unit:
self._attr_native_unit_of_measurement = UNIT_MAP.get(
data.unit, data.unit
)
class HomeConnectProgramSensor(HomeConnectSensor):
"""Sensor class for Home Connect sensors that reports information related to the running program."""

View File

@@ -354,7 +354,7 @@
"options": {
"consumer_products_coffee_maker_enum_type_flow_rate_normal": "Normal",
"consumer_products_coffee_maker_enum_type_flow_rate_intense": "Intense",
"consumer_products_coffee_maker_enum_type_flow_rate_intense_plus": "Intense plus"
"consumer_products_coffee_maker_enum_type_flow_rate_intense_plus": "Intense +"
}
},
"coffee_milk_ratio": {
@@ -410,7 +410,7 @@
"laundry_care_dryer_enum_type_drying_target_iron_dry": "Iron dry",
"laundry_care_dryer_enum_type_drying_target_gentle_dry": "Gentle dry",
"laundry_care_dryer_enum_type_drying_target_cupboard_dry": "Cupboard dry",
"laundry_care_dryer_enum_type_drying_target_cupboard_dry_plus": "Cupboard dry plus",
"laundry_care_dryer_enum_type_drying_target_cupboard_dry_plus": "Cupboard dry +",
"laundry_care_dryer_enum_type_drying_target_extra_dry": "Extra dry"
}
},
@@ -592,7 +592,7 @@
"description": "Defines if the program sequence is optimized with a special drying cycle to ensure more shine on glasses and plastic items."
},
"dishcare_dishwasher_option_vario_speed_plus": {
"name": "Vario speed plus",
"name": "Vario speed +",
"description": "Defines if the program run time is reduced by up to 66% with the usual optimum cleaning and drying."
},
"dishcare_dishwasher_option_silence_on_demand": {
@@ -608,7 +608,7 @@
"description": "Defines if improved drying for glasses and plasticware is enabled."
},
"dishcare_dishwasher_option_hygiene_plus": {
"name": "Hygiene plus",
"name": "Hygiene +",
"description": "Defines if the cleaning is done with increased temperature. This ensures maximum hygienic cleanliness for regular use."
},
"dishcare_dishwasher_option_eco_dry": {
@@ -1462,7 +1462,7 @@
"inactive": "Inactive",
"ready": "Ready",
"delayedstart": "Delayed start",
"run": "Run",
"run": "Running",
"pause": "[%key:common::state::paused%]",
"actionrequired": "Action required",
"finished": "Finished",
@@ -1529,8 +1529,8 @@
"map3": "Map 3"
}
},
"current_cavity_temperature": {
"name": "Current cavity temperature"
"oven_current_cavity_temperature": {
"name": "Current oven cavity temperature"
},
"freezer_door_alarm": {
"name": "Freezer door alarm",

View File

@@ -437,18 +437,21 @@ def ws_expose_entity(
def ws_list_exposed_entities(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Expose an entity to an assistant."""
"""List entities which are exposed to assistants."""
result: dict[str, Any] = {}
exposed_entities = hass.data[DATA_EXPOSED_ENTITIES]
entity_registry = er.async_get(hass)
for entity_id in chain(exposed_entities.entities, entity_registry.entities):
result[entity_id] = {}
exposed_to = {}
entity_settings = async_get_entity_settings(hass, entity_id)
for assistant, settings in entity_settings.items():
if "should_expose" not in settings:
if "should_expose" not in settings or not settings["should_expose"]:
continue
result[entity_id][assistant] = settings["should_expose"]
exposed_to[assistant] = True
if not exposed_to:
continue
result[entity_id] = exposed_to
connection.send_result(msg["id"], {"exposed_entities": result})

View File

@@ -12,7 +12,7 @@
},
"imperial_unit_system": {
"title": "The imperial unit system is deprecated",
"description": "The imperial unit system is deprecated and your system is currently using us customary. Please update your configuration to use the us customary unit system and reload the core configuration to fix this issue."
"description": "The imperial unit system is deprecated and your system is currently using US customary. Please update your configuration to use the US customary unit system and reload the Core configuration to fix this issue."
},
"deprecated_yaml": {
"title": "The {integration_title} YAML configuration is being removed",
@@ -111,8 +111,8 @@
"description": "Checks the Home Assistant YAML-configuration files for errors. Errors will be shown in the Home Assistant logs."
},
"reload_core_config": {
"name": "Reload core configuration",
"description": "Reloads the core configuration from the YAML-configuration."
"name": "Reload Core configuration",
"description": "Reloads the Core configuration from the YAML-configuration."
},
"restart": {
"name": "[%key:common::action::restart%]",
@@ -160,7 +160,7 @@
},
"update_entity": {
"name": "Update entity",
"description": "Forces one or more entities to update its data.",
"description": "Forces one or more entities to update their data.",
"fields": {
"entity_id": {
"name": "Entities to update",

View File

@@ -9,7 +9,7 @@
}
},
"switch": {
"watchdog_on_off": {
"watchdog": {
"default": "mdi:dog"
},
"manual_operation": {

View File

@@ -154,7 +154,6 @@ class HKDevice:
self._pending_subscribes: set[tuple[int, int]] = set()
self._subscribe_timer: CALLBACK_TYPE | None = None
self._load_platforms_lock = asyncio.Lock()
self._full_update_requested: bool = False
@property
def entity_map(self) -> Accessories:
@@ -841,48 +840,11 @@ class HKDevice:
async def async_request_update(self, now: datetime | None = None) -> None:
"""Request an debounced update from the accessory."""
self._full_update_requested = True
await self._debounced_update.async_call()
async def async_update(self, now: datetime | None = None) -> None:
"""Poll state of all entities attached to this bridge/accessory."""
to_poll = self.pollable_characteristics
accessories = self.entity_map.accessories
if (
not self._full_update_requested
and len(accessories) == 1
and self.available
and not (to_poll - self.watchable_characteristics)
and self.pairing.is_available
and await self.pairing.controller.async_reachable(
self.unique_id, timeout=5.0
)
):
# If its a single accessory and all chars are watchable,
# only poll the firmware version to keep the connection alive
# https://github.com/home-assistant/core/issues/123412
#
# Firmware revision is used here since iOS does this to keep camera
# connections alive, and the goal is to not regress
# https://github.com/home-assistant/core/issues/116143
# by polling characteristics that are not normally polled frequently
# and may not be tested by the device vendor.
#
_LOGGER.debug(
"Accessory is reachable, limiting poll to firmware version: %s",
self.unique_id,
)
first_accessory = accessories[0]
accessory_info = first_accessory.services.first(
service_type=ServicesTypes.ACCESSORY_INFORMATION
)
assert accessory_info is not None
firmware_iid = accessory_info[CharacteristicsTypes.FIRMWARE_REVISION].iid
to_poll = {(first_accessory.aid, firmware_iid)}
self._full_update_requested = False
if not to_poll:
self.async_update_available_state()
_LOGGER.debug(

View File

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

View File

@@ -94,7 +94,12 @@ async def async_setup_devices(bridge: HueBridge):
add_device(hue_resource)
# create/update all current devices found in controllers
known_devices = [add_device(hue_device) for hue_device in dev_controller]
# sort the devices to ensure bridges are added first
hue_devices = list(dev_controller)
hue_devices.sort(
key=lambda dev: dev.metadata.archetype != DeviceArchetypes.BRIDGE_V2
)
known_devices = [add_device(hue_device) for hue_device in hue_devices]
known_devices += [add_device(hue_room) for hue_room in api.groups.room]
known_devices += [add_device(hue_zone) for hue_zone in api.groups.zone]

View File

@@ -46,7 +46,7 @@
"services": {
"update": {
"name": "Update",
"description": "Updates iCloud devices.",
"description": "Asks for a state update of all devices linked to an iCloud account.",
"fields": {
"account": {
"name": "Account",

View File

@@ -280,7 +280,7 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]):
if self.custom_event_template is not None:
try:
data["custom"] = self.custom_event_template.async_render(
data, parse_result=True
data | {"text": message.text}, parse_result=True
)
_LOGGER.debug(
"IMAP custom template (%s) for msguid %s (%s) rendered to: %s, initial: %s",

View File

@@ -28,5 +28,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/inkbird",
"iot_class": "local_push",
"requirements": ["inkbird-ble==0.7.0"]
"requirements": ["inkbird-ble==0.7.1"]
}

View File

@@ -111,7 +111,7 @@
},
"services": {
"add_all_link": {
"name": "Add all link",
"name": "Add All-Link",
"description": "Tells the Insteon Modem (IM) start All-Linking mode. Once the IM is in All-Linking mode, press the link button on the device to complete All-Linking.",
"fields": {
"group": {
@@ -120,13 +120,13 @@
},
"mode": {
"name": "[%key:common::config_flow::data::mode%]",
"description": "Linking mode controller - IM is controller responder - IM is responder."
"description": "Linking mode of the Insteon Modem."
}
}
},
"delete_all_link": {
"name": "Delete all link",
"description": "Tells the Insteon Modem (IM) to remove an All-Link record from the All-Link Database of the IM and a device. Once the IM is set to delete the link, press the link button on the corresponding device to complete the process.",
"name": "Delete All-Link",
"description": "Tells the Insteon Modem (IM) to remove an All-Link record from the All-Link database of the IM and a device. Once the IM is set to delete the link, press the link button on the corresponding device to complete the process.",
"fields": {
"group": {
"name": "Group",
@@ -135,8 +135,8 @@
}
},
"load_all_link_database": {
"name": "Load all link database",
"description": "Load the All-Link Database for a device. WARNING - Loading a device All-LInk database is very time consuming and inconsistent. This may take a LONG time and may need to be repeated to obtain all records.",
"name": "Load All-Link database",
"description": "Loads the All-Link database for a device. WARNING - Loading a device All-Link database is very time consuming and inconsistent. This may take a LONG time and may need to be repeated to obtain all records.",
"fields": {
"entity_id": {
"name": "Entity",
@@ -149,8 +149,8 @@
}
},
"print_all_link_database": {
"name": "Print all link database",
"description": "Prints the All-Link Database for a device. Requires that the All-Link Database is loaded into memory.",
"name": "Print All-Link database",
"description": "Prints the All-Link database for a device. Requires that the All-Link database is loaded into memory.",
"fields": {
"entity_id": {
"name": "Entity",
@@ -159,8 +159,8 @@
}
},
"print_im_all_link_database": {
"name": "Print IM all link database",
"description": "Prints the All-Link Database for the INSTEON Modem (IM)."
"name": "Print IM All-Link database",
"description": "Prints the All-Link database for the INSTEON Modem (IM)."
},
"x10_all_units_off": {
"name": "X10 all units off",

View File

@@ -2,13 +2,15 @@
from __future__ import annotations
from collections.abc import Collection
import logging
from typing import Any, Protocol
from aiohttp import web
import voluptuous as vol
from homeassistant.components import http
from homeassistant.components import http, sensor
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.cover import (
ATTR_POSITION,
DOMAIN as COVER_DOMAIN,
@@ -39,7 +41,12 @@ from homeassistant.const import (
SERVICE_TURN_ON,
)
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, State
from homeassistant.helpers import config_validation as cv, integration_platform, intent
from homeassistant.helpers import (
area_registry as ar,
config_validation as cv,
integration_platform,
intent,
)
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
@@ -140,6 +147,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
intent.async_register(hass, GetCurrentDateIntentHandler())
intent.async_register(hass, GetCurrentTimeIntentHandler())
intent.async_register(hass, RespondIntentHandler())
intent.async_register(hass, GetTemperatureIntent())
return True
@@ -444,6 +452,109 @@ class RespondIntentHandler(intent.IntentHandler):
return response
class GetTemperatureIntent(intent.IntentHandler):
"""Handle GetTemperature intents."""
intent_type = intent.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,
vol.Optional("floor"): intent.non_empty_string,
vol.Optional("preferred_area_id"): cv.string,
vol.Optional("preferred_floor_id"): cv.string,
}
platforms = {CLIMATE_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"]
floor_name: str | None = None
if "floor" in slots:
floor_name = slots["floor"]["value"]
match_preferences = intent.MatchTargetsPreferences(
area_id=slots.get("preferred_area_id", {}).get("value"),
floor_id=slots.get("preferred_floor_id", {}).get("value"),
)
if (not name) and (area or match_preferences.area_id):
# Look for temperature sensors assigned to an area
area_registry = ar.async_get(hass)
area_temperature_ids: dict[str, str] = {}
# Keep candidates that are registered as area temperature sensors
def area_candidate_filter(
candidate: intent.MatchTargetsCandidate,
possible_area_ids: Collection[str],
) -> bool:
for area_id in possible_area_ids:
temperature_id = area_temperature_ids.get(area_id)
if (temperature_id is None) and (
area_entry := area_registry.async_get_area(area_id)
):
temperature_id = area_entry.temperature_entity_id or ""
area_temperature_ids[area_id] = temperature_id
if candidate.state.entity_id == temperature_id:
return True
return False
match_constraints = intent.MatchTargetsConstraints(
area_name=area,
floor_name=floor_name,
domains=[sensor.DOMAIN],
device_classes=[sensor.SensorDeviceClass.TEMPERATURE],
assistant=intent_obj.assistant,
single_target=True,
)
match_result = intent.async_match_targets(
hass,
match_constraints,
match_preferences,
area_candidate_filter=area_candidate_filter,
)
if match_result.is_match:
# Found temperature sensor
response = intent_obj.create_response()
response.response_type = intent.IntentResponseType.QUERY_ANSWER
response.async_set_states(matched_states=match_result.states)
return response
# Look for climate devices
match_constraints = intent.MatchTargetsConstraints(
name=name,
area_name=area,
floor_name=floor_name,
domains=[CLIMATE_DOMAIN],
assistant=intent_obj.assistant,
single_target=True,
)
match_result = intent.async_match_targets(
hass, match_constraints, match_preferences
)
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
async def _async_process_intent(
hass: HomeAssistant, domain: str, platform: IntentPlatformProtocol
) -> None:

View File

@@ -13,5 +13,6 @@
"documentation": "https://www.home-assistant.io/integrations/iron_os",
"iot_class": "local_polling",
"loggers": ["pynecil"],
"quality_scale": "platinum",
"requirements": ["pynecil==4.0.1"]
}

View File

@@ -21,8 +21,10 @@ rules:
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: todo
test-before-setup: todo
test-before-configure:
status: exempt
comment: Device is set up from a Bluetooth discovery
test-before-setup: done
unique-config-entry: done
# Silver
@@ -70,7 +72,9 @@ rules:
repair-issues:
status: exempt
comment: no repairs/issues
stale-devices: todo
stale-devices:
status: exempt
comment: Stale devices are removed with the config entry as there is only one device per entry
# Platinum
async-dependency: done

View File

@@ -11,7 +11,6 @@
},
"config_subentries": {
"entity": {
"title": "Add entity",
"step": {
"add_sensor": {
"description": "Configure the new sensor",
@@ -27,7 +26,12 @@
"state": "Initial state"
}
}
}
},
"initiate_flow": {
"user": "Add sensor",
"reconfigure": "Reconfigure sensor"
},
"entry_type": "Sensor"
}
},
"options": {

View File

@@ -28,6 +28,7 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
DATA_COMPONENT: HassKey[EntityComponent[LawnMowerEntity]] = HassKey(DOMAIN)
ENTITY_ID_FORMAT = DOMAIN + ".{}"
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
SCAN_INTERVAL = timedelta(seconds=60)

View File

@@ -47,6 +47,7 @@ PLATFORMS = [
Platform.SENSOR,
Platform.SWITCH,
Platform.VACUUM,
Platform.WATER_HEATER,
]
_LOGGER = logging.getLogger(__name__)

View File

@@ -0,0 +1,201 @@
"""Support for waterheater entities."""
from __future__ import annotations
import logging
from typing import Any
from thinqconnect import DeviceType
from thinqconnect.integration import ExtendedProperty
from homeassistant.components.water_heater import (
ATTR_OPERATION_MODE,
STATE_ECO,
STATE_HEAT_PUMP,
STATE_OFF,
STATE_PERFORMANCE,
WaterHeaterEntity,
WaterHeaterEntityDescription,
WaterHeaterEntityFeature,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import ThinqConfigEntry
from .coordinator import DeviceDataUpdateCoordinator
from .entity import ThinQEntity
DEVICE_TYPE_WH_MAP: dict[DeviceType, WaterHeaterEntityDescription] = {
DeviceType.WATER_HEATER: WaterHeaterEntityDescription(
key=ExtendedProperty.WATER_HEATER,
name=None,
),
DeviceType.SYSTEM_BOILER: WaterHeaterEntityDescription(
key=ExtendedProperty.WATER_BOILER,
name=None,
),
}
# Mapping between device and HA operation modes
DEVICE_OP_MODE_TO_HA = {
"auto": STATE_ECO,
"heat_pump": STATE_HEAT_PUMP,
"turbo": STATE_PERFORMANCE,
"vacation": STATE_OFF,
}
HA_STATE_TO_DEVICE_OP_MODE = {v: k for k, v in DEVICE_OP_MODE_TO_HA.items()}
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ThinqConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up an entry for water_heater platform."""
entities: list[ThinQWaterHeaterEntity] = []
for coordinator in entry.runtime_data.coordinators.values():
if (
description := DEVICE_TYPE_WH_MAP.get(coordinator.api.device.device_type)
) is not None:
if coordinator.api.device.device_type == DeviceType.WATER_HEATER:
entities.append(
ThinQWaterHeaterEntity(
coordinator, description, ExtendedProperty.WATER_HEATER
)
)
elif coordinator.api.device.device_type == DeviceType.SYSTEM_BOILER:
entities.append(
ThinQWaterBoilerEntity(
coordinator, description, ExtendedProperty.WATER_BOILER
)
)
if entities:
async_add_entities(entities)
class ThinQWaterHeaterEntity(ThinQEntity, WaterHeaterEntity):
"""Represent a ThinQ water heater entity."""
def __init__(
self,
coordinator: DeviceDataUpdateCoordinator,
entity_description: WaterHeaterEntityDescription,
property_id: str,
) -> None:
"""Initialize a water_heater entity."""
super().__init__(coordinator, entity_description, property_id)
self._attr_supported_features = (
WaterHeaterEntityFeature.TARGET_TEMPERATURE
| WaterHeaterEntityFeature.OPERATION_MODE
)
self._attr_temperature_unit = (
self._get_unit_of_measurement(self.data.unit) or UnitOfTemperature.CELSIUS
)
if modes := self.data.job_modes:
self._attr_operation_list = [
DEVICE_OP_MODE_TO_HA.get(mode, mode) for mode in modes
]
else:
self._attr_operation_list = [STATE_HEAT_PUMP]
def _update_status(self) -> None:
"""Update status itself."""
super()._update_status()
self._attr_current_temperature = self.data.current_temp
self._attr_target_temperature = self.data.target_temp
if self.data.max is not None:
self._attr_max_temp = self.data.max
if self.data.min is not None:
self._attr_min_temp = self.data.min
if self.data.step is not None:
self._attr_target_temperature_step = self.data.step
self._attr_temperature_unit = (
self._get_unit_of_measurement(self.data.unit) or UnitOfTemperature.CELSIUS
)
if self.data.is_on:
self._attr_current_operation = (
DEVICE_OP_MODE_TO_HA.get(job_mode, job_mode)
if (job_mode := self.data.job_mode) is not None
else STATE_HEAT_PUMP
)
else:
self._attr_current_operation = STATE_OFF
_LOGGER.debug(
"[%s:%s] update status: c:%s, t:%s, op_mode:%s, op_list:%s, is_on:%s",
self.coordinator.device_name,
self.property_id,
self.current_temperature,
self.target_temperature,
self.current_operation,
self.operation_list,
self.data.is_on,
)
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperatures."""
_LOGGER.debug(
"[%s:%s] async_set_temperature: %s",
self.coordinator.device_name,
self.property_id,
kwargs,
)
if (operation_mode := kwargs.get(ATTR_OPERATION_MODE)) is not None:
await self.async_set_operation_mode(str(operation_mode))
if operation_mode == STATE_OFF:
return
if (
temperature := kwargs.get(ATTR_TEMPERATURE)
) is not None and temperature != self.target_temperature:
await self.async_call_api(
self.coordinator.api.async_set_target_temperature(
self.property_id, temperature
)
)
async def async_set_operation_mode(self, operation_mode: str) -> None:
"""Set new operation mode."""
mode = HA_STATE_TO_DEVICE_OP_MODE.get(operation_mode, operation_mode)
_LOGGER.debug(
"[%s:%s] async_set_operation_mode: %s",
self.coordinator.device_name,
self.property_id,
mode,
)
await self.async_call_api(
self.coordinator.api.async_set_job_mode(self.property_id, mode)
)
class ThinQWaterBoilerEntity(ThinQWaterHeaterEntity):
"""Represent a ThinQ water boiler entity."""
def __init__(
self,
coordinator: DeviceDataUpdateCoordinator,
entity_description: WaterHeaterEntityDescription,
property_id: str,
) -> None:
"""Initialize a water_heater entity."""
super().__init__(coordinator, entity_description, property_id)
self._attr_supported_features |= WaterHeaterEntityFeature.ON_OFF
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
_LOGGER.debug(
"[%s:%s] async_turn_on", self.coordinator.device_name, self.property_id
)
await self.async_call_api(self.coordinator.api.async_turn_on(self.property_id))
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
_LOGGER.debug(
"[%s:%s] async_turn_off", self.coordinator.device_name, self.property_id
)
await self.async_call_api(self.coordinator.api.async_turn_off(self.property_id))

View File

@@ -66,7 +66,7 @@
}
},
"set_state": {
"name": "Set State",
"name": "Set state",
"description": "Sets a color/brightness and possibly turn the light on/off.",
"fields": {
"infrared": {
@@ -209,11 +209,11 @@
},
"palette": {
"name": "Palette",
"description": "List of at least 2 and at most 16 colors as hue (0-360), saturation (0-100), brightness (0-100) and kelvin (1500-9000) values to use for this effect. Overrides the theme attribute."
"description": "List of at least 2 and at most 16 colors as hue (0-360), saturation (0-100), brightness (0-100) and Kelvin (1500-9000) values to use for this effect. Overrides the 'Theme' attribute."
},
"theme": {
"name": "[%key:component::lifx::entity::select::theme::name%]",
"description": "Predefined color theme to use for the effect. Overridden by the palette attribute."
"description": "Predefined color theme to use for the effect. Overridden by the 'Palette' attribute."
},
"power_on": {
"name": "Power on",
@@ -243,7 +243,7 @@
},
"palette": {
"name": "Palette",
"description": "List of 1 to 6 colors as hue (0-360), saturation (0-100), brightness (0-100) and kelvin (1500-9000) values to use for this effect."
"description": "List of 1 to 6 colors as hue (0-360), saturation (0-100), brightness (0-100) and Kelvin (1500-9000) values to use for this effect."
},
"power_on": {
"name": "Power on",
@@ -256,16 +256,16 @@
"description": "Stops a running effect."
},
"paint_theme": {
"name": "Paint Theme",
"description": "Paint either a provided theme or custom palette across one or more LIFX lights.",
"name": "Paint theme",
"description": "Paints either a provided theme or custom palette across one or more LIFX lights.",
"fields": {
"palette": {
"name": "Palette",
"description": "List of at least 2 and at most 16 colors as hue (0-360), saturation (0-100), brightness (0-100) and kelvin (1500-9000) values to paint across the target lights. Overrides the theme attribute."
"description": "List of at least 2 and at most 16 colors as hue (0-360), saturation (0-100), brightness (0-100) and Kelvin (1500-9000) values to paint across the target lights. Overrides the 'Theme' attribute."
},
"theme": {
"name": "[%key:component::lifx::entity::select::theme::name%]",
"description": "Predefined color theme to paint. Overridden by the palette attribute."
"description": "Predefined color theme to paint. Overridden by the 'Palette' attribute."
},
"transition": {
"name": "Transition",

View File

@@ -0,0 +1 @@
"""LinkedGo virtual integration."""

View File

@@ -0,0 +1,6 @@
{
"domain": "linkedgo",
"name": "LinkedGo",
"integration_type": "virtual",
"supported_by": "shelly"
}

View File

@@ -53,12 +53,12 @@
},
"services": {
"set_hold_time": {
"name": "Set Hold Time",
"description": "Sets the time to hold until.",
"name": "Set hold time",
"description": "Sets the time period to keep the temperature and override the schedule.",
"fields": {
"time_period": {
"name": "Time Period",
"description": "Time to hold until."
"name": "Time period",
"description": "Duration for which to override the schedule."
}
}
}

View File

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

View File

@@ -17,12 +17,12 @@
},
"media_content_type": {
"name": "Media content type",
"description": "The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST MUSIC."
"description": "The type of the content to play."
}
}
},
"extract_media_url": {
"name": "Get Media URL",
"name": "Get media URL",
"description": "Extract media URL from a service.",
"fields": {
"url": {

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