Compare commits

..

147 Commits

Author SHA1 Message Date
Paulus Schoutsen
b8cd4193a9 Add test command to copilot 2025-06-21 20:32:09 -04:00
Raphael Hehl
a102eaf0cd Bump uiprotect to version 7.14.1 (#147280) 2025-06-22 02:14:26 +02:00
Shai Ungar
f3533dff44 Bump pyseventeentrack to 1.1.1 (#147253)
Update pyseventeentrack requirement to version 1.1.1
2025-06-21 22:50:53 +01:00
Simone Chemelli
c453eed32d Bump aioamazondevices to 3.1.14 (#147257) 2025-06-21 15:44:22 +02:00
Joakim Sørensen
79a9f34150 Handle the new JSON payload from traccar clients (#147254) 2025-06-21 11:53:17 +02:00
hanwg
7442f7af28 Fix Telegram bot parsing of inline keyboard (#146376)
* bug fix for inline keyboard

* update inline keyboard test

* Update tests/components/telegram_bot/test_telegram_bot.py

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

* revert last_message_id and updated tests

* removed TypeError test

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-06-21 03:21:10 +02:00
Markus Adrario
2e5de732a7 Bump pyHomee to version 1.2.10 (#147248)
bump pyHomee to version 1.2.10
2025-06-21 00:32:14 +01:00
Michael Hansen
9bcd74c449 Change async_supports_streaming_input to an instance method (#147245) 2025-06-20 15:39:22 -05:00
Noah Husby
ace18e540b Bump aiorussound to 4.6.1 (#147233) 2025-06-20 21:59:59 +02:00
Michael Hansen
65f897793d Use string instead of boolean for voice event (#147244)
Use string instead of bool
2025-06-20 15:18:03 -04:00
Robert Resch
435c08685d Bump deebot-client to 13.4.0 (#147221) 2025-06-20 20:22:33 +02:00
J. Diego Rodríguez Royo
95f292c43d Bump aiohomeconnect to 0.18.1 (#147236) 2025-06-20 19:27:29 +02:00
Manu
9346c584c3 Add reconfigure flow to ntfy integration (#143743) 2025-06-20 18:42:47 +02:00
Michael Hansen
6738085391 Minor clean up missed in previous PR (#147229) 2025-06-20 10:54:11 -05:00
Markus Adrario
d9e5bad55e Use entity name in homee (#147142)
* add name to HomeeEntity

* review change
2025-06-20 16:55:48 +02:00
Maciej Bieniek
f7429f3431 Fix Shelly entity names for gen1 sleeping devices (#147019) 2025-06-20 15:19:39 +02:00
Petar Petrov
46aea5d9dc Bump zwave-js-server-python to 0.64.0 (#147176) 2025-06-20 14:59:54 +02:00
Paulus Schoutsen
33bde48c9c AI Task integration (#145128)
* Add AI Task integration

* Remove GenTextTaskType

* Add AI Task prefs

* Add action to LLM task

* Remove WS command

* Rename result to text for GenTextTaskResult

* Apply suggestions from code review

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

* Add supported feature for generate text

* Update const.py

Co-authored-by: HarvsG <11440490+HarvsG@users.noreply.github.com>

* Update homeassistant/components/ai_task/services.yaml

Co-authored-by: HarvsG <11440490+HarvsG@users.noreply.github.com>

* Use WS API to set preferences

* Simplify pref storage

* Simplify pref test

* Update homeassistant/components/ai_task/services.yaml

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

---------

Co-authored-by: Allen Porter <allen.porter@gmail.com>
Co-authored-by: HarvsG <11440490+HarvsG@users.noreply.github.com>
2025-06-20 08:56:08 -04:00
Noah Husby
1b73acc025 Add sub-device support to Russound RIO (#146763) 2025-06-20 14:52:34 +02:00
Guido Schmitz
e28965770e Add translations for devolo Home Control exceptions (#147099)
* Add translations for devolo Home Control exceptions

* Adapt invalid_auth message

* Adapt connection_failed message
2025-06-20 14:31:16 +02:00
Kevin Stillhammer
f9d4bde0f6 Bump here-routing to 1.2.0 (#147204)
* Bump here-routing to 1.2.0

* Fix mypy typing errors

* Correct types for call assertion
2025-06-20 13:44:14 +02:00
Duco Sebel
a493bdc208 Implement battery group mode in HomeWizard (#146770)
* Implement battery group mode for HomeWizard P1

* Clean up test

* Disable 'entity_registry_enabled_default'

* Fix failing tests because of 'entity_registry_enabled_default'

* Proof entities are disabled by default

* Undo dev change

* Update homeassistant/components/homewizard/select.py

* Update homeassistant/components/homewizard/select.py

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

* Update homeassistant/components/homewizard/strings.json

* Apply suggestions from code review

* Update tests due to updated translations

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-06-20 13:19:45 +02:00
Markus Adrario
9ae9ad1e43 Improve test-coverage for homee locks (#147160)
test for unknown user
2025-06-20 12:28:49 +02:00
epenet
1b60ea8951 Migrate lutron to use runtime_data (#147198) 2025-06-20 12:26:07 +02:00
epenet
313eaff14e Migrate kaleidescape to use runtime_data (#147171)
* Migrate kaleidescape to use runtime_data

* Adjust tests
2025-06-20 12:25:57 +02:00
epenet
7dfd68f8c0 Migrate keenetic_ndms2 to use runtime_data (#147194)
* Migrate keenetic_ndms2 to use runtime_data

* Adjust tests
2025-06-20 12:23:59 +02:00
epenet
544fd2a4a6 Migrate lacrosse_view to use runtime_data (#147202) 2025-06-20 12:23:29 +02:00
epenet
cd51070219 Migrate kmtronic to use runtime_data (#147193) 2025-06-20 11:39:13 +02:00
Marc Mueller
3c91c78383 Use PEP 695 TypeVar syntax for ecovacs (#147153) 2025-06-20 10:41:25 +02:00
Brett Adams
96e0d1f5c6 Fix Charge Cable binary sensor in Teslemetry (#147136) 2025-06-20 10:39:43 +02:00
epenet
2859e7de9b Migrate kodi to use runtime_data (#147191) 2025-06-20 10:38:01 +02:00
Robert Resch
88683a318d Add support of taking a camera snapshot via go2rtc (#145205) 2025-06-20 10:34:43 +02:00
epenet
84e9422254 Move juicenet coordinator to separate module (#147168) 2025-06-20 10:33:17 +02:00
epenet
fde36d5034 Simplify update_listener in konnected (#147172) 2025-06-20 10:31:28 +02:00
Andre Lengwenus
8c1e43c07c Bump pypck to 0.8.9 (#147174) 2025-06-20 10:28:35 +02:00
epenet
05343392a7 Simplify update_listener in keenetic_ndms2 (#147173) 2025-06-20 10:27:47 +02:00
epenet
32314dbb13 Simplify update_listener in kmtronic (#147184) 2025-06-20 10:27:07 +02:00
epenet
8f661fc5cf Migrate kegtron to use runtime_data (#147177) 2025-06-20 10:26:53 +02:00
epenet
e315cb9859 Migrate kostal_plenticore to use runtime_data (#147188) 2025-06-20 10:25:08 +02:00
epenet
d0e77eb1e2 Migrate keymitt_ble to use runtime_data (#147179) 2025-06-20 10:24:56 +02:00
epenet
e23cac8bef Simplify remove listener in kodi (#147183) 2025-06-20 10:23:41 +02:00
epenet
973700542b Move kmtronic coordinator to separate module (#147182) 2025-06-20 10:19:19 +02:00
Krisjanis Lejejs
2e21493c19 Bump hass-nabucasa from 0.102.0 to 0.103.0 (#147186) 2025-06-20 10:18:03 +02:00
Markus Adrario
73bed96a0f remove unwanted attribute in homee sensor tests (#147158) 2025-06-20 08:11:20 +02:00
Markus Adrario
0a5d13f104 fix and improve cover tests for homee (#147164) 2025-06-20 08:10:44 +02:00
epenet
d16ec81727 Migrate justnimbus to use runtime_data (#147170) 2025-06-20 08:10:06 +02:00
Martin Hjelmare
11564e3df5 Fix Z-Wave device class endpoint discovery (#142171)
* Add test fixture and test for Glass 9 shutter

* Fix zwave_js device class discovery matcher

* Fall back to node device class

* Fix test_special_meters modifying node state

* Handle value added after node ready
2025-06-20 08:56:20 +03:00
Michael Hansen
341d9f15f0 Add ask_question action to Assist satellite (#145233)
* Add get_response to Assist satellite and ESPHome

* Rename get_response to ask_question

* Add possible answers to questions

* Add wildcard support and entity test

* Add ESPHome test

* Refactor to remove async_ask_question

* Use single entity_id instead of target

* Fix error message

* Remove ESPHome test

* Clean up

* Revert fix
2025-06-19 16:50:14 -05:00
Marc Mueller
2c13c70e12 Update ruff to 0.12.0 (#147106) 2025-06-19 20:39:09 +02:00
Marc Mueller
73d0d87705 Use PEP 695 TypeVar syntax for nextdns (#147155) 2025-06-19 20:26:07 +02:00
Marc Mueller
b8dfb2c850 Use PEP 695 TypeVar syntax for eheimdigital (#147154) 2025-06-19 20:25:45 +02:00
Marc Mueller
cf67a68454 Use PEP 695 TypeVar syntax for paperless_ngx (#147156) 2025-06-19 20:24:51 +02:00
karwosts
b003429912 Expose statistics selector, use for recorder.get_statistics (#147056)
* Expose statistics selector, use for `recorder.get_statistics`

* code review

* syntax formatting

* rerun ci
2025-06-19 20:04:28 +02:00
hahn-th
4aff032442 Bump homematicip to 2.0.6 (#147151) 2025-06-19 18:55:14 +02:00
Martin Hjelmare
da3d8a6332 Improve advanced Z-Wave battery discovery (#147127) 2025-06-19 18:56:47 +03:00
Marc Mueller
7a5c088149 [ci] Bump cache key version (#147148) 2025-06-19 17:42:30 +02:00
Norbert Rittel
31eec6f471 Add missing hyphen to "mains-powered" and "battery-powered" in zha (#147128)
Add missing hyphen to "mains-powered" and "battery-powered"
2025-06-19 14:36:40 +03:00
G Johansson
c602a0e279 Deprecated hass.http.register_static_path now raises error (#147039) 2025-06-19 13:14:42 +02:00
Marc Mueller
513045e489 Update pytest warnings filter (#147132) 2025-06-19 13:07:42 +02:00
Erik Montnemery
0db6520802 Add comment in helpers.llm.ActionTool explaining limitations (#147116) 2025-06-19 12:59:35 +02:00
Erik Montnemery
5bc2e271d2 Re-raise annotated_yaml.YAMLException as HomeAssistantError (#147129)
* Re-raise annotated_yaml.YAMLException as HomeAssistantError

* Fix comment
2025-06-19 12:52:01 +02:00
G Johansson
77dca49c75 Fix pylint plugin for vacuum entity (#146467)
* Clean out legacy VacuumEntity from pylint plugins

* Fix

* Fix pylint for vacuum

* More fixes

* Revert partial

* Add back state
2025-06-19 12:49:10 +02:00
Franck Nijhof
1baba8b880 Adjust feature request links in issue reporting (#147130) 2025-06-19 12:36:43 +02:00
Markus Adrario
875d81cab2 update pyHomee to v1.2.9 (#147094) 2025-06-19 12:04:59 +02:00
Raphael Hehl
956f726ef3 Bump uiprotect to version 7.14.0 (#147102) 2025-06-19 11:20:29 +02:00
epenet
fada81e1ce Bump ovoenergy to 2.0.1 (#147112) 2025-06-19 08:46:03 +02:00
Simon Lamon
6a16424bb4 Fix nightly build (#147110)
Update builder.yml
2025-06-19 08:20:19 +02:00
Abílio Costa
f90a740429 Use non-autospec mock for Reolink's binary_sensor, camera and diag tests (#147095) 2025-06-19 08:03:48 +02:00
Michael Hansen
3dba7e5bd2 Send intent progress events to ESPHome (#146966) 2025-06-18 22:12:37 -04:00
Erik Montnemery
8d8ff011fc Minor improvements of service helper (#147079) 2025-06-19 00:17:12 +01:00
Michael Hansen
6befd065a1 Bump aioesphomeapi to 32.2.4 (#147100)
Bump aioesphomeapi
2025-06-18 15:49:44 -05:00
Abílio Costa
9adf493acd Use non-autospec mock for Reolink's init tests (#146991) 2025-06-18 17:58:50 +01:00
Michael Hansen
a29d5fb56c tts_output is optional in run-start (#147092) 2025-06-18 12:08:53 -04:00
Petro31
bcb87cf812 Support variables, icon, and picture for all compatible template platforms (#145893)
* Fix template entity variables in blueprints

* add picture and icon tests

* add variable test for all platforms

* apply comments

* Update all test names
2025-06-18 16:49:46 +02:00
Jan Bouwhuis
d01758cea8 Ensure mqtt sensor has a valid native unit of measurement (#146722) 2025-06-18 15:48:38 +02:00
Joakim Sørensen
5487bfe1d9 Bump hass-nabucasa from 0.101.0 to 0.102.0 (#147087) 2025-06-18 15:47:01 +02:00
Simone Chemelli
fec65f40fc Bump aioamazondevices to 3.1.12 (#147055)
* Bump aioamazondevices to 3.1.10

* bump to 3.1.12
2025-06-18 10:20:51 +02:00
Guido Schmitz
596951ea9f Cleanup devolo Home Control tests (#147051) 2025-06-18 09:24:09 +02:00
Norbert Rittel
75d6b885cf Fix typo in state name references of homee (#146905)
Fix typo in state references

Replace wrong semicolons with colon.
2025-06-18 09:23:37 +02:00
Guido Schmitz
3fad76dfa1 Use missed typed ConfigEntry in devolo Home Control (#147049) 2025-06-18 09:22:37 +02:00
Pete Sage
43d8a151ab Remove internals from Sonos test_init.py (#147063)
* fix: test init

* fix: revert

* fix: revert

* fix: revert

* fix: revert

* fix: simplify
2025-06-18 09:21:21 +02:00
starkillerOG
07110e288d If no Reolink HTTP api available, do not set configuration_url (#146684)
* If no http api available, do not set configuration_url

* Add tests
2025-06-18 09:16:08 +02:00
Jan-Philipp Benecke
ba2aac4614 Bump aiowebdav2 to 0.4.6 (#147054) 2025-06-18 09:15:27 +02:00
msw
3449dae7a2 Capitalize "Ice Bites" and switch to "Cubed ice" (#147060) (#147061) 2025-06-18 09:14:45 +02:00
G Johansson
b8cd3f3635 Bump holidays lib to 0.75 (#147043) 2025-06-18 10:11:01 +03:00
Martin Hjelmare
be53ad5449 Disable Z-Wave idle notification button (#147026)
* Update test

* Disable Z-Wave idle notification button

* Update tests
2025-06-18 08:29:04 +03:00
J. Diego Rodríguez Royo
ffd940e07c Set quality scale at Home Connect manifest (#147050) 2025-06-17 21:42:40 +01:00
Josef Zweck
5e31b5ac4f Handle missing widget in lamarzocco (#147047) 2025-06-17 21:25:27 +02:00
puddly
81257f9d57 Bump ZHA to 0.0.60 (#147045) 2025-06-17 22:06:53 +03:00
Josef Zweck
ce1678719a Bump pylamarzocco to 2.0.9 (#147046) 2025-06-17 20:59:41 +02:00
Guido Schmitz
fc6844b3c9 Add _attr_has_entity_name to devolo Home Network device tracker platform (#146978)
* Add _attr_has_entity_name to devolo Home Network device tracker platform

* Set name

* Fix tests
2025-06-17 20:49:52 +02:00
J. Diego Rodríguez Royo
8e82e3aa3a Bump aiohomeconnect to 0.18.0 (#147044) 2025-06-17 20:48:09 +02:00
G Johansson
3bc68941e6 Remove not used constant in climate (#147041) 2025-06-17 20:43:16 +02:00
Josef Zweck
e69b38ab2c Fix log in onedrive (#147029) 2025-06-17 19:57:52 +02:00
Abílio Costa
ed9503324d Fix flaky Reolink webhook test (#147036) 2025-06-17 17:18:48 +01:00
Allen Porter
22a06a6c2e Bump ical to 10.0.4 (#147005)
* Bump ical to 10.0.4

* Bump ical to 10.0.4 in google
2025-06-17 07:06:51 -07:00
Michael Hansen
3b611b9b03 Add TTS response timeout for idle state (#146984)
* Add TTS response timeout for idle state

* Consider time spent sending TTS audio in timeout
2025-06-17 09:39:18 -04:00
Noah Husby
79cc3bffc6 Bump aiorussound to 4.6.0 (#147023) 2025-06-17 14:40:56 +02:00
Martin Hjelmare
5c455304a5 Disable Z-Wave indidator CC entities by default (#147018)
* Update discovery tests

* Disable Z-Wave indidator CC entities by default
2025-06-17 15:39:22 +03:00
Erik Montnemery
058f860be7 Fix incorrect use of zip in service.async_get_all_descriptions (#147013)
* Fix incorrect use of zip in service.async_get_all_descriptions

* Fix lint errors in test
2025-06-17 14:24:31 +02:00
Joost Lekkerkerker
ef319c966d Bump nextcord to 3.1.0 (#147020) 2025-06-17 14:11:55 +02:00
Robin Lintermann
adc4e9fdc1 Bump pysmarlaapi version to 0.9.0 (#146629)
Bump pysmarlaapi version
Fix default values of entities
2025-06-17 11:23:50 +02:00
Maciej Bieniek
40a00fb790 Address late review for NextDNS integration (#146980)
key instead of Key
2025-06-17 11:23:03 +02:00
G Johansson
0926b16095 Remove deprecated support feature values in cover (#146987) 2025-06-17 10:46:08 +02:00
G Johansson
308c89af4a Remove deprecated support feature values in media_player (#146986) 2025-06-17 10:33:41 +02:00
G Johansson
b0c2a47288 Remove deprecated support feature values in vacuum (#146982) 2025-06-17 10:32:58 +02:00
Joost Lekkerkerker
c446cce2cc Bump pySmartThings to 3.2.5 (#146983) 2025-06-16 22:44:14 +01:00
Abílio Costa
e02267ad89 Improve bootstrap file logging test (#146670) 2025-06-16 21:55:16 +01:00
Thomas55555
36381e6753 Bump aioautomower to 2025.6.0 (#146979) 2025-06-16 22:52:23 +02:00
Manu
6533562f4e Rename Xiaomi Miio integration to Xiaomi Home (#146555)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-06-16 21:51:54 +01:00
Ludovic BOUÉ
1bc6ea98ce Set Matter SolarPower tagList in fixture (#146837)
Update solar_power.json

Set tagList to [{"0":null,"1":15,"2":2,"3":"Solar"}]
2025-06-16 22:46:27 +02:00
elmurato
bab34b844b Fix blocking open in Minecraft Server (#146820)
Fix blocking open by dnspython
2025-06-16 22:46:11 +02:00
Etienne C.
ad3dac0373 Removed rounding of durations in Here Travel Time sensors (#146838)
* Removed rounding of durations

* Set duration sensors unit to seconds

* Updated Here Travel Time tests

* Update homeassistant/components/here_travel_time/sensor.py

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

* Update homeassistant/components/here_travel_time/sensor.py

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

* Updated Here Travel Time tests

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-06-16 22:20:01 +02:00
Maciej Bieniek
c5d93e5456 Fix translation key in NextDNS integration (#146976)
* Fix translation key

* Better wording
2025-06-16 21:37:19 +02:00
J. Diego Rodríguez Royo
ef9b46dce5 Record current IQS state for Home Connect (#131703)
* Home Connect quality scale

* Update current iqs

* Docs rules done

* parallel-updates rule

* Complete appropriate-polling's comment

* Apply suggestions

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

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-06-16 21:30:06 +02:00
Abílio Costa
6f3ceb83c2 Use non-autospec mock for Reolink's button tests (#146969) 2025-06-16 21:14:02 +02:00
Joost Lekkerkerker
589577a04c Add diagnostics support to Meater (#146967) 2025-06-16 20:17:30 +02:00
Joost Lekkerkerker
cb21bb6542 Make Meater cook state an enum (#146958) 2025-06-16 19:13:34 +01:00
mswilson
ad64139b8e Add switch for Samsung ice bites (and rename ice maker) (#146925)
* Add switch for ice bites (and rename ice maker)

Fixes: home-assistant/home-assistant.io#37826

* Fix tests

* Fix

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
2025-06-16 19:31:49 +02:00
Joost Lekkerkerker
9ae0cfc7e5 Create entities directly on setup in Meater (#146953)
* Don't wait an update when adding devices in Meater

* Fix
2025-06-16 18:23:20 +02:00
Joost Lekkerkerker
dffaf49eca Use runtime data in Meater (#146961) 2025-06-16 17:18:21 +02:00
Maciej Bieniek
4add783108 Use entity base class for NextDNS entities (#146934)
* Add entity module

* Add NextDnsEntityDescription class

* Remove NextDnsEntityDescription

* Create DeviceInfo in entity module

* Use property
2025-06-16 16:58:47 +02:00
Joost Lekkerkerker
421251308f Add Meater sensor tests (#146952) 2025-06-16 16:19:35 +02:00
Aviad Levy
cce878213f Add Telegram Bot message reactions (#146354) 2025-06-16 14:48:59 +01:00
Joost Lekkerkerker
664441eaec Improve Meater config flow tests (#146951) 2025-06-16 15:40:43 +02:00
Maciej Bieniek
d4686a3cce Add config flow data description for NextDNS (#146938)
* Add config flow data description

* Better wording
2025-06-16 15:28:25 +02:00
Hessel
6e92247799 Fix missing key for ecosmart in older Wallbox models (#146847)
* fix 146839, missing key

* added tests for this issue

* added tests for this issue

* added tests for this issue, formatting

* Prevent loading select on missing key

* Prevent loading select on missing key - formatting fixed

* Update homeassistant/components/wallbox/coordinator.py

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

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-06-16 15:15:17 +02:00
Etienne C.
f5355c833e Add duration device class in Here Travel Time sensors (#146804) 2025-06-16 15:14:43 +02:00
Joost Lekkerkerker
add9f4c5ab Move Meater coordinator to module (#146946)
* Move Meater coordinator to module

* Fix tests
2025-06-16 14:48:44 +02:00
starkillerOG
38973fe64a Add Reolink privacy mask switch (#146906) 2025-06-16 14:40:19 +02:00
epenet
d657964729 Simplify habitica service actions (#146746) 2025-06-16 14:37:38 +02:00
Nathan Spencer
25c408484c Set goalzero total run time sensor device class to duration (#146897) 2025-06-16 14:35:56 +02:00
Florian von Garrel
c335b5b37c Add verify ssl option to paperless-ngx integration (#146802)
* add verify ssl config option

* Refactoring

* Use .get() with default value instead of migration

* Reconfigure fix

* minor changes
2025-06-16 14:31:22 +02:00
Josef Zweck
61b00892c3 Add debug log for update in onedrive (#146907) 2025-06-16 14:17:36 +02:00
Maciej Bieniek
e47e2c92fe Change PARALLEL_UPDATES to 0 for read-only NextDNS platforms (#146939)
Change PARALLEL_UPDATES to 0 for read-only platforms
2025-06-16 14:11:48 +02:00
Duco Sebel
3283965b45 Re-enable v2 API support for HomeWizard P1 Meter (#146927) 2025-06-16 14:11:35 +02:00
epenet
4a9cbc79f2 Bump pysml to 0.1.5 (#146935) 2025-06-16 12:56:03 +01:00
epenet
33978ce59e Bump pyosoenergyapi to 1.1.5 (#146942) 2025-06-16 12:46:38 +01:00
epenet
d5262231a1 Bump pymysensors to 0.25.0 (#146941) 2025-06-16 13:37:39 +02:00
Brett Adams
b563f9078a Significantly improve Tesla Fleet config flow (#146794)
* Improved config flow

* Tests

* Improvements

* Dashboard url & tests

* Apply suggestions from code review

Co-authored-by: Norbert Rittel <norbert@rittel.de>

* revert oauth change

* fully restore oauth file

* remove CONF_DOMAIN

* Add pick_implementation back in

* Use try else

* Improve translation

* use CONF_DOMAIN

---------

Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-06-16 13:29:17 +02:00
epenet
e8667dfbe0 Bump nessclient to 1.2.0 (#146937) 2025-06-16 12:11:57 +01:00
dependabot[bot]
8d4f5d78ff Bump dawidd6/action-download-artifact from 10 to 11 (#146928)
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 10 to 11.
- [Release notes](https://github.com/dawidd6/action-download-artifact/releases)
- [Commits](https://github.com/dawidd6/action-download-artifact/compare/v10...v11)

---
updated-dependencies:
- dependency-name: dawidd6/action-download-artifact
  dependency-version: '11'
  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-06-16 10:42:10 +02:00
mbo18
e354a850c9 Bump python-rflink to 0.0.67 (#146908)
* update python-rflink

* remove from FORBIDDEN_PACKAGE_EXCEPTIONS
2025-06-16 10:36:20 +02:00
Ernst Klamer
5ea026d369 Bump bthome-ble to 3.13.1 (#146871) 2025-06-16 11:29:00 +03:00
Brett Adams
ddfe17d0a4 Bump tesla-fleet-api to match Protobuf compatibility (#146918)
Bump for v1.2.0
2025-06-16 10:12:34 +02:00
Yuxin Wang
85aa7bef1e Add sensor categorizations for APCUPSD (#146863)
* Add sensor categorizations

* Fix snapshot problem

* Fix snapshot problem
2025-06-16 08:43:31 +02:00
Paulus Schoutsen
8498928e47 Move Google Gen AI fixture to allow reuse (#146921) 2025-06-15 23:00:27 -04:00
464 changed files with 24697 additions and 4204 deletions

View File

@@ -1,15 +1,14 @@
name: Report an issue with Home Assistant Core
description: Report an issue with Home Assistant Core.
type: Bug
body:
- type: markdown
attributes:
value: |
This issue form is for reporting bugs only!
If you have a feature or enhancement request, please use the [feature request][fr] section of our [Community Forum][fr].
If you have a feature or enhancement request, please [request them here instead][fr].
[fr]: https://community.home-assistant.io/c/feature-requests
[fr]: https://github.com/orgs/home-assistant/discussions
- type: textarea
validations:
required: true

View File

@@ -10,8 +10,8 @@ contact_links:
url: https://www.home-assistant.io/help
about: We use GitHub for tracking bugs, check our website for resources on getting help.
- name: Feature Request
url: https://community.home-assistant.io/c/feature-requests
about: Please use our Community Forum for making feature requests.
url: https://github.com/orgs/home-assistant/discussions
about: Please use this link to request new features or enhancements to existing features.
- name: I'm unsure where to go
url: https://www.home-assistant.io/join-chat
about: If you are unsure where to go, then joining our chat is recommended; Just ask!

View File

@@ -94,6 +94,7 @@ automation application.
- Provide descriptive state attributes
- Testing:
- Test location: `tests/components/{domain}/`
- Run tests with `pytest --no-header --no-summary -x <location>`
- Use pytest fixtures from `tests.common`
- Mock external dependencies
- Use snapshots for complex data

View File

@@ -94,7 +94,7 @@ jobs:
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@v10
uses: dawidd6/action-download-artifact@v11
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/frontend
@@ -105,10 +105,10 @@ jobs:
- name: Download nightly wheels of intents
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@v10
uses: dawidd6/action-download-artifact@v11
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/intents-package
repo: OHF-Voice/intents-package
branch: main
workflow: nightly.yaml
workflow_conclusion: success

View File

@@ -37,7 +37,7 @@ on:
type: boolean
env:
CACHE_VERSION: 2
CACHE_VERSION: 3
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.7"

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.12
rev: v0.12.0
hooks:
- id: ruff-check
args:

View File

@@ -38,8 +38,7 @@ def validate_python() -> None:
def ensure_config_path(config_dir: str) -> None:
"""Validate the configuration directory."""
# pylint: disable-next=import-outside-toplevel
from . import config as config_util
from . import config as config_util # noqa: PLC0415
lib_dir = os.path.join(config_dir, "deps")
@@ -80,8 +79,7 @@ def ensure_config_path(config_dir: str) -> None:
def get_arguments() -> argparse.Namespace:
"""Get parsed passed in arguments."""
# pylint: disable-next=import-outside-toplevel
from . import config as config_util
from . import config as config_util # noqa: PLC0415
parser = argparse.ArgumentParser(
description="Home Assistant: Observe, Control, Automate.",
@@ -177,8 +175,7 @@ def main() -> int:
validate_os()
if args.script is not None:
# pylint: disable-next=import-outside-toplevel
from . import scripts
from . import scripts # noqa: PLC0415
return scripts.run(args.script)
@@ -188,8 +185,7 @@ def main() -> int:
ensure_config_path(config_dir)
# pylint: disable-next=import-outside-toplevel
from . import config, runner
from . import config, runner # noqa: PLC0415
safe_mode = config.safe_mode_enabled(config_dir)

View File

@@ -52,28 +52,28 @@ _LOGGER = logging.getLogger(__name__)
def _generate_secret() -> str:
"""Generate a secret."""
import pyotp # pylint: disable=import-outside-toplevel
import pyotp # noqa: PLC0415
return str(pyotp.random_base32())
def _generate_random() -> int:
"""Generate a 32 digit number."""
import pyotp # pylint: disable=import-outside-toplevel
import pyotp # noqa: PLC0415
return int(pyotp.random_base32(length=32, chars=list("1234567890")))
def _generate_otp(secret: str, count: int) -> str:
"""Generate one time password."""
import pyotp # pylint: disable=import-outside-toplevel
import pyotp # noqa: PLC0415
return str(pyotp.HOTP(secret).at(count))
def _verify_otp(secret: str, otp: str, count: int) -> bool:
"""Verify one time password."""
import pyotp # pylint: disable=import-outside-toplevel
import pyotp # noqa: PLC0415
return bool(pyotp.HOTP(secret).verify(otp, count))

View File

@@ -37,7 +37,7 @@ DUMMY_SECRET = "FPPTH34D4E3MI2HG"
def _generate_qr_code(data: str) -> str:
"""Generate a base64 PNG string represent QR Code image of data."""
import pyqrcode # pylint: disable=import-outside-toplevel
import pyqrcode # noqa: PLC0415
qr_code = pyqrcode.create(data)
@@ -59,7 +59,7 @@ def _generate_qr_code(data: str) -> str:
def _generate_secret_and_qr_code(username: str) -> tuple[str, str, str]:
"""Generate a secret, url, and QR code."""
import pyotp # pylint: disable=import-outside-toplevel
import pyotp # noqa: PLC0415
ota_secret = pyotp.random_base32()
url = pyotp.totp.TOTP(ota_secret).provisioning_uri(
@@ -107,7 +107,7 @@ class TotpAuthModule(MultiFactorAuthModule):
def _add_ota_secret(self, user_id: str, secret: str | None = None) -> str:
"""Create a ota_secret for user."""
import pyotp # pylint: disable=import-outside-toplevel
import pyotp # noqa: PLC0415
ota_secret: str = secret or pyotp.random_base32()
@@ -163,7 +163,7 @@ class TotpAuthModule(MultiFactorAuthModule):
def _validate_2fa(self, user_id: str, code: str) -> bool:
"""Validate two factor authentication code."""
import pyotp # pylint: disable=import-outside-toplevel
import pyotp # noqa: PLC0415
if (ota_secret := self._users.get(user_id)) is None: # type: ignore[union-attr]
# even we cannot find user, we still do verify
@@ -196,7 +196,7 @@ class TotpSetupFlow(SetupFlow[TotpAuthModule]):
Return self.async_show_form(step_id='init') if user_input is None.
Return self.async_create_entry(data={'result': result}) if finish.
"""
import pyotp # pylint: disable=import-outside-toplevel
import pyotp # noqa: PLC0415
errors: dict[str, str] = {}

View File

@@ -394,7 +394,7 @@ async def async_setup_hass(
def open_hass_ui(hass: core.HomeAssistant) -> None:
"""Open the UI."""
import webbrowser # pylint: disable=import-outside-toplevel
import webbrowser # noqa: PLC0415
if hass.config.api is None or "frontend" not in hass.config.components:
_LOGGER.warning("Cannot launch the UI because frontend not loaded")
@@ -561,8 +561,7 @@ async def async_enable_logging(
if not log_no_color:
try:
# pylint: disable-next=import-outside-toplevel
from colorlog import ColoredFormatter
from colorlog import ColoredFormatter # noqa: PLC0415
# basicConfig must be called after importing colorlog in order to
# ensure that the handlers it sets up wraps the correct streams.
@@ -606,7 +605,7 @@ async def async_enable_logging(
)
threading.excepthook = lambda args: logging.getLogger().exception(
"Uncaught thread exception",
exc_info=( # type: ignore[arg-type]
exc_info=( # type: ignore[arg-type] # noqa: LOG014
args.exc_type,
args.exc_value,
args.exc_traceback,
@@ -1060,5 +1059,5 @@ async def _async_setup_multi_components(
_LOGGER.error(
"Error setting up integration %s - received exception",
domain,
exc_info=(type(result), result, result.__traceback__),
exc_info=(type(result), result, result.__traceback__), # noqa: LOG014
)

View File

@@ -2,13 +2,22 @@
import logging
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import (
HassJobType,
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
callback,
)
from homeassistant.helpers import config_validation as cv, storage
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
from .const import DATA_COMPONENT, DATA_PREFERENCES, DOMAIN
from .const import DATA_COMPONENT, DATA_PREFERENCES, DOMAIN, AITaskEntityFeature
from .entity import AITaskEntity
from .http import async_setup as async_setup_conversation_http
from .task import GenTextTask, GenTextTaskResult, async_generate_text
@@ -16,6 +25,7 @@ from .task import GenTextTask, GenTextTaskResult, async_generate_text
__all__ = [
"DOMAIN",
"AITaskEntity",
"AITaskEntityFeature",
"GenTextTask",
"GenTextTaskResult",
"async_generate_text",
@@ -36,6 +46,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass.data[DATA_PREFERENCES] = AITaskPreferences(hass)
await hass.data[DATA_PREFERENCES].async_load()
async_setup_conversation_http(hass)
hass.services.async_register(
DOMAIN,
"generate_text",
async_service_generate_text,
schema=vol.Schema(
{
vol.Required("task_name"): cv.string,
vol.Optional("entity_id"): cv.entity_id,
vol.Required("instructions"): cv.string,
}
),
supports_response=SupportsResponse.ONLY,
job_type=HassJobType.Coroutinefunction,
)
return True
@@ -49,9 +73,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
async def async_service_generate_text(call: ServiceCall) -> ServiceResponse:
"""Run the run task service."""
result = await async_generate_text(hass=call.hass, **call.data)
return result.as_dict() # type: ignore[return-value]
class AITaskPreferences:
"""AI Task preferences."""
KEYS = ("gen_text_entity_id",)
gen_text_entity_id: str | None = None
def __init__(self, hass: HomeAssistant) -> None:
@@ -65,7 +97,8 @@ class AITaskPreferences:
data = await self._store.async_load()
if data is None:
return
self.gen_text_entity_id = data.get("gen_text_entity_id")
for key in self.KEYS:
setattr(self, key, data[key])
@callback
def async_set_preferences(
@@ -84,16 +117,9 @@ class AITaskPreferences:
if not changed:
return
self._store.async_delay_save(
lambda: {
"gen_text_entity_id": self.gen_text_entity_id,
},
10,
)
self._store.async_delay_save(self.as_dict, 10)
@callback
def as_dict(self) -> dict[str, str | None]:
"""Get the current preferences."""
return {
"gen_text_entity_id": self.gen_text_entity_id,
}
return {key: getattr(self, key) for key in self.KEYS}

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from enum import IntFlag
from typing import TYPE_CHECKING
from homeassistant.util.hass_dict import HassKey
@@ -19,3 +20,10 @@ DATA_PREFERENCES: HassKey[AITaskPreferences] = HassKey(f"{DOMAIN}_preferences")
DEFAULT_SYSTEM_PROMPT = (
"You are a Home Assistant expert and help users with their tasks."
)
class AITaskEntityFeature(IntFlag):
"""Supported features of the AI task entity."""
GENERATE_TEXT = 1
"""Generate text based on instructions."""

View File

@@ -4,6 +4,8 @@ from collections.abc import AsyncGenerator
import contextlib
from typing import final
from propcache.api import cached_property
from homeassistant.components.conversation import (
ChatLog,
UserContent,
@@ -15,7 +17,7 @@ from homeassistant.helpers.chat_session import async_get_chat_session
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.util import dt as dt_util
from .const import DEFAULT_SYSTEM_PROMPT, DOMAIN
from .const import DEFAULT_SYSTEM_PROMPT, DOMAIN, AITaskEntityFeature
from .task import GenTextTask, GenTextTaskResult
@@ -23,6 +25,7 @@ class AITaskEntity(RestoreEntity):
"""Entity that supports conversations."""
_attr_should_poll = False
_attr_supported_features = AITaskEntityFeature(0)
__last_activity: str | None = None
@property
@@ -33,6 +36,11 @@ class AITaskEntity(RestoreEntity):
return None
return self.__last_activity
@cached_property
def supported_features(self) -> AITaskEntityFeature:
"""Flag supported features."""
return self._attr_supported_features
async def async_internal_added_to_hass(self) -> None:
"""Call when the entity is added to hass."""
await super().async_internal_added_to_hass()

View File

@@ -8,43 +8,15 @@ from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
from .const import DATA_PREFERENCES
from .task import async_generate_text
@callback
def async_setup(hass: HomeAssistant) -> None:
"""Set up the HTTP API for the conversation integration."""
websocket_api.async_register_command(hass, websocket_generate_text)
websocket_api.async_register_command(hass, websocket_get_preferences)
websocket_api.async_register_command(hass, websocket_set_preferences)
@websocket_api.websocket_command(
{
vol.Required("type"): "ai_task/generate_text",
vol.Required("task_name"): str,
vol.Optional("entity_id"): str,
vol.Required("instructions"): str,
}
)
@websocket_api.require_admin
@websocket_api.async_response
async def websocket_generate_text(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Run a generate text task."""
msg.pop("type")
msg_id = msg.pop("id")
try:
result = await async_generate_text(hass=hass, **msg)
except ValueError as err:
connection.send_error(msg_id, websocket_api.const.ERR_UNKNOWN_ERROR, str(err))
return
connection.send_result(msg_id, result.as_dict())
@websocket_api.websocket_command(
{
vol.Required("type"): "ai_task/preferences/get",

View File

@@ -0,0 +1,7 @@
{
"services": {
"generate_text": {
"service": "mdi:file-star-four-points-outline"
}
}
}

View File

@@ -0,0 +1,19 @@
generate_text:
fields:
task_name:
example: "home summary"
required: true
selector:
text:
instructions:
example: "Generate a funny notification that garage door was left open"
required: true
selector:
text:
entity_id:
required: false
selector:
entity:
domain: ai_task
supported_features:
- ai_task.AITaskEntityFeature.GENERATE_TEXT

View File

@@ -0,0 +1,22 @@
{
"services": {
"generate_text": {
"name": "Generate text",
"description": "Use AI to run a task that generates text.",
"fields": {
"task_name": {
"name": "Task Name",
"description": "Name of the task."
},
"instructions": {
"name": "Instructions",
"description": "Instructions on what needs to be done."
},
"entity_id": {
"name": "Entity ID",
"description": "Entity ID to run the task on. If not provided, the preferred entity will be used."
}
}
}
}
}

View File

@@ -6,7 +6,7 @@ from dataclasses import dataclass
from homeassistant.core import HomeAssistant
from .const import DATA_COMPONENT, DATA_PREFERENCES
from .const import DATA_COMPONENT, DATA_PREFERENCES, AITaskEntityFeature
async def async_generate_text(
@@ -27,6 +27,9 @@ async def async_generate_text(
if entity is None:
raise ValueError(f"AI Task entity {entity_id} not found")
if AITaskEntityFeature.GENERATE_TEXT not in entity.supported_features:
raise ValueError(f"AI Task entity {entity_id} does not support generating text")
return await entity.internal_async_generate_text(
GenTextTask(
name=task_name,
@@ -57,12 +60,12 @@ class GenTextTaskResult:
conversation_id: str
"""Unique identifier for the conversation."""
result: str
"""Result of the task."""
text: str
"""Generated text."""
def as_dict(self) -> dict[str, str]:
"""Return result as a dict."""
return {
"conversation_id": self.conversation_id,
"result": self.result,
"text": self.text,
}

View File

@@ -39,14 +39,14 @@ class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
)
self._abort_if_unique_id_configured()
try:
location_point_valid = await test_location(
location_point_valid = await check_location(
websession,
user_input["api_key"],
user_input["latitude"],
user_input["longitude"],
)
if not location_point_valid:
location_nearest_valid = await test_location(
location_nearest_valid = await check_location(
websession,
user_input["api_key"],
user_input["latitude"],
@@ -88,7 +88,7 @@ class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
)
async def test_location(
async def check_location(
client: ClientSession,
api_key: str,
latitude: float,

View File

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

View File

@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
)
from homeassistant.const import (
PERCENTAGE,
EntityCategory,
UnitOfApparentPower,
UnitOfElectricCurrent,
UnitOfElectricPotential,
@@ -35,6 +36,7 @@ SENSORS: dict[str, SensorEntityDescription] = {
"alarmdel": SensorEntityDescription(
key="alarmdel",
translation_key="alarm_delay",
entity_category=EntityCategory.DIAGNOSTIC,
),
"ambtemp": SensorEntityDescription(
key="ambtemp",
@@ -47,15 +49,18 @@ SENSORS: dict[str, SensorEntityDescription] = {
key="apc",
translation_key="apc_status",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"apcmodel": SensorEntityDescription(
key="apcmodel",
translation_key="apc_model",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"badbatts": SensorEntityDescription(
key="badbatts",
translation_key="bad_batteries",
entity_category=EntityCategory.DIAGNOSTIC,
),
"battdate": SensorEntityDescription(
key="battdate",
@@ -82,6 +87,7 @@ SENSORS: dict[str, SensorEntityDescription] = {
key="cable",
translation_key="cable_type",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"cumonbatt": SensorEntityDescription(
key="cumonbatt",
@@ -94,52 +100,63 @@ SENSORS: dict[str, SensorEntityDescription] = {
key="date",
translation_key="date",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"dipsw": SensorEntityDescription(
key="dipsw",
translation_key="dip_switch_settings",
entity_category=EntityCategory.DIAGNOSTIC,
),
"dlowbatt": SensorEntityDescription(
key="dlowbatt",
translation_key="low_battery_signal",
entity_category=EntityCategory.DIAGNOSTIC,
),
"driver": SensorEntityDescription(
key="driver",
translation_key="driver",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"dshutd": SensorEntityDescription(
key="dshutd",
translation_key="shutdown_delay",
entity_category=EntityCategory.DIAGNOSTIC,
),
"dwake": SensorEntityDescription(
key="dwake",
translation_key="wake_delay",
entity_category=EntityCategory.DIAGNOSTIC,
),
"end apc": SensorEntityDescription(
key="end apc",
translation_key="date_and_time",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"extbatts": SensorEntityDescription(
key="extbatts",
translation_key="external_batteries",
entity_category=EntityCategory.DIAGNOSTIC,
),
"firmware": SensorEntityDescription(
key="firmware",
translation_key="firmware_version",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"hitrans": SensorEntityDescription(
key="hitrans",
translation_key="transfer_high",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
),
"hostname": SensorEntityDescription(
key="hostname",
translation_key="hostname",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"humidity": SensorEntityDescription(
key="humidity",
@@ -163,10 +180,12 @@ SENSORS: dict[str, SensorEntityDescription] = {
key="lastxfer",
translation_key="last_transfer",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"linefail": SensorEntityDescription(
key="linefail",
translation_key="line_failure",
entity_category=EntityCategory.DIAGNOSTIC,
),
"linefreq": SensorEntityDescription(
key="linefreq",
@@ -198,15 +217,18 @@ SENSORS: dict[str, SensorEntityDescription] = {
translation_key="transfer_low",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
),
"mandate": SensorEntityDescription(
key="mandate",
translation_key="manufacture_date",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"masterupd": SensorEntityDescription(
key="masterupd",
translation_key="master_update",
entity_category=EntityCategory.DIAGNOSTIC,
),
"maxlinev": SensorEntityDescription(
key="maxlinev",
@@ -217,11 +239,13 @@ SENSORS: dict[str, SensorEntityDescription] = {
"maxtime": SensorEntityDescription(
key="maxtime",
translation_key="max_time",
entity_category=EntityCategory.DIAGNOSTIC,
),
"mbattchg": SensorEntityDescription(
key="mbattchg",
translation_key="max_battery_charge",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
),
"minlinev": SensorEntityDescription(
key="minlinev",
@@ -232,41 +256,48 @@ SENSORS: dict[str, SensorEntityDescription] = {
"mintimel": SensorEntityDescription(
key="mintimel",
translation_key="min_time",
entity_category=EntityCategory.DIAGNOSTIC,
),
"model": SensorEntityDescription(
key="model",
translation_key="model",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"nombattv": SensorEntityDescription(
key="nombattv",
translation_key="battery_nominal_voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
),
"nominv": SensorEntityDescription(
key="nominv",
translation_key="nominal_input_voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
),
"nomoutv": SensorEntityDescription(
key="nomoutv",
translation_key="nominal_output_voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
),
"nompower": SensorEntityDescription(
key="nompower",
translation_key="nominal_output_power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
entity_category=EntityCategory.DIAGNOSTIC,
),
"nomapnt": SensorEntityDescription(
key="nomapnt",
translation_key="nominal_apparent_power",
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
device_class=SensorDeviceClass.APPARENT_POWER,
entity_category=EntityCategory.DIAGNOSTIC,
),
"numxfers": SensorEntityDescription(
key="numxfers",
@@ -291,21 +322,25 @@ SENSORS: dict[str, SensorEntityDescription] = {
key="reg1",
translation_key="register_1_fault",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"reg2": SensorEntityDescription(
key="reg2",
translation_key="register_2_fault",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"reg3": SensorEntityDescription(
key="reg3",
translation_key="register_3_fault",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"retpct": SensorEntityDescription(
key="retpct",
translation_key="restore_capacity",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
),
"selftest": SensorEntityDescription(
key="selftest",
@@ -315,20 +350,24 @@ SENSORS: dict[str, SensorEntityDescription] = {
key="sense",
translation_key="sensitivity",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"serialno": SensorEntityDescription(
key="serialno",
translation_key="serial_number",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"starttime": SensorEntityDescription(
key="starttime",
translation_key="startup_time",
entity_category=EntityCategory.DIAGNOSTIC,
),
"statflag": SensorEntityDescription(
key="statflag",
translation_key="online_status",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"status": SensorEntityDescription(
key="status",
@@ -337,6 +376,7 @@ SENSORS: dict[str, SensorEntityDescription] = {
"stesti": SensorEntityDescription(
key="stesti",
translation_key="self_test_interval",
entity_category=EntityCategory.DIAGNOSTIC,
),
"timeleft": SensorEntityDescription(
key="timeleft",
@@ -360,23 +400,28 @@ SENSORS: dict[str, SensorEntityDescription] = {
key="upsname",
translation_key="ups_name",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"version": SensorEntityDescription(
key="version",
translation_key="version",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"xoffbat": SensorEntityDescription(
key="xoffbat",
translation_key="transfer_from_battery",
entity_category=EntityCategory.DIAGNOSTIC,
),
"xoffbatt": SensorEntityDescription(
key="xoffbatt",
translation_key="transfer_from_battery",
entity_category=EntityCategory.DIAGNOSTIC,
),
"xonbatt": SensorEntityDescription(
key="xonbatt",
translation_key="transfer_to_battery",
entity_category=EntityCategory.DIAGNOSTIC,
),
}

View File

@@ -1,13 +1,23 @@
"""Base class for assist satellite entities."""
from dataclasses import asdict
import logging
from pathlib import Path
from typing import Any
from hassil.util import (
PUNCTUATION_END,
PUNCTUATION_END_WORD,
PUNCTUATION_START,
PUNCTUATION_START_WORD,
)
import voluptuous as vol
from homeassistant.components.http import StaticPathConfig
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
@@ -23,6 +33,7 @@ from .const import (
)
from .entity import (
AssistSatelliteAnnouncement,
AssistSatelliteAnswer,
AssistSatelliteConfiguration,
AssistSatelliteEntity,
AssistSatelliteEntityDescription,
@@ -34,6 +45,7 @@ from .websocket_api import async_register_websocket_api
__all__ = [
"DOMAIN",
"AssistSatelliteAnnouncement",
"AssistSatelliteAnswer",
"AssistSatelliteConfiguration",
"AssistSatelliteEntity",
"AssistSatelliteEntityDescription",
@@ -86,6 +98,62 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"async_internal_start_conversation",
[AssistSatelliteEntityFeature.START_CONVERSATION],
)
async def handle_ask_question(call: ServiceCall) -> dict[str, Any]:
"""Handle a Show View service call."""
satellite_entity_id: str = call.data[ATTR_ENTITY_ID]
satellite_entity: AssistSatelliteEntity | None = component.get_entity(
satellite_entity_id
)
if satellite_entity is None:
raise HomeAssistantError(
f"Invalid Assist satellite entity id: {satellite_entity_id}"
)
ask_question_args = {
"question": call.data.get("question"),
"question_media_id": call.data.get("question_media_id"),
"preannounce": call.data.get("preannounce", False),
"answers": call.data.get("answers"),
}
if preannounce_media_id := call.data.get("preannounce_media_id"):
ask_question_args["preannounce_media_id"] = preannounce_media_id
answer = await satellite_entity.async_internal_ask_question(**ask_question_args)
if answer is None:
raise HomeAssistantError("No answer from satellite")
return asdict(answer)
hass.services.async_register(
domain=DOMAIN,
service="ask_question",
service_func=handle_ask_question,
schema=vol.All(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_domain(DOMAIN),
vol.Optional("question"): str,
vol.Optional("question_media_id"): str,
vol.Optional("preannounce"): bool,
vol.Optional("preannounce_media_id"): str,
vol.Optional("answers"): [
{
vol.Required("id"): str,
vol.Required("sentences"): vol.All(
cv.ensure_list,
[cv.string],
has_one_non_empty_item,
has_no_punctuation,
),
}
],
},
cv.has_at_least_one_key("question", "question_media_id"),
),
supports_response=SupportsResponse.ONLY,
)
hass.data[CONNECTION_TEST_DATA] = {}
async_register_websocket_api(hass)
hass.http.register_view(ConnectionTestView())
@@ -110,3 +178,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
def has_no_punctuation(value: list[str]) -> list[str]:
"""Validate result does not contain punctuation."""
for sentence in value:
if (
PUNCTUATION_START.search(sentence)
or PUNCTUATION_END.search(sentence)
or PUNCTUATION_START_WORD.search(sentence)
or PUNCTUATION_END_WORD.search(sentence)
):
raise vol.Invalid("sentence should not contain punctuation")
return value
def has_one_non_empty_item(value: list[str]) -> list[str]:
"""Validate result has at least one item."""
if len(value) < 1:
raise vol.Invalid("at least one sentence is required")
for sentence in value:
if not sentence:
raise vol.Invalid("sentences cannot be empty")
return value

View File

@@ -4,12 +4,16 @@ from abc import abstractmethod
import asyncio
from collections.abc import AsyncIterable
import contextlib
from dataclasses import dataclass
from dataclasses import dataclass, field
from enum import StrEnum
import logging
import time
from typing import Any, Literal, final
from hassil import Intents, recognize
from hassil.expression import Expression, ListReference, Sequence
from hassil.intents import WildcardSlotList
from homeassistant.components import conversation, media_source, stt, tts
from homeassistant.components.assist_pipeline import (
OPTION_PREFERRED,
@@ -105,6 +109,20 @@ class AssistSatelliteAnnouncement:
"""Media ID to be played before announcement."""
@dataclass
class AssistSatelliteAnswer:
"""Answer to a question."""
id: str | None
"""Matched answer id or None if no answer was matched."""
sentence: str
"""Raw sentence text from user response."""
slots: dict[str, Any] = field(default_factory=dict)
"""Matched slots from answer."""
class AssistSatelliteEntity(entity.Entity):
"""Entity encapsulating the state and functionality of an Assist satellite."""
@@ -122,6 +140,7 @@ class AssistSatelliteEntity(entity.Entity):
_wake_word_intercept_future: asyncio.Future[str | None] | None = None
_attr_tts_options: dict[str, Any] | None = None
_pipeline_task: asyncio.Task | None = None
_ask_question_future: asyncio.Future[str | None] | None = None
__assist_satellite_state = AssistSatelliteState.IDLE
@@ -309,6 +328,112 @@ class AssistSatelliteEntity(entity.Entity):
"""Start a conversation from the satellite."""
raise NotImplementedError
async def async_internal_ask_question(
self,
question: str | None = None,
question_media_id: str | None = None,
preannounce: bool = True,
preannounce_media_id: str = PREANNOUNCE_URL,
answers: list[dict[str, Any]] | None = None,
) -> AssistSatelliteAnswer | None:
"""Ask a question and get a user's response from the satellite.
If question_media_id is not provided, question is synthesized to audio
with the selected pipeline.
If question_media_id is provided, it is played directly. It is possible
to omit the message and the satellite will not show any text.
If preannounce is True, a sound is played before the start message or media.
If preannounce_media_id is provided, it overrides the default sound.
Calls async_start_conversation.
"""
await self._cancel_running_pipeline()
if question is None:
question = ""
announcement = await self._resolve_announcement_media_id(
question,
question_media_id,
preannounce_media_id=preannounce_media_id if preannounce else None,
)
if self._is_announcing:
raise SatelliteBusyError
self._is_announcing = True
self._set_state(AssistSatelliteState.RESPONDING)
self._ask_question_future = asyncio.Future()
try:
# Wait for announcement to finish
await self.async_start_conversation(announcement)
# Wait for response text
response_text = await self._ask_question_future
if response_text is None:
raise HomeAssistantError("No answer from question")
if not answers:
return AssistSatelliteAnswer(id=None, sentence=response_text)
return self._question_response_to_answer(response_text, answers)
finally:
self._is_announcing = False
self._set_state(AssistSatelliteState.IDLE)
self._ask_question_future = None
def _question_response_to_answer(
self, response_text: str, answers: list[dict[str, Any]]
) -> AssistSatelliteAnswer:
"""Match text to a pre-defined set of answers."""
# Build intents and match
intents = Intents.from_dict(
{
"language": self.hass.config.language,
"intents": {
"QuestionIntent": {
"data": [
{
"sentences": answer["sentences"],
"metadata": {"answer_id": answer["id"]},
}
for answer in answers
]
}
},
}
)
# Assume slot list references are wildcards
wildcard_names: set[str] = set()
for intent in intents.intents.values():
for intent_data in intent.data:
for sentence in intent_data.sentences:
_collect_list_references(sentence, wildcard_names)
for wildcard_name in wildcard_names:
intents.slot_lists[wildcard_name] = WildcardSlotList(wildcard_name)
# Match response text
result = recognize(response_text, intents)
if result is None:
# No match
return AssistSatelliteAnswer(id=None, sentence=response_text)
assert result.intent_metadata
return AssistSatelliteAnswer(
id=result.intent_metadata["answer_id"],
sentence=response_text,
slots={
entity_name: entity.value
for entity_name, entity in result.entities.items()
},
)
async def async_accept_pipeline_from_satellite(
self,
audio_stream: AsyncIterable[bytes],
@@ -351,6 +476,11 @@ class AssistSatelliteEntity(entity.Entity):
self._internal_on_pipeline_event(PipelineEvent(PipelineEventType.RUN_END))
return
if (self._ask_question_future is not None) and (
start_stage == PipelineStage.STT
):
end_stage = PipelineStage.STT
device_id = self.registry_entry.device_id if self.registry_entry else None
# Refresh context if necessary
@@ -433,6 +563,16 @@ class AssistSatelliteEntity(entity.Entity):
self._set_state(AssistSatelliteState.IDLE)
elif event.type is PipelineEventType.STT_START:
self._set_state(AssistSatelliteState.LISTENING)
elif event.type is PipelineEventType.STT_END:
# Intercepting text for ask question
if (
(self._ask_question_future is not None)
and (not self._ask_question_future.done())
and event.data
):
self._ask_question_future.set_result(
event.data.get("stt_output", {}).get("text")
)
elif event.type is PipelineEventType.INTENT_START:
self._set_state(AssistSatelliteState.PROCESSING)
elif event.type is PipelineEventType.TTS_START:
@@ -443,6 +583,12 @@ class AssistSatelliteEntity(entity.Entity):
if not self._run_has_tts:
self._set_state(AssistSatelliteState.IDLE)
if (self._ask_question_future is not None) and (
not self._ask_question_future.done()
):
# No text for ask question
self._ask_question_future.set_result(None)
self.on_pipeline_event(event)
@callback
@@ -577,3 +723,15 @@ class AssistSatelliteEntity(entity.Entity):
media_id_source=media_id_source,
preannounce_media_id=preannounce_media_id,
)
def _collect_list_references(expression: Expression, list_names: set[str]) -> None:
"""Collect list reference names recursively."""
if isinstance(expression, Sequence):
seq: Sequence = expression
for item in seq.items:
_collect_list_references(item, list_names)
elif isinstance(expression, ListReference):
# {list}
list_ref: ListReference = expression
list_names.add(list_ref.slot_name)

View File

@@ -10,6 +10,9 @@
},
"start_conversation": {
"service": "mdi:forum"
},
"ask_question": {
"service": "mdi:microphone-question"
}
}
}

View File

@@ -5,5 +5,6 @@
"dependencies": ["assist_pipeline", "http", "stt", "tts"],
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
"integration_type": "entity",
"quality_scale": "internal"
"quality_scale": "internal",
"requirements": ["hassil==2.2.3"]
}

View File

@@ -54,3 +54,35 @@ start_conversation:
required: false
selector:
text:
ask_question:
fields:
entity_id:
required: true
selector:
entity:
domain: assist_satellite
supported_features:
- assist_satellite.AssistSatelliteEntityFeature.START_CONVERSATION
question:
required: false
example: "What kind of music would you like to play?"
default: ""
selector:
text:
question_media_id:
required: false
selector:
text:
preannounce:
required: false
default: true
selector:
boolean:
preannounce_media_id:
required: false
selector:
text:
answers:
required: false
selector:
object:

View File

@@ -59,6 +59,36 @@
"description": "Custom media ID to play before the start message or media."
}
}
},
"ask_question": {
"name": "Ask question",
"description": "Asks a question and gets the user's response.",
"fields": {
"entity_id": {
"name": "Entity",
"description": "Assist satellite entity to ask the question on."
},
"question": {
"name": "Question",
"description": "The question to ask."
},
"question_media_id": {
"name": "Question media ID",
"description": "The media ID of the question to use instead of text-to-speech."
},
"preannounce": {
"name": "Preannounce",
"description": "Play a sound before the start message or media."
},
"preannounce_media_id": {
"name": "Preannounce media ID",
"description": "Custom media ID to play before the start message or media."
},
"answers": {
"name": "Answers",
"description": "Possible answers to the question."
}
}
}
}
}

View File

@@ -12,7 +12,7 @@ DATA_BLUEPRINTS = "automation_blueprints"
def _blueprint_in_use(hass: HomeAssistant, blueprint_path: str) -> bool:
"""Return True if any automation references the blueprint."""
from . import automations_with_blueprint # pylint: disable=import-outside-toplevel
from . import automations_with_blueprint # noqa: PLC0415
return len(automations_with_blueprint(hass, blueprint_path)) > 0
@@ -28,8 +28,7 @@ async def _reload_blueprint_automations(
@callback
def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints:
"""Get automation blueprints."""
# pylint: disable-next=import-outside-toplevel
from .config import AUTOMATION_BLUEPRINT_SCHEMA
from .config import AUTOMATION_BLUEPRINT_SCHEMA # noqa: PLC0415
return blueprint.DomainBlueprints(
hass,

View File

@@ -94,8 +94,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if not with_hassio:
reader_writer = CoreBackupReaderWriter(hass)
else:
# pylint: disable-next=import-outside-toplevel, hass-component-root-import
from homeassistant.components.hassio.backup import SupervisorBackupReaderWriter
# pylint: disable-next=hass-component-root-import
from homeassistant.components.hassio.backup import ( # noqa: PLC0415
SupervisorBackupReaderWriter,
)
reader_writer = SupervisorBackupReaderWriter(hass)

View File

@@ -20,5 +20,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bthome",
"iot_class": "local_push",
"requirements": ["bthome-ble==3.12.4"]
"requirements": ["bthome-ble==3.13.1"]
}

View File

@@ -240,6 +240,10 @@ async def _async_get_stream_image(
height: int | None = None,
wait_for_next_keyframe: bool = False,
) -> bytes | None:
if (provider := camera._webrtc_provider) and ( # noqa: SLF001
image := await provider.async_get_image(camera, width=width, height=height)
) is not None:
return image
if not camera.stream and CameraEntityFeature.STREAM in camera.supported_features:
camera.stream = await camera.async_create_stream()
if camera.stream:

View File

@@ -156,6 +156,15 @@ class CameraWebRTCProvider(ABC):
"""Close the session."""
return ## This is an optional method so we need a default here.
async def async_get_image(
self,
camera: Camera,
width: int | None = None,
height: int | None = None,
) -> bytes | None:
"""Get an image from the camera."""
return None
@callback
def async_register_webrtc_provider(

View File

@@ -105,11 +105,6 @@ DEFAULT_MAX_HUMIDITY = 99
CONVERTIBLE_ATTRIBUTE = [ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH]
# Can be removed in 2025.1 after deprecation period of the new feature flags
CHECK_TURN_ON_OFF_FEATURE_FLAG = (
ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
)
SET_TEMPERATURE_SCHEMA = vol.All(
cv.has_at_least_one_key(
ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW

View File

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

View File

@@ -54,10 +54,10 @@ class Control4RuntimeData:
type Control4ConfigEntry = ConfigEntry[Control4RuntimeData]
async def call_c4_api_retry(func, *func_args):
async def call_c4_api_retry(func, *func_args): # noqa: RET503
"""Call C4 API function and retry on failure."""
# Ruff doesn't understand this loop - the exception is always raised after the retries
for i in range(API_RETRY_TIMES): # noqa: RET503
for i in range(API_RETRY_TIMES):
try:
return await func(*func_args)
except client_exceptions.ClientError as exception:

View File

@@ -271,7 +271,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
# Temporary migration. We can remove this in 2024.10
from homeassistant.components.assist_pipeline import ( # pylint: disable=import-outside-toplevel
from homeassistant.components.assist_pipeline import ( # noqa: PLC0415
async_migrate_engine,
)

View File

@@ -300,10 +300,6 @@ class CoverEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
def supported_features(self) -> CoverEntityFeature:
"""Flag supported features."""
if (features := self._attr_supported_features) is not None:
if type(features) is int:
new_features = CoverEntityFeature(features)
self._report_deprecated_supported_features_values(new_features)
return new_features
return features
supported_features = (

View File

@@ -18,7 +18,7 @@ from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.device_registry import DeviceEntry
from .const import GATEWAY_SERIAL_PATTERN, PLATFORMS
from .const import DOMAIN, GATEWAY_SERIAL_PATTERN, PLATFORMS
type DevoloHomeControlConfigEntry = ConfigEntry[list[HomeControl]]
@@ -32,10 +32,16 @@ async def async_setup_entry(
credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid)
if not credentials_valid:
raise ConfigEntryAuthFailed
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
)
if await hass.async_add_executor_job(mydevolo.maintenance):
raise ConfigEntryNotReady
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="maintenance",
)
gateway_ids = await hass.async_add_executor_job(mydevolo.get_gateway_ids)
@@ -69,7 +75,11 @@ async def async_setup_entry(
)
)
except GatewayOfflineError as err:
raise ConfigEntryNotReady from err
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="connection_failed",
translation_placeholders={"gateway_id": gateway_id},
) from err
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -91,7 +101,9 @@ async def async_unload_entry(
async def async_remove_config_entry_device(
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry
hass: HomeAssistant,
config_entry: DevoloHomeControlConfigEntry,
device_entry: DeviceEntry,
) -> bool:
"""Remove a config entry from a device."""
return True

View File

@@ -45,5 +45,16 @@
"name": "Brightness"
}
}
},
"exceptions": {
"connection_failed": {
"message": "Failed to connect to devolo Home Control central unit {gateway_id}."
},
"invalid_auth": {
"message": "Authentication failed. Please re-authenticaticate with your mydevolo account."
},
"maintenance": {
"message": "devolo Home Control is currently in maintenance mode."
}
}
}

View File

@@ -87,6 +87,7 @@ class DevoloScannerEntity( # pylint: disable=hass-enforce-class-module
):
"""Representation of a devolo device tracker."""
_attr_has_entity_name = True
_attr_translation_key = "device_tracker"
def __init__(
@@ -99,6 +100,7 @@ class DevoloScannerEntity( # pylint: disable=hass-enforce-class-module
super().__init__(coordinator)
self._device = device
self._attr_mac_address = mac
self._attr_name = mac
@property
def extra_state_attributes(self) -> dict[str, str]:

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_push",
"loggers": ["discord"],
"requirements": ["nextcord==2.6.0"]
"requirements": ["nextcord==3.1.0"]
}

View File

@@ -108,8 +108,7 @@ def download_file(service: ServiceCall) -> None:
_LOGGER.debug("%s -> %s", url, final_path)
with open(final_path, "wb") as fil:
for chunk in req.iter_content(1024):
fil.write(chunk)
fil.writelines(req.iter_content(1024))
_LOGGER.debug("Downloading of %s done", url)
service.hass.bus.fire(

View File

@@ -2,9 +2,9 @@
from collections.abc import Callable
from dataclasses import dataclass
from typing import Generic
from deebot_client.capabilities import CapabilityEvent
from deebot_client.events.base import Event
from deebot_client.events.water_info import MopAttachedEvent
from homeassistant.components.binary_sensor import (
@@ -16,15 +16,14 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import EcovacsConfigEntry
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity
from .util import get_supported_entities
@dataclass(kw_only=True, frozen=True)
class EcovacsBinarySensorEntityDescription(
class EcovacsBinarySensorEntityDescription[EventT: Event](
BinarySensorEntityDescription,
EcovacsCapabilityEntityDescription,
Generic[EventT],
):
"""Class describing Deebot binary sensor entity."""
@@ -55,7 +54,7 @@ async def async_setup_entry(
)
class EcovacsBinarySensor(
class EcovacsBinarySensor[EventT: Event](
EcovacsDescriptionEntity[CapabilityEvent[EventT]],
BinarySensorEntity,
):

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any, Generic, TypeVar
from typing import Any
from deebot_client.capabilities import Capabilities
from deebot_client.device import Device
@@ -18,11 +18,8 @@ from homeassistant.helpers.entity import Entity, EntityDescription
from .const import DOMAIN
CapabilityEntity = TypeVar("CapabilityEntity")
EventT = TypeVar("EventT", bound=Event)
class EcovacsEntity(Entity, Generic[CapabilityEntity]):
class EcovacsEntity[CapabilityEntityT](Entity):
"""Ecovacs entity."""
_attr_should_poll = False
@@ -32,7 +29,7 @@ class EcovacsEntity(Entity, Generic[CapabilityEntity]):
def __init__(
self,
device: Device,
capability: CapabilityEntity,
capability: CapabilityEntityT,
**kwargs: Any,
) -> None:
"""Initialize entity."""
@@ -80,7 +77,7 @@ class EcovacsEntity(Entity, Generic[CapabilityEntity]):
self._subscribe(AvailabilityEvent, on_available)
def _subscribe(
def _subscribe[EventT: Event](
self,
event_type: type[EventT],
callback: Callable[[EventT], Coroutine[Any, Any, None]],
@@ -98,13 +95,13 @@ class EcovacsEntity(Entity, Generic[CapabilityEntity]):
self._device.events.request_refresh(event_type)
class EcovacsDescriptionEntity(EcovacsEntity[CapabilityEntity]):
class EcovacsDescriptionEntity[CapabilityEntityT](EcovacsEntity[CapabilityEntityT]):
"""Ecovacs entity."""
def __init__(
self,
device: Device,
capability: CapabilityEntity,
capability: CapabilityEntityT,
entity_description: EntityDescription,
**kwargs: Any,
) -> None:
@@ -114,13 +111,12 @@ class EcovacsDescriptionEntity(EcovacsEntity[CapabilityEntity]):
@dataclass(kw_only=True, frozen=True)
class EcovacsCapabilityEntityDescription(
class EcovacsCapabilityEntityDescription[CapabilityEntityT](
EntityDescription,
Generic[CapabilityEntity],
):
"""Ecovacs entity description."""
capability_fn: Callable[[Capabilities], CapabilityEntity | None]
capability_fn: Callable[[Capabilities], CapabilityEntityT | None]
class EcovacsLegacyEntity(Entity):

View File

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

View File

@@ -4,10 +4,10 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Generic
from deebot_client.capabilities import CapabilitySet
from deebot_client.events import CleanCountEvent, CutDirectionEvent, VolumeEvent
from deebot_client.events.base import Event
from homeassistant.components.number import (
NumberEntity,
@@ -23,16 +23,14 @@ from .entity import (
EcovacsCapabilityEntityDescription,
EcovacsDescriptionEntity,
EcovacsEntity,
EventT,
)
from .util import get_supported_entities
@dataclass(kw_only=True, frozen=True)
class EcovacsNumberEntityDescription(
class EcovacsNumberEntityDescription[EventT: Event](
NumberEntityDescription,
EcovacsCapabilityEntityDescription,
Generic[EventT],
):
"""Ecovacs number entity description."""
@@ -94,7 +92,7 @@ async def async_setup_entry(
async_add_entities(entities)
class EcovacsNumberEntity(
class EcovacsNumberEntity[EventT: Event](
EcovacsDescriptionEntity[CapabilitySet[EventT, [int]]],
NumberEntity,
):

View File

@@ -2,11 +2,12 @@
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Generic
from typing import Any
from deebot_client.capabilities import CapabilitySetTypes
from deebot_client.device import Device
from deebot_client.events import WorkModeEvent
from deebot_client.events.base import Event
from deebot_client.events.water_info import WaterAmountEvent
from homeassistant.components.select import SelectEntity, SelectEntityDescription
@@ -15,15 +16,14 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import EcovacsConfigEntry
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity
from .util import get_name_key, get_supported_entities
@dataclass(kw_only=True, frozen=True)
class EcovacsSelectEntityDescription(
class EcovacsSelectEntityDescription[EventT: Event](
SelectEntityDescription,
EcovacsCapabilityEntityDescription,
Generic[EventT],
):
"""Ecovacs select entity description."""
@@ -66,7 +66,7 @@ async def async_setup_entry(
async_add_entities(entities)
class EcovacsSelectEntity(
class EcovacsSelectEntity[EventT: Event](
EcovacsDescriptionEntity[CapabilitySetTypes[EventT, [str], str]],
SelectEntity,
):

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Generic
from typing import Any
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan, DeviceType
from deebot_client.device import Device
@@ -46,16 +46,14 @@ from .entity import (
EcovacsDescriptionEntity,
EcovacsEntity,
EcovacsLegacyEntity,
EventT,
)
from .util import get_name_key, get_options, get_supported_entities
@dataclass(kw_only=True, frozen=True)
class EcovacsSensorEntityDescription(
class EcovacsSensorEntityDescription[EventT: Event](
EcovacsCapabilityEntityDescription,
SensorEntityDescription,
Generic[EventT],
):
"""Ecovacs sensor entity description."""

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["sml"],
"requirements": ["pysml==0.0.12"]
"requirements": ["pysml==0.1.5"]
}

View File

@@ -2,7 +2,7 @@
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Generic, TypeVar, override
from typing import Any, override
from eheimdigital.classic_vario import EheimDigitalClassicVario
from eheimdigital.device import EheimDigitalDevice
@@ -30,16 +30,16 @@ from .entity import EheimDigitalEntity, exception_handler
PARALLEL_UPDATES = 0
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
@dataclass(frozen=True, kw_only=True)
class EheimDigitalNumberDescription(NumberEntityDescription, Generic[_DeviceT_co]):
class EheimDigitalNumberDescription[_DeviceT: EheimDigitalDevice](
NumberEntityDescription
):
"""Class describing EHEIM Digital sensor entities."""
value_fn: Callable[[_DeviceT_co], float | None]
set_value_fn: Callable[[_DeviceT_co, float], Awaitable[None]]
uom_fn: Callable[[_DeviceT_co], str] | None = None
value_fn: Callable[[_DeviceT], float | None]
set_value_fn: Callable[[_DeviceT, float], Awaitable[None]]
uom_fn: Callable[[_DeviceT], str] | None = None
CLASSICVARIO_DESCRIPTIONS: tuple[
@@ -136,7 +136,7 @@ async def async_setup_entry(
device_address: dict[str, EheimDigitalDevice],
) -> None:
"""Set up the number entities for one or multiple devices."""
entities: list[EheimDigitalNumber[EheimDigitalDevice]] = []
entities: list[EheimDigitalNumber[Any]] = []
for device in device_address.values():
if isinstance(device, EheimDigitalClassicVario):
entities.extend(
@@ -163,18 +163,18 @@ async def async_setup_entry(
async_setup_device_entities(coordinator.hub.devices)
class EheimDigitalNumber(
EheimDigitalEntity[_DeviceT_co], NumberEntity, Generic[_DeviceT_co]
class EheimDigitalNumber[_DeviceT: EheimDigitalDevice](
EheimDigitalEntity[_DeviceT], NumberEntity
):
"""Represent a EHEIM Digital number entity."""
entity_description: EheimDigitalNumberDescription[_DeviceT_co]
entity_description: EheimDigitalNumberDescription[_DeviceT]
def __init__(
self,
coordinator: EheimDigitalUpdateCoordinator,
device: _DeviceT_co,
description: EheimDigitalNumberDescription[_DeviceT_co],
device: _DeviceT,
description: EheimDigitalNumberDescription[_DeviceT],
) -> None:
"""Initialize an EHEIM Digital number entity."""
super().__init__(coordinator, device)

View File

@@ -2,7 +2,7 @@
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Generic, TypeVar, override
from typing import Any, override
from eheimdigital.classic_vario import EheimDigitalClassicVario
from eheimdigital.device import EheimDigitalDevice
@@ -17,15 +17,15 @@ from .entity import EheimDigitalEntity, exception_handler
PARALLEL_UPDATES = 0
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
@dataclass(frozen=True, kw_only=True)
class EheimDigitalSelectDescription(SelectEntityDescription, Generic[_DeviceT_co]):
class EheimDigitalSelectDescription[_DeviceT: EheimDigitalDevice](
SelectEntityDescription
):
"""Class describing EHEIM Digital select entities."""
value_fn: Callable[[_DeviceT_co], str | None]
set_value_fn: Callable[[_DeviceT_co, str], Awaitable[None]]
value_fn: Callable[[_DeviceT], str | None]
set_value_fn: Callable[[_DeviceT, str], Awaitable[None]]
CLASSICVARIO_DESCRIPTIONS: tuple[
@@ -59,7 +59,7 @@ async def async_setup_entry(
device_address: dict[str, EheimDigitalDevice],
) -> None:
"""Set up the number entities for one or multiple devices."""
entities: list[EheimDigitalSelect[EheimDigitalDevice]] = []
entities: list[EheimDigitalSelect[Any]] = []
for device in device_address.values():
if isinstance(device, EheimDigitalClassicVario):
entities.extend(
@@ -75,18 +75,18 @@ async def async_setup_entry(
async_setup_device_entities(coordinator.hub.devices)
class EheimDigitalSelect(
EheimDigitalEntity[_DeviceT_co], SelectEntity, Generic[_DeviceT_co]
class EheimDigitalSelect[_DeviceT: EheimDigitalDevice](
EheimDigitalEntity[_DeviceT], SelectEntity
):
"""Represent an EHEIM Digital select entity."""
entity_description: EheimDigitalSelectDescription[_DeviceT_co]
entity_description: EheimDigitalSelectDescription[_DeviceT]
def __init__(
self,
coordinator: EheimDigitalUpdateCoordinator,
device: _DeviceT_co,
description: EheimDigitalSelectDescription[_DeviceT_co],
device: _DeviceT,
description: EheimDigitalSelectDescription[_DeviceT],
) -> None:
"""Initialize an EHEIM Digital select entity."""
super().__init__(coordinator, device)

View File

@@ -2,7 +2,7 @@
from collections.abc import Callable
from dataclasses import dataclass
from typing import Generic, TypeVar, override
from typing import Any, override
from eheimdigital.classic_vario import EheimDigitalClassicVario
from eheimdigital.device import EheimDigitalDevice
@@ -20,14 +20,14 @@ from .entity import EheimDigitalEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
@dataclass(frozen=True, kw_only=True)
class EheimDigitalSensorDescription(SensorEntityDescription, Generic[_DeviceT_co]):
class EheimDigitalSensorDescription[_DeviceT: EheimDigitalDevice](
SensorEntityDescription
):
"""Class describing EHEIM Digital sensor entities."""
value_fn: Callable[[_DeviceT_co], float | str | None]
value_fn: Callable[[_DeviceT], float | str | None]
CLASSICVARIO_DESCRIPTIONS: tuple[
@@ -75,7 +75,7 @@ async def async_setup_entry(
device_address: dict[str, EheimDigitalDevice],
) -> None:
"""Set up the light entities for one or multiple devices."""
entities: list[EheimDigitalSensor[EheimDigitalDevice]] = []
entities: list[EheimDigitalSensor[Any]] = []
for device in device_address.values():
if isinstance(device, EheimDigitalClassicVario):
entities += [
@@ -91,18 +91,18 @@ async def async_setup_entry(
async_setup_device_entities(coordinator.hub.devices)
class EheimDigitalSensor(
EheimDigitalEntity[_DeviceT_co], SensorEntity, Generic[_DeviceT_co]
class EheimDigitalSensor[_DeviceT: EheimDigitalDevice](
EheimDigitalEntity[_DeviceT], SensorEntity
):
"""Represent a EHEIM Digital sensor entity."""
entity_description: EheimDigitalSensorDescription[_DeviceT_co]
entity_description: EheimDigitalSensorDescription[_DeviceT]
def __init__(
self,
coordinator: EheimDigitalUpdateCoordinator,
device: _DeviceT_co,
description: EheimDigitalSensorDescription[_DeviceT_co],
device: _DeviceT,
description: EheimDigitalSensorDescription[_DeviceT],
) -> None:
"""Initialize an EHEIM Digital number entity."""
super().__init__(coordinator, device)

View File

@@ -3,7 +3,7 @@
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from datetime import time
from typing import Generic, TypeVar, final, override
from typing import Any, final, override
from eheimdigital.classic_vario import EheimDigitalClassicVario
from eheimdigital.device import EheimDigitalDevice
@@ -19,15 +19,13 @@ from .entity import EheimDigitalEntity, exception_handler
PARALLEL_UPDATES = 0
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
@dataclass(frozen=True, kw_only=True)
class EheimDigitalTimeDescription(TimeEntityDescription, Generic[_DeviceT_co]):
class EheimDigitalTimeDescription[_DeviceT: EheimDigitalDevice](TimeEntityDescription):
"""Class describing EHEIM Digital time entities."""
value_fn: Callable[[_DeviceT_co], time | None]
set_value_fn: Callable[[_DeviceT_co, time], Awaitable[None]]
value_fn: Callable[[_DeviceT], time | None]
set_value_fn: Callable[[_DeviceT, time], Awaitable[None]]
CLASSICVARIO_DESCRIPTIONS: tuple[
@@ -79,7 +77,7 @@ async def async_setup_entry(
device_address: dict[str, EheimDigitalDevice],
) -> None:
"""Set up the time entities for one or multiple devices."""
entities: list[EheimDigitalTime[EheimDigitalDevice]] = []
entities: list[EheimDigitalTime[Any]] = []
for device in device_address.values():
if isinstance(device, EheimDigitalClassicVario):
entities.extend(
@@ -103,18 +101,18 @@ async def async_setup_entry(
@final
class EheimDigitalTime(
EheimDigitalEntity[_DeviceT_co], TimeEntity, Generic[_DeviceT_co]
class EheimDigitalTime[_DeviceT: EheimDigitalDevice](
EheimDigitalEntity[_DeviceT], TimeEntity
):
"""Represent an EHEIM Digital time entity."""
entity_description: EheimDigitalTimeDescription[_DeviceT_co]
entity_description: EheimDigitalTimeDescription[_DeviceT]
def __init__(
self,
coordinator: EheimDigitalUpdateCoordinator,
device: _DeviceT_co,
description: EheimDigitalTimeDescription[_DeviceT_co],
device: _DeviceT,
description: EheimDigitalTimeDescription[_DeviceT],
) -> None:
"""Initialize an EHEIM Digital time entity."""
super().__init__(coordinator, device)

View File

@@ -60,6 +60,7 @@ _VOICE_ASSISTANT_EVENT_TYPES: EsphomeEnumMapper[
VoiceAssistantEventType.VOICE_ASSISTANT_STT_START: PipelineEventType.STT_START,
VoiceAssistantEventType.VOICE_ASSISTANT_STT_END: PipelineEventType.STT_END,
VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_START: PipelineEventType.INTENT_START,
VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_PROGRESS: PipelineEventType.INTENT_PROGRESS,
VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_END: PipelineEventType.INTENT_END,
VoiceAssistantEventType.VOICE_ASSISTANT_TTS_START: PipelineEventType.TTS_START,
VoiceAssistantEventType.VOICE_ASSISTANT_TTS_END: PipelineEventType.TTS_END,
@@ -282,6 +283,12 @@ class EsphomeAssistSatellite(
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_STT_END:
assert event.data is not None
data_to_send = {"text": event.data["stt_output"]["text"]}
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_PROGRESS:
data_to_send = {
"tts_start_streaming": "1"
if (event.data and event.data.get("tts_start_streaming"))
else "0",
}
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_END:
assert event.data is not None
data_to_send = {
@@ -332,7 +339,7 @@ class EsphomeAssistSatellite(
}
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_RUN_START:
assert event.data is not None
if tts_output := event.data["tts_output"]:
if tts_output := event.data.get("tts_output"):
path = tts_output["url"]
url = async_process_play_media_url(self.hass, path)
data_to_send = {"url": url}

View File

@@ -63,9 +63,7 @@ class ESPHomeDashboardManager:
if not (data := self._data) or not (info := data.get("info")):
return
if is_hassio(self._hass):
from homeassistant.components.hassio import ( # pylint: disable=import-outside-toplevel
get_addons_info,
)
from homeassistant.components.hassio import get_addons_info # noqa: PLC0415
if (addons := get_addons_info(self._hass)) is not None and info[
"addon_slug"

View File

@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==32.2.1",
"aioesphomeapi==32.2.4",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==2.16.0"
],

View File

@@ -364,8 +364,7 @@ def _frontend_root(dev_repo_path: str | None) -> pathlib.Path:
if dev_repo_path is not None:
return pathlib.Path(dev_repo_path) / "hass_frontend"
# Keep import here so that we can import frontend without installing reqs
# pylint: disable-next=import-outside-toplevel
import hass_frontend
import hass_frontend # noqa: PLC0415
return hass_frontend.where()

View File

@@ -1,8 +1,11 @@
"""The go2rtc component."""
from __future__ import annotations
import logging
import shutil
from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
from awesomeversion import AwesomeVersion
from go2rtc_client import Go2RtcRestClient
@@ -32,7 +35,7 @@ from homeassistant.components.default_config import DOMAIN as DEFAULT_CONFIG_DOM
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import (
config_validation as cv,
discovery_flow,
@@ -98,6 +101,7 @@ CONFIG_SCHEMA = vol.Schema(
_DATA_GO2RTC: HassKey[str] = HassKey(DOMAIN)
_RETRYABLE_ERRORS = (ClientConnectionError, ServerConnectionError)
type Go2RtcConfigEntry = ConfigEntry[WebRTCProvider]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@@ -151,13 +155,14 @@ async def _remove_go2rtc_entries(hass: HomeAssistant) -> None:
await hass.config_entries.async_remove(entry.entry_id)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: Go2RtcConfigEntry) -> bool:
"""Set up go2rtc from a config entry."""
url = hass.data[_DATA_GO2RTC]
url = hass.data[_DATA_GO2RTC]
session = async_get_clientsession(hass)
client = Go2RtcRestClient(session, url)
# Validate the server URL
try:
client = Go2RtcRestClient(async_get_clientsession(hass), url)
version = await client.validate_server_version()
if version < AwesomeVersion(RECOMMENDED_VERSION):
ir.async_create_issue(
@@ -188,13 +193,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
return False
provider = WebRTCProvider(hass, url)
async_register_webrtc_provider(hass, provider)
provider = entry.runtime_data = WebRTCProvider(hass, url, session, client)
entry.async_on_unload(async_register_webrtc_provider(hass, provider))
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: Go2RtcConfigEntry) -> bool:
"""Unload a go2rtc config entry."""
await entry.runtime_data.teardown()
return True
@@ -206,12 +212,18 @@ async def _get_binary(hass: HomeAssistant) -> str | None:
class WebRTCProvider(CameraWebRTCProvider):
"""WebRTC provider."""
def __init__(self, hass: HomeAssistant, url: str) -> None:
def __init__(
self,
hass: HomeAssistant,
url: str,
session: ClientSession,
rest_client: Go2RtcRestClient,
) -> None:
"""Initialize the WebRTC provider."""
self._hass = hass
self._url = url
self._session = async_get_clientsession(hass)
self._rest_client = Go2RtcRestClient(self._session, url)
self._session = session
self._rest_client = rest_client
self._sessions: dict[str, Go2RtcWsClient] = {}
@property
@@ -232,32 +244,16 @@ class WebRTCProvider(CameraWebRTCProvider):
send_message: WebRTCSendMessage,
) -> None:
"""Handle the WebRTC offer and return the answer via the provided callback."""
try:
await self._update_stream_source(camera)
except HomeAssistantError as err:
send_message(WebRTCError("go2rtc_webrtc_offer_failed", str(err)))
return
self._sessions[session_id] = ws_client = Go2RtcWsClient(
self._session, self._url, source=camera.entity_id
)
if not (stream_source := await camera.stream_source()):
send_message(
WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source")
)
return
streams = await self._rest_client.streams.list()
if (stream := streams.get(camera.entity_id)) is None or not any(
stream_source == producer.url for producer in stream.producers
):
await self._rest_client.streams.add(
camera.entity_id,
[
stream_source,
# We are setting any ffmpeg rtsp related logs to debug
# Connection problems to the camera will be logged by the first stream
# Therefore setting it to debug will not hide any important logs
f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug",
],
)
@callback
def on_messages(message: ReceiveMessages) -> None:
"""Handle messages."""
@@ -291,3 +287,48 @@ class WebRTCProvider(CameraWebRTCProvider):
"""Close the session."""
ws_client = self._sessions.pop(session_id)
self._hass.async_create_task(ws_client.close())
async def async_get_image(
self,
camera: Camera,
width: int | None = None,
height: int | None = None,
) -> bytes | None:
"""Get an image from the camera."""
await self._update_stream_source(camera)
return await self._rest_client.get_jpeg_snapshot(
camera.entity_id, width, height
)
async def _update_stream_source(self, camera: Camera) -> None:
"""Update the stream source in go2rtc config if needed."""
if not (stream_source := await camera.stream_source()):
await self.teardown()
raise HomeAssistantError("Camera has no stream source")
if not self.async_is_supported(stream_source):
await self.teardown()
raise HomeAssistantError("Stream source is not supported by go2rtc")
streams = await self._rest_client.streams.list()
if (stream := streams.get(camera.entity_id)) is None or not any(
stream_source == producer.url for producer in stream.producers
):
await self._rest_client.streams.add(
camera.entity_id,
[
stream_source,
# We are setting any ffmpeg rtsp related logs to debug
# Connection problems to the camera will be logged by the first stream
# Therefore setting it to debug will not hide any important logs
f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug",
f"ffmpeg:{camera.entity_id}#video=mjpeg",
],
)
async def teardown(self) -> None:
"""Tear down the provider."""
for ws_client in self._sessions.values():
await ws_client.close()
self._sessions.clear()

View File

@@ -109,6 +109,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="timestamp",
translation_key="timestamp",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,

View File

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

View File

@@ -212,8 +212,7 @@ class AbstractConfig(ABC):
def async_enable_report_state(self) -> None:
"""Enable proactive mode."""
# Circular dep
# pylint: disable-next=import-outside-toplevel
from .report_state import async_enable_report_state
from .report_state import async_enable_report_state # noqa: PLC0415
if self._unsub_report_state is None:
self._unsub_report_state = async_enable_report_state(self.hass, self)
@@ -395,8 +394,7 @@ class AbstractConfig(ABC):
async def _handle_local_webhook(self, hass, webhook_id, request):
"""Handle an incoming local SDK message."""
# Circular dep
# pylint: disable-next=import-outside-toplevel
from . import smart_home
from . import smart_home # noqa: PLC0415
self._local_last_active = utcnow()
@@ -655,8 +653,9 @@ class GoogleEntity:
if "matter" in self.hass.config.components and any(
x for x in device_entry.identifiers if x[0] == "matter"
):
# pylint: disable-next=import-outside-toplevel
from homeassistant.components.matter import get_matter_device_info
from homeassistant.components.matter import ( # noqa: PLC0415
get_matter_device_info,
)
# Import matter can block the event loop for multiple seconds
# so we import it here to avoid blocking the event loop during

File diff suppressed because it is too large Load Diff

View File

@@ -29,8 +29,7 @@ async def update_addon(
client = get_supervisor_client(hass)
if backup:
# pylint: disable-next=import-outside-toplevel
from .backup import backup_addon_before_update
from .backup import backup_addon_before_update # noqa: PLC0415
await backup_addon_before_update(hass, addon, addon_name, installed_version)
@@ -50,8 +49,7 @@ async def update_core(hass: HomeAssistant, version: str | None, backup: bool) ->
client = get_supervisor_client(hass)
if backup:
# pylint: disable-next=import-outside-toplevel
from .backup import backup_core_before_update
from .backup import backup_core_before_update # noqa: PLC0415
await backup_core_before_update(hass)
@@ -71,8 +69,7 @@ async def update_os(hass: HomeAssistant, version: str | None, backup: bool) -> N
client = get_supervisor_client(hass)
if backup:
# pylint: disable-next=import-outside-toplevel
from .backup import backup_core_before_update
from .backup import backup_core_before_update # noqa: PLC0415
await backup_core_before_update(hass)

View File

@@ -100,9 +100,11 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
try:
response = await self._api.route(
transport_mode=TransportMode(params.travel_mode),
origin=here_routing.Place(params.origin[0], params.origin[1]),
origin=here_routing.Place(
float(params.origin[0]), float(params.origin[1])
),
destination=here_routing.Place(
params.destination[0], params.destination[1]
float(params.destination[0]), float(params.destination[1])
),
routing_mode=params.route_mode,
arrival_time=params.arrival,
@@ -133,8 +135,8 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
def _parse_routing_response(self, response: dict[str, Any]) -> HERETravelTimeData:
"""Parse the routing response dict to a HERETravelTimeData."""
distance: float = 0.0
duration: float = 0.0
duration_in_traffic: float = 0.0
duration: int = 0
duration_in_traffic: int = 0
for section in response["routes"][0]["sections"]:
distance += DistanceConverter.convert(
@@ -167,8 +169,8 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
destination_name = names[0]["value"]
return HERETravelTimeData(
attribution=None,
duration=round(duration / 60),
duration_in_traffic=round(duration_in_traffic / 60),
duration=duration,
duration_in_traffic=duration_in_traffic,
distance=distance,
origin=f"{mapped_origin_lat},{mapped_origin_lon}",
destination=f"{mapped_destination_lat},{mapped_destination_lon}",
@@ -271,13 +273,13 @@ class HERETransitDataUpdateCoordinator(
UnitOfLength.METERS,
UnitOfLength.KILOMETERS,
)
duration: float = sum(
duration: int = sum(
section["travelSummary"]["duration"] for section in sections
)
return HERETravelTimeData(
attribution=attribution,
duration=round(duration / 60),
duration_in_traffic=round(duration / 60),
duration=duration,
duration_in_traffic=duration,
distance=distance,
origin=f"{mapped_origin_lat},{mapped_origin_lon}",
destination=f"{mapped_destination_lat},{mapped_destination_lon}",

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/here_travel_time",
"iot_class": "cloud_polling",
"loggers": ["here_routing", "here_transit", "homeassistant.helpers.location"],
"requirements": ["here-routing==1.0.1", "here-transit==1.2.1"]
"requirements": ["here-routing==1.2.0", "here-transit==1.2.1"]
}

View File

@@ -6,6 +6,8 @@ from dataclasses import dataclass
from datetime import datetime
from typing import TypedDict
from here_routing import RoutingMode
class HERETravelTimeData(TypedDict):
"""Routing information."""
@@ -27,6 +29,6 @@ class HERETravelTimeAPIParams:
destination: list[str]
origin: list[str]
travel_mode: str
route_mode: str
route_mode: RoutingMode
arrival: datetime | None
departure: datetime | None

View File

@@ -55,14 +55,18 @@ def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...]
icon=ICONS.get(travel_mode, ICON_CAR),
key=ATTR_DURATION,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTime.MINUTES,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.MINUTES,
),
SensorEntityDescription(
translation_key="duration_in_traffic",
icon=ICONS.get(travel_mode, ICON_CAR),
key=ATTR_DURATION_IN_TRAFFIC,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTime.MINUTES,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.MINUTES,
),
SensorEntityDescription(
translation_key="distance",

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.74", "babel==2.15.0"]
"requirements": ["holidays==0.75", "babel==2.15.0"]
}

View File

@@ -21,6 +21,7 @@
"documentation": "https://www.home-assistant.io/integrations/home_connect",
"iot_class": "cloud_push",
"loggers": ["aiohomeconnect"],
"requirements": ["aiohomeconnect==0.17.1"],
"quality_scale": "platinum",
"requirements": ["aiohomeconnect==0.18.1"],
"zeroconf": ["_homeconnect._tcp.local."]
}

View File

@@ -0,0 +1,71 @@
rules:
# Bronze
action-setup: done
appropriate-polling:
status: done
comment: |
Full polling is performed at the configuration entry setup and
device polling is performed when a CONNECTED or a PAIRED event is received.
If many CONNECTED or PAIRED events are received for a device within a short time span,
the integration will stop polling for that device and will create a repair issue.
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions: done
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: done
test-coverage: done
# Gold
devices: done
diagnostics: done
discovery-update-info: done
discovery: done
docs-data-update: done
docs-examples: done
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: done
entity-category: done
entity-device-class: done
entity-disabled-by-default:
status: done
comment: |
Event entities are disabled by default to prevent user confusion regarding
which events are supported by its appliance.
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow:
status: exempt
comment: |
This integration doesn't have settings in its configuration flow.
repair-issues: done
stale-devices: done
# Platinum
async-dependency: done
inject-websession: done
strict-typing: done

View File

@@ -309,8 +309,7 @@ class OptionsFlowHandler(OptionsFlow, ABC):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Set up the options flow."""
# pylint: disable-next=import-outside-toplevel
from homeassistant.components.zha.radio_manager import (
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
ZhaMultiPANMigrationHelper,
)
@@ -451,16 +450,11 @@ class OptionsFlowHandler(OptionsFlow, ABC):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Configure the Silicon Labs Multiprotocol add-on."""
# pylint: disable-next=import-outside-toplevel
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN
# pylint: disable-next=import-outside-toplevel
from homeassistant.components.zha.radio_manager import (
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN # noqa: PLC0415
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
ZhaMultiPANMigrationHelper,
)
# pylint: disable-next=import-outside-toplevel
from homeassistant.components.zha.silabs_multiprotocol import (
from homeassistant.components.zha.silabs_multiprotocol import ( # noqa: PLC0415
async_get_channel as async_get_zha_channel,
)
@@ -747,11 +741,8 @@ class OptionsFlowHandler(OptionsFlow, ABC):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Perform initial backup and reconfigure ZHA."""
# pylint: disable-next=import-outside-toplevel
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN
# pylint: disable-next=import-outside-toplevel
from homeassistant.components.zha.radio_manager import (
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN # noqa: PLC0415
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
ZhaMultiPANMigrationHelper,
)

View File

@@ -67,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeeConfigEntry) -> boo
entry.runtime_data = homee
entry.async_on_unload(homee.disconnect)
def _connection_update_callback(connected: bool) -> None:
async def _connection_update_callback(connected: bool) -> None:
"""Call when the device is notified of changes."""
if connected:
_LOGGER.warning("Reconnected to Homee at %s", entry.data[CONF_HOST])

View File

@@ -28,6 +28,7 @@ class HomeeEntity(Entity):
self._entry = entry
node = entry.runtime_data.get_node_by_id(attribute.node_id)
# Homee hub itself has node-id -1
assert node is not None
if node.id == -1:
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.runtime_data.settings.uid)},
@@ -41,6 +42,8 @@ class HomeeEntity(Entity):
model=get_name_for_enum(NodeProfile, node.profile),
via_device=(DOMAIN, entry.runtime_data.settings.uid),
)
if attribute.name:
self._attr_name = attribute.name
self._host_connected = entry.runtime_data.connected
@@ -79,7 +82,7 @@ class HomeeEntity(Entity):
def _on_node_updated(self, attribute: HomeeAttribute) -> None:
self.schedule_update_ha_state()
def _on_connection_changed(self, connected: bool) -> None:
async def _on_connection_changed(self, connected: bool) -> None:
self._host_connected = connected
self.schedule_update_ha_state()
@@ -166,6 +169,6 @@ class HomeeNodeEntity(Entity):
def _on_node_updated(self, node: HomeeNode) -> None:
self.schedule_update_ha_state()
def _on_connection_changed(self, connected: bool) -> None:
async def _on_connection_changed(self, connected: bool) -> None:
self._host_connected = connected
self.schedule_update_ha_state()

View File

@@ -58,9 +58,13 @@ class HomeeLock(HomeeEntity, LockEntity):
AttributeChangedBy, self._attribute.changed_by
)
if self._attribute.changed_by == AttributeChangedBy.USER:
changed_id = self._entry.runtime_data.get_user_by_id(
user = self._entry.runtime_data.get_user_by_id(
self._attribute.changed_by_id
).username
)
if user is not None:
changed_id = user.username
else:
changed_id = "Unknown"
return f"{changed_by_name}-{changed_id}"

View File

@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["homee"],
"quality_scale": "bronze",
"requirements": ["pyHomee==1.2.8"]
"requirements": ["pyHomee==1.2.10"]
}

View File

@@ -177,9 +177,9 @@
"state_attributes": {
"event_type": {
"state": {
"upper": "[%key;component::homee::entity::event::button_state::state_attributes::event_type::state::upper%]",
"lower": "[%key;component::homee::entity::event::button_state::state_attributes::event_type::state::lower%]",
"released": "[%key;component::homee::entity::event::button_state::state_attributes::event_type::state::released%]"
"upper": "[%key:component::homee::entity::event::button_state::state_attributes::event_type::state::upper%]",
"lower": "[%key:component::homee::entity::event::button_state::state_attributes::event_type::state::lower%]",
"released": "[%key:component::homee::entity::event::button_state::state_attributes::event_type::state::released%]"
}
}
}
@@ -189,7 +189,7 @@
"state_attributes": {
"event_type": {
"state": {
"release": "[%key;component::homee::entity::event::button_state::state_attributes::event_type::state::released%]",
"release": "[%key:component::homee::entity::event::button_state::state_attributes::event_type::state::released%]",
"up": "Up",
"down": "Down",
"stop": "Stop",

View File

@@ -28,6 +28,7 @@ def get_device_class(
) -> SwitchDeviceClass:
"""Check device class of Switch according to node profile."""
node = config_entry.runtime_data.get_node_by_id(attribute.node_id)
assert node is not None
if node.profile in [
NodeProfile.ON_OFF_PLUG,
NodeProfile.METERING_PLUG,

View File

@@ -355,11 +355,10 @@ class HomekitControllerFlowHandler(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="ignored_model")
# Late imports in case BLE is not available
# pylint: disable-next=import-outside-toplevel
from aiohomekit.controller.ble.discovery import BleDiscovery
# pylint: disable-next=import-outside-toplevel
from aiohomekit.controller.ble.manufacturer_data import HomeKitAdvertisement
from aiohomekit.controller.ble.discovery import BleDiscovery # noqa: PLC0415
from aiohomekit.controller.ble.manufacturer_data import ( # noqa: PLC0415
HomeKitAdvertisement,
)
mfr_data = discovery_info.manufacturer_data

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
"iot_class": "cloud_push",
"loggers": ["homematicip"],
"requirements": ["homematicip==2.0.5"]
"requirements": ["homematicip==2.0.6"]
}

View File

@@ -23,9 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeWizardConfigEntry) -
api: HomeWizardEnergy
is_battery = entry.unique_id.startswith("HWE-BAT") if entry.unique_id else False
if (token := entry.data.get(CONF_TOKEN)) and is_battery:
if token := entry.data.get(CONF_TOKEN):
api = HomeWizardEnergyV2(
entry.data[CONF_IP_ADDRESS],
token=token,
@@ -37,8 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeWizardConfigEntry) -
clientsession=async_get_clientsession(hass),
)
if is_battery:
await async_check_v2_support_and_create_issue(hass, entry)
await async_check_v2_support_and_create_issue(hass, entry)
coordinator = HWEnergyDeviceUpdateCoordinator(hass, entry, api)
try:

View File

@@ -8,7 +8,13 @@ import logging
from homeassistant.const import Platform
DOMAIN = "homewizard"
PLATFORMS = [Platform.BUTTON, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
PLATFORMS = [
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
]
LOGGER = logging.getLogger(__package__)

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import Callable, Coroutine
from typing import Any, Concatenate
from homewizard_energy.errors import DisabledError, RequestError
from homewizard_energy.errors import DisabledError, RequestError, UnauthorizedError
from homeassistant.exceptions import HomeAssistantError
@@ -41,5 +41,10 @@ def homewizard_exception_handler[_HomeWizardEntityT: HomeWizardEntity, **_P](
translation_domain=DOMAIN,
translation_key="api_disabled",
) from ex
except UnauthorizedError as ex:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="api_unauthorized",
) from ex
return handler

View File

@@ -0,0 +1,89 @@
"""Support for HomeWizard select platform."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from homewizard_energy import HomeWizardEnergy
from homewizard_energy.models import Batteries, CombinedModels as DeviceResponseEntry
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import HomeWizardConfigEntry, HWEnergyDeviceUpdateCoordinator
from .entity import HomeWizardEntity
from .helpers import homewizard_exception_handler
PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class HomeWizardSelectEntityDescription(SelectEntityDescription):
"""Class describing HomeWizard select entities."""
available_fn: Callable[[DeviceResponseEntry], bool]
create_fn: Callable[[DeviceResponseEntry], bool]
current_fn: Callable[[DeviceResponseEntry], str | None]
set_fn: Callable[[HomeWizardEnergy, str], Awaitable[Any]]
DESCRIPTIONS = [
HomeWizardSelectEntityDescription(
key="battery_group_mode",
translation_key="battery_group_mode",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
options=[Batteries.Mode.ZERO, Batteries.Mode.STANDBY, Batteries.Mode.TO_FULL],
available_fn=lambda x: x.batteries is not None,
create_fn=lambda x: x.batteries is not None,
current_fn=lambda x: x.batteries.mode if x.batteries else None,
set_fn=lambda api, mode: api.batteries(mode=Batteries.Mode(mode)),
),
]
async def async_setup_entry(
hass: HomeAssistant,
entry: HomeWizardConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up HomeWizard select based on a config entry."""
async_add_entities(
HomeWizardSelectEntity(
coordinator=entry.runtime_data,
description=description,
)
for description in DESCRIPTIONS
if description.create_fn(entry.runtime_data.data)
)
class HomeWizardSelectEntity(HomeWizardEntity, SelectEntity):
"""Defines a HomeWizard select entity."""
entity_description: HomeWizardSelectEntityDescription
def __init__(
self,
coordinator: HWEnergyDeviceUpdateCoordinator,
description: HomeWizardSelectEntityDescription,
) -> None:
"""Initialize the switch."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{description.key}"
@property
def current_option(self) -> str | None:
"""Return the selected entity option to represent the entity state."""
return self.entity_description.current_fn(self.coordinator.data)
@homewizard_exception_handler
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
await self.entity_description.set_fn(self.coordinator.api, option)
await self.coordinator.async_request_refresh()

View File

@@ -152,14 +152,27 @@
"cloud_connection": {
"name": "Cloud connection"
}
},
"select": {
"battery_group_mode": {
"name": "Battery group mode",
"state": {
"zero": "Zero mode",
"to_full": "Manual charge mode",
"standby": "Standby"
}
}
}
},
"exceptions": {
"api_disabled": {
"message": "The local API is disabled."
},
"api_unauthorized": {
"message": "The local API is unauthorized. Restore API access by following the instructions in the repair issue."
},
"communication_error": {
"message": "An error occurred while communicating with HomeWizard device"
"message": "An error occurred while communicating with your HomeWizard Energy device"
}
},
"issues": {

View File

@@ -278,8 +278,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
ssl_certificate is not None
and (hass.config.external_url or hass.config.internal_url) is None
):
# pylint: disable-next=import-outside-toplevel
from homeassistant.components.cloud import (
from homeassistant.components.cloud import ( # noqa: PLC0415
CloudNotAvailable,
async_remote_ui_url,
)
@@ -511,12 +510,14 @@ class HomeAssistantHTTP:
) -> None:
"""Register a folder or file to serve as a static path."""
frame.report_usage(
"calls hass.http.register_static_path which is deprecated because "
"it does blocking I/O in the event loop, instead "
"calls hass.http.register_static_path which "
"does blocking I/O in the event loop, instead "
"call `await hass.http.async_register_static_paths("
f'[StaticPathConfig("{url_path}", "{path}", {cache_headers})])`',
exclude_integrations={"http"},
core_behavior=frame.ReportBehavior.LOG,
core_behavior=frame.ReportBehavior.ERROR,
core_integration_behavior=frame.ReportBehavior.ERROR,
custom_integration_behavior=frame.ReportBehavior.ERROR,
breaks_in_ha_version="2025.7",
)
configs = [StaticPathConfig(url_path, path, cache_headers)]

View File

@@ -136,8 +136,7 @@ async def process_wrong_login(request: Request) -> None:
_LOGGER.warning(log_msg)
# Circular import with websocket_api
# pylint: disable=import-outside-toplevel
from homeassistant.components import persistent_notification
from homeassistant.components import persistent_notification # noqa: PLC0415
persistent_notification.async_create(
hass, notification_msg, "Login attempt failed", NOTIFICATION_ID_LOGIN

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"quality_scale": "silver",
"requirements": ["aioautomower==2025.5.1"]
"requirements": ["aioautomower==2025.6.0"]
}

View File

@@ -444,8 +444,9 @@ class TimerManager:
timer.finish()
if timer.conversation_command:
# pylint: disable-next=import-outside-toplevel
from homeassistant.components.conversation import async_converse
from homeassistant.components.conversation import ( # noqa: PLC0415
async_converse,
)
self.hass.async_create_background_task(
async_converse(

View File

@@ -1,6 +1,5 @@
"""The JuiceNet integration."""
from datetime import timedelta
import logging
import aiohttp
@@ -14,9 +13,9 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
from .coordinator import JuiceNetCoordinator
from .device import JuiceNetApi
_LOGGER = logging.getLogger(__name__)
@@ -74,20 +73,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return False
_LOGGER.debug("%d JuiceNet device(s) found", len(juicenet.devices))
async def async_update_data():
"""Update all device states from the JuiceNet API."""
for device in juicenet.devices:
await device.update_state(True)
return True
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name="JuiceNet",
update_method=async_update_data,
update_interval=timedelta(seconds=30),
)
coordinator = JuiceNetCoordinator(hass, entry, juicenet)
await coordinator.async_config_entry_first_refresh()

View File

@@ -0,0 +1,33 @@
"""The JuiceNet integration."""
from datetime import timedelta
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .device import JuiceNetApi
_LOGGER = logging.getLogger(__name__)
class JuiceNetCoordinator(DataUpdateCoordinator[None]):
"""Coordinator for JuiceNet."""
def __init__(
self, hass: HomeAssistant, entry: ConfigEntry, juicenet_api: JuiceNetApi
) -> None:
"""Initialize the JuiceNet coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=entry,
name="JuiceNet",
update_interval=timedelta(seconds=30),
)
self.juicenet_api = juicenet_api
async def _async_update_data(self) -> None:
for device in self.juicenet_api.devices:
await device.update_state(True)

View File

@@ -1,19 +1,21 @@
"""Adapter to wrap the pyjuicenet api for home assistant."""
from pyjuicenet import Api, Charger
class JuiceNetApi:
"""Represent a connection to JuiceNet."""
def __init__(self, api):
def __init__(self, api: Api) -> None:
"""Create an object from the provided API instance."""
self.api = api
self._devices = []
self._devices: list[Charger] = []
async def setup(self):
async def setup(self) -> None:
"""JuiceNet device setup."""
self._devices = await self.api.get_devices()
@property
def devices(self) -> list:
def devices(self) -> list[Charger]:
"""Get a list of devices managed by this account."""
return self._devices

View File

@@ -3,21 +3,19 @@
from pyjuicenet import Charger
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import JuiceNetCoordinator
class JuiceNetDevice(CoordinatorEntity):
class JuiceNetEntity(CoordinatorEntity[JuiceNetCoordinator]):
"""Represent a base JuiceNet device."""
_attr_has_entity_name = True
def __init__(
self, device: Charger, key: str, coordinator: DataUpdateCoordinator
self, device: Charger, key: str, coordinator: JuiceNetCoordinator
) -> None:
"""Initialise the sensor."""
super().__init__(coordinator)

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from dataclasses import dataclass
from pyjuicenet import Api, Charger
from pyjuicenet import Charger
from homeassistant.components.number import (
DEFAULT_MAX_VALUE,
@@ -14,10 +14,11 @@ from homeassistant.components.number import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
from .entity import JuiceNetDevice
from .coordinator import JuiceNetCoordinator
from .device import JuiceNetApi
from .entity import JuiceNetEntity
@dataclass(frozen=True, kw_only=True)
@@ -47,8 +48,8 @@ async def async_setup_entry(
) -> None:
"""Set up the JuiceNet Numbers."""
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
api: Api = juicenet_data[JUICENET_API]
coordinator = juicenet_data[JUICENET_COORDINATOR]
api: JuiceNetApi = juicenet_data[JUICENET_API]
coordinator: JuiceNetCoordinator = juicenet_data[JUICENET_COORDINATOR]
entities = [
JuiceNetNumber(device, description, coordinator)
@@ -58,7 +59,7 @@ async def async_setup_entry(
async_add_entities(entities)
class JuiceNetNumber(JuiceNetDevice, NumberEntity):
class JuiceNetNumber(JuiceNetEntity, NumberEntity):
"""Implementation of a JuiceNet number."""
entity_description: JuiceNetNumberEntityDescription
@@ -67,7 +68,7 @@ class JuiceNetNumber(JuiceNetDevice, NumberEntity):
self,
device: Charger,
description: JuiceNetNumberEntityDescription,
coordinator: DataUpdateCoordinator,
coordinator: JuiceNetCoordinator,
) -> None:
"""Initialise the number."""
super().__init__(device, description.key, coordinator)

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
from pyjuicenet import Charger
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
@@ -21,7 +23,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
from .entity import JuiceNetDevice
from .coordinator import JuiceNetCoordinator
from .device import JuiceNetApi
from .entity import JuiceNetEntity
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
@@ -74,8 +78,8 @@ async def async_setup_entry(
) -> None:
"""Set up the JuiceNet Sensors."""
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
api = juicenet_data[JUICENET_API]
coordinator = juicenet_data[JUICENET_COORDINATOR]
api: JuiceNetApi = juicenet_data[JUICENET_API]
coordinator: JuiceNetCoordinator = juicenet_data[JUICENET_COORDINATOR]
entities = [
JuiceNetSensorDevice(device, coordinator, description)
@@ -85,11 +89,14 @@ async def async_setup_entry(
async_add_entities(entities)
class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity):
class JuiceNetSensorDevice(JuiceNetEntity, SensorEntity):
"""Implementation of a JuiceNet sensor."""
def __init__(
self, device, coordinator, description: SensorEntityDescription
self,
device: Charger,
coordinator: JuiceNetCoordinator,
description: SensorEntityDescription,
) -> None:
"""Initialise the sensor."""
super().__init__(device, description.key, coordinator)

View File

@@ -2,13 +2,17 @@
from typing import Any
from pyjuicenet import Charger
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
from .entity import JuiceNetDevice
from .coordinator import JuiceNetCoordinator
from .device import JuiceNetApi
from .entity import JuiceNetEntity
async def async_setup_entry(
@@ -18,20 +22,20 @@ async def async_setup_entry(
) -> None:
"""Set up the JuiceNet switches."""
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
api = juicenet_data[JUICENET_API]
coordinator = juicenet_data[JUICENET_COORDINATOR]
api: JuiceNetApi = juicenet_data[JUICENET_API]
coordinator: JuiceNetCoordinator = juicenet_data[JUICENET_COORDINATOR]
async_add_entities(
JuiceNetChargeNowSwitch(device, coordinator) for device in api.devices
)
class JuiceNetChargeNowSwitch(JuiceNetDevice, SwitchEntity):
class JuiceNetChargeNowSwitch(JuiceNetEntity, SwitchEntity):
"""Implementation of a JuiceNet switch."""
_attr_translation_key = "charge_now"
def __init__(self, device, coordinator):
def __init__(self, device: Charger, coordinator: JuiceNetCoordinator) -> None:
"""Initialise the switch."""
super().__init__(device, "charge_now", coordinator)

View File

@@ -2,15 +2,14 @@
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from .const import DOMAIN, PLATFORMS
from .coordinator import JustNimbusCoordinator
from .const import PLATFORMS
from .coordinator import JustNimbusConfigEntry, JustNimbusCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: JustNimbusConfigEntry) -> bool:
"""Set up JustNimbus from a config entry."""
if "zip_code" in entry.data:
coordinator = JustNimbusCoordinator(hass, entry)
@@ -18,13 +17,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
raise ConfigEntryAuthFailed
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: JustNimbusConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -16,13 +16,17 @@ from .const import CONF_ZIP_CODE, DOMAIN
_LOGGER = logging.getLogger(__name__)
type JustNimbusConfigEntry = ConfigEntry[JustNimbusCoordinator]
class JustNimbusCoordinator(DataUpdateCoordinator[justnimbus.JustNimbusModel]):
"""Data update coordinator."""
config_entry: ConfigEntry
config_entry: JustNimbusConfigEntry
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
def __init__(
self, hass: HomeAssistant, config_entry: JustNimbusConfigEntry
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,

View File

@@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_CLIENT_ID,
EntityCategory,
@@ -24,8 +23,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from . import JustNimbusCoordinator
from .const import DOMAIN
from .coordinator import JustNimbusConfigEntry, JustNimbusCoordinator
from .entity import JustNimbusEntity
@@ -102,16 +100,15 @@ SENSOR_TYPES = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: JustNimbusConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the JustNimbus sensor."""
coordinator: JustNimbusCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
JustNimbusSensor(
device_id=entry.data[CONF_CLIENT_ID],
description=description,
coordinator=coordinator,
coordinator=entry.runtime_data,
)
for description in SENSOR_TYPES
)

View File

@@ -3,26 +3,22 @@
from __future__ import annotations
from dataclasses import dataclass
import logging
from typing import TYPE_CHECKING
from kaleidescape import Device as KaleidescapeDevice, KaleidescapeError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from .const import DOMAIN
if TYPE_CHECKING:
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Event, HomeAssistant
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE, Platform.SENSOR]
type KaleidescapeConfigEntry = ConfigEntry[KaleidescapeDevice]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(
hass: HomeAssistant, entry: KaleidescapeConfigEntry
) -> bool:
"""Set up Kaleidescape from a config entry."""
device = KaleidescapeDevice(
entry.data[CONF_HOST], timeout=5, reconnect=True, reconnect_delay=5
@@ -36,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
f"Unable to connect to {entry.data[CONF_HOST]}: {err}"
) from err
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = device
entry.runtime_data = device
async def disconnect(event: Event) -> None:
await device.disconnect()
@@ -44,18 +40,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect)
)
entry.async_on_unload(device.disconnect)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: KaleidescapeConfigEntry
) -> bool:
"""Unload config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
await hass.data[DOMAIN][entry.entry_id].disconnect()
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@dataclass

View File

@@ -2,8 +2,8 @@
from __future__ import annotations
from datetime import datetime
import logging
from typing import TYPE_CHECKING
from kaleidescape import const as kaleidescape_const
@@ -12,19 +12,13 @@ from homeassistant.components.media_player import (
MediaPlayerEntityFeature,
MediaPlayerState,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.dt import utcnow
from .const import DOMAIN
from . import KaleidescapeConfigEntry
from .entity import KaleidescapeEntity
if TYPE_CHECKING:
from datetime import datetime
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
KALEIDESCAPE_PLAYING_STATES = [
kaleidescape_const.PLAY_STATUS_PLAYING,
kaleidescape_const.PLAY_STATUS_FORWARD,
@@ -39,11 +33,11 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: KaleidescapeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the platform from a config entry."""
entities = [KaleidescapeMediaPlayer(hass.data[DOMAIN][entry.entry_id])]
entities = [KaleidescapeMediaPlayer(entry.runtime_data)]
async_add_entities(entities)

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