Compare commits

...

270 Commits

Author SHA1 Message Date
Franck Nijhof
be40db3dff Bump version to 2024.12.0b4 2024-12-02 13:02:23 +01:00
Josef Zweck
c3c500955a Use format_mac correctly for acaia (#132062) 2024-12-02 12:59:41 +01:00
ashionky
1e5a5925e6 Bump refoss to v1.2.5 (#132051) 2024-12-02 12:59:37 +01:00
TimL
d956e4b11d Bump psymlight v0.1.4 (#132045) 2024-12-02 12:59:33 +01:00
J. Nick Koston
8ff8cd8b65 Bump aiohttp to 3.11.9 (#132036)
changelog: https://github.com/aio-libs/aiohttp/compare/v3.11.8...v3.11.9
2024-12-02 12:59:29 +01:00
Joost Lekkerkerker
fab35f227d Handle not found playlists in Spotify (#132033)
* Handle not found playlists

* Handle not found playlists

* Handle not found playlists

* Handle not found playlists

* Handle not found playlists

* Update homeassistant/components/spotify/coordinator.py

---------

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2024-12-02 12:59:26 +01:00
Joost Lekkerkerker
e4d19541f5 Bump spotifyaio to 0.8.11 (#132032) 2024-12-02 12:59:22 +01:00
Joost Lekkerkerker
6b6fc6bbeb Bump yt-dlp to 2024.11.18 (#132026) 2024-12-02 12:59:18 +01:00
J. Nick Koston
f2bafee84a Bump yarl to 1.18.3 (#132025)
changelog: https://github.com/aio-libs/yarl/compare/v1.18.0...v1.18.3
2024-12-02 12:59:15 +01:00
J. Nick Koston
4e0cdb0537 Bump propcache to 0.2.1 (#132022) 2024-12-02 12:59:06 +01:00
Richard Kroegel
79c919f62d Bump bimmer_connected to 0.17.2 (#132005) 2024-12-02 12:58:53 +01:00
Erik Montnemery
b6dec11487 Freeze integration setup timeout for recorder during non-live data migration (#131998) 2024-12-02 12:58:44 +01:00
Bouwe Westerdijk
e2073d7762 Bugfix for Plugwise, small code optimization (#131990) 2024-12-02 12:58:37 +01:00
Paulus Schoutsen
d7428786cd Bump version to 2024.12.0b3 2024-12-01 03:14:16 +00:00
J. Nick Koston
673bdcc556 Reduce precision loss when converting HomeKit temperature (#131973) 2024-12-01 03:14:11 +00:00
J. Nick Koston
e8ef990e72 Strip trailing spaces from HomeKit names (#131971) 2024-12-01 03:14:10 +00:00
starkillerOG
0d155c416a Bump reolink_aio to 0.11.4 (#131957) 2024-12-01 03:14:10 +00:00
Andrew Jackson
e48be5c406 Bump aiomealie to 0.9.4 (#131951) 2024-12-01 03:14:09 +00:00
Matthias Alphart
787a1613ec Fix KNX IP Secure tunnelling endpoint selection with keyfile (#131941) 2024-12-01 03:14:08 +00:00
Raphael Hehl
bb847b346d Bump uiprotect to 6.6.4 (#131931) 2024-12-01 03:14:07 +00:00
Jc2k
e9b34eaad0 Bump aiohomekit to 3.2.7 (#131924) 2024-12-01 03:14:06 +00:00
Marcel van der Veldt
572347025b Fix media player join action for Music Assistant integration (#131910)
* Fix media player join action for Music Assistant integration

* Add tests for join/unjoin

* add one more test
2024-12-01 03:14:05 +00:00
Josef Zweck
29e80e56c6 Bump aioacaia to 0.1.10 (#131906) 2024-12-01 03:14:04 +00:00
Oliver
b60b2fdd7c Bump denonavr to v1.0.1 (#131882) 2024-12-01 03:14:04 +00:00
Josef Zweck
aaf3f61675 Guard against hostname change in lamarzocco discovery (#131873)
* Guard against hostname change in lamarzocco discovery

* switch to abort_entries_match
2024-12-01 03:13:50 +00:00
karwosts
5bf972ff16 Fix history stats count update immediately after change (#131856)
* Fix history stats count update immediately after change

* rerun CI
2024-12-01 03:13:45 +00:00
Glenn Vandeuren (aka Iondependent)
8eb52edabf Fix modbus state not dumped on restart (#131319)
* Fix modbus state not dumped on restart

* Update test_init.py

* Set event back  to stop

* Update test_init.py

---------

Co-authored-by: VandeurenGlenn <8685280+VandeurenGlenn@users.noreply.github.com>
2024-12-01 03:13:44 +00:00
J. Nick Koston
4326689f52 Bump SQLAlchemy to 2.0.36 (#126683)
* Bump SQLAlchemy to 2.0.35

changelog: https://docs.sqlalchemy.org/en/20/changelog/changelog_20.html#change-2.0.35

* fix mocking

* adjust to .36

* remove ignored as these are now typed

* fix SQLAlchemy
2024-12-01 03:13:44 +00:00
Franck Nijhof
06838c0280 Bump version to 2024.12.0b2 2024-11-28 21:02:37 +01:00
Richard Kroegel
f97d96e3ae Add captcha to BMW ConfigFlow (#131351)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2024-11-28 21:02:23 +01:00
karwosts
ee960933db Fix flaky test in history stats (#131869) 2024-11-28 20:55:34 +01:00
Joost Lekkerkerker
2ea0c54788 Only download translation strings we have defined (#131864) 2024-11-28 20:55:31 +01:00
Madhan
dd18672341 Bump PyMetEireann to 2024.11.0 (#131860)
Co-authored-by: Joostlek <joostlek@outlook.com>
2024-11-28 20:55:26 +01:00
Bram Kragten
ac4ae0430e Update frontend to 20241127.1 (#131855) 2024-11-28 20:55:23 +01:00
Joost Lekkerkerker
eeb63d42a0 Bump pyatv to 0.16.0 (#131852) 2024-11-28 20:55:20 +01:00
Michael
9d48f36754 Allow empty trigger sentence responses in conversations (#131849)
allow empty trigger sentence responses
2024-11-28 20:55:16 +01:00
Joost Lekkerkerker
157198bf41 Make wake word selection part of configuration (#131832) 2024-11-28 20:55:13 +01:00
Joost Lekkerkerker
be25b9d4d0 Bump spotifyaio to 0.8.10 (#131827) 2024-11-28 20:55:10 +01:00
epenet
e08b71086f Fix more flaky translation checks (#131824) 2024-11-28 20:55:07 +01:00
Norbert Rittel
9677c6e24c Remove wrong plural "s" in 'todo.remove_item' action (#131814) 2024-11-28 20:55:03 +01:00
Franck Nijhof
e2cda54473 Ensure custom integrations are assigned the custom IQS scale (#131795) 2024-11-28 20:55:00 +01:00
epenet
3ca49dc8a6 Bump samsungtvws to 2.7.1 (#131784) 2024-11-28 20:54:57 +01:00
Joost Lekkerkerker
80bc70771e Remove Spotify featured playlists and categories from media browser (#131758) 2024-11-28 20:54:54 +01:00
Erik Montnemery
7ab1bfcf1f Improve recorder history queries (#131702)
* Improve recorder history queries

* Remove some comments

* Update StatesManager._oldest_ts when adding pending state

* Update after review

* Improve tests

* Improve post-purge logic

* Avoid calling dt_util.utc_to_timestamp in new code

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2024-11-28 20:54:50 +01:00
Richard Kroegel
99f8dbd278 Bump bimmer_connected to 0.17.0 (#131352) 2024-11-28 20:54:46 +01:00
Franck Nijhof
3af0bc2c33 Bump version to 2024.12.0b1 2024-11-28 08:44:28 +01:00
TheJulianJES
b8c4ce932c Fix Home Connect microwave programs (#131782) 2024-11-28 08:44:14 +01:00
puddly
0a3a3edf77 Bump ZHA to 0.0.41 (#131776) 2024-11-28 08:44:11 +01:00
J. Nick Koston
71376229f6 Bump aioesphomeapi to 27.0.3 (#131773) 2024-11-28 08:44:07 +01:00
Manu
c9dde419a2 Fix rounding of attributes in Habitica integration (#131772) 2024-11-28 08:44:04 +01:00
Josef Zweck
2fc01a02db Bump pylamarzocco to 1.2.12 (#131765) 2024-11-28 08:44:01 +01:00
J. Nick Koston
f02d2344fc Bump uiprotect to 6.6.3 (#131764) 2024-11-28 08:43:58 +01:00
Joost Lekkerkerker
509311ac19 Remove Spotify audio feature sensors (#131754) 2024-11-28 08:43:54 +01:00
J. Nick Koston
47e7c4f1c1 Bump orjson to 3.10.12 (#131752)
changelog: https://github.com/ijl/orjson/compare/3.10.11...3.10.12
2024-11-28 08:43:51 +01:00
J. Nick Koston
c9d3ba900e Bump aiohttp to 3.11.8 (#131744) 2024-11-28 08:43:48 +01:00
Allen Porter
74a3d11aea Add a missing rainbird data description (#131740) 2024-11-28 08:43:45 +01:00
Marcel van der Veldt
897abc114e Bump music assistant client 1.0.8 (#131739) 2024-11-28 08:43:41 +01:00
Josef Zweck
3fff3003f2 Add missing data_description for lamarzocco OptionsFlow (#131708) 2024-11-28 08:43:37 +01:00
Franck Nijhof
db5c93f96d Bump version to 2024.12.0b0 2024-11-27 18:36:24 +01:00
epenet
e04b6f0cd8 Add quality scale hassfest check for config-entry-unload (#131720)
* Add dataclass to hassfest quality_scale

* Add basic check for config-entry-unloading

* Future-proof with a list of errors
2024-11-27 18:17:53 +01:00
Jan Bouwhuis
a6cb6fd239 Create MQTT device referenced by via device (#131588) 2024-11-27 18:12:46 +01:00
Paul Bottein
e8975cffe6 Update hash regex for frontend file in tests (#131742) 2024-11-27 18:07:26 +01:00
Paulus Schoutsen
ae34a6b375 Do not double expose scripts in LLM tools (#131726) 2024-11-27 18:04:08 +01:00
Raphael Hehl
1f1fdf80db Unifiprotect replace direct mocks with MockConfigEntry for test_async_ufp_instance_for_config_entry_ids (#131736)
Co-authored-by: J. Nick Koston <nick@koston.org>
2024-11-27 11:03:34 -06:00
Lutz
fda178da23 Add video event proxy endpoint for unifiprotect (#129980)
Co-authored-by: J. Nick Koston <nick@koston.org>
2024-11-27 11:03:21 -06:00
G Johansson
1450fe0880 Improve test quality in alarm_control_panel (#130541) 2024-11-27 17:49:02 +01:00
Abílio Costa
e4e9d76b45 Raise error if sensor has translated and hardcoded unit (#131657) 2024-11-27 17:45:53 +01:00
Marcel van der Veldt
3485ce9c71 Add actions to Music Assistant integration (#129515)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2024-11-27 17:43:48 +01:00
Michael Hansen
3eb483c1b0 Bump intents to 2024.11.27 (#131727) 2024-11-27 17:42:59 +01:00
J. Diego Rodríguez Royo
c2d6599736 Home connect program select entities (#126157)
* Home connect selector for programs

* Mark program switches as deprecated

* Simplified translation keys

* Improvements for program select entity

* Revert mark program switches as deprecated

* Return `None` if program is `None` or empty string

* Fix program format

* Use `is` instead of `==`

* Program selector entity selects program instead of start the selected program

* Fix typo

* Active and selected program

* Added ServiceValidationError

* Delete unnecessary `service` param at tests

* Use full program keys

* Fix again typos in programs states

* Use map for translations

* Add error handling for when the selected program is not registered on the program map

* Reverse map for programs and translation keys

* Remove stale string

* Log only once that the program is not part of the official Home Connect API specification

* pop programs

* Move `RE_CAMEL_CASE` to a better place

* Added warning if updated program is not valid

* Stale test function name

* Improve log about unknown program at update

* Add underscore before numbers in translation keys

* Added suggested changes

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

* Use target for adding an executor job

* Apply suggestions from code review

* Clean whitespace

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2024-11-27 16:34:41 +01:00
Bram Kragten
b2537a45e0 Update frontend to 20241127.0 (#131722) 2024-11-27 16:33:05 +01:00
epenet
e05401a922 Update snapshot to fix CI (#131725) 2024-11-27 16:28:36 +01:00
Raphael Hehl
f4b57617fb Unifiprotect fix missing domain check (#131724) 2024-11-27 16:23:59 +01:00
Manu
c21e221f65 Add data description to Iron OS integration (#131719) 2024-11-27 16:20:38 +01:00
epenet
d6f4a79b46 Remove workaround for flaky translation tests (#131628) 2024-11-27 08:37:36 -06:00
G Johansson
a7db35c76c Add horizontal swing support to ClimateEntity (#125578)
* Add horizontal swing support to ClimateEntity

* Fixes + tests

* Fixes
2024-11-27 15:06:46 +01:00
epenet
88feb8a7ad Fix ADS platform schema (#131701) 2024-11-27 14:47:17 +01:00
Abílio Costa
d8dd6a99b3 Use default translation on SensorEntity unit_of_measurement (#131633)
* Use translations on SensorEntity unit_of_measurement property

* Use default language for unit translation

* Update brother integration snapshot

* Update snapshots
2024-11-27 14:45:53 +01:00
epenet
137db5ac79 Bump samsungtvws to 2.7.0 (#131690) 2024-11-27 14:45:37 +01:00
Shay Levy
326f51a019 Bump aioshelly to 12.1.0 (#131714) 2024-11-27 15:20:47 +02:00
G Johansson
3464ffc53e Add open to Template lock (#129292)
* Add open to Template lock

* Update from review
2024-11-27 13:26:57 +01:00
Cyrill Raccaud
284fe17b1c Add time and offset config to Swiss public transport connections (#120357)
* add time and offset config for connections

* split the config flow

* fix arrival config

* add time_mode data description

* use delta as dict instead of string

* simplify the config_flow

* improve descriptions of config_flow

* improve config flow

* remove obsolete string

* switch priority of the config options

* improvements
2024-11-27 13:22:28 +01:00
Diogo Gomes
345c1fe0b2 Have Utility Meter monitor Timezone changes in configuration (#131112)
* listen to config changes for possible DST changes

* Add test

* check tz actually changed

* Update tests/components/utility_meter/test_sensor.py

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

* Update tests/components/utility_meter/test_sensor.py

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

* Clean up comment

---------

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2024-11-27 12:12:45 +01:00
Franck Nijhof
56b4733e4a Clean up early assignment in script response (#131691) 2024-11-27 10:24:06 +01:00
Louis Christ
96eae1221c Fix bluesound_group attribute in bluesound integration (#130815)
Co-authored-by: Robert Resch <robert@resch.dev>
2024-11-27 09:40:20 +01:00
Guido Schmitz
507bb4a685 Add data_description to devolo Home Network (#131511) 2024-11-27 09:26:19 +01:00
Petro31
33222436d2 Nested stop actions will now return response_variables (#126393)
fix-nested-stop-variable-response
2024-11-27 09:18:02 +01:00
G Johansson
1e05f98ddd Use report_usage for deprecation warning in alarm_control_panel (#130543)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-11-27 08:57:32 +01:00
epenet
2b939ce6ec Add translation checks for service exceptions (#131266)
* Add translation checks for service exceptions

* Adjust

* Remove invalid comment
2024-11-27 08:46:45 +01:00
Manu
00c4fa4146 Add missing section data_description to translation validator in hassfest (#131675)
Add missing data_description to translation validator in hassfest
2024-11-27 08:45:18 +01:00
TheJulianJES
605651f364 Bump ZHA to 0.0.40 (#131680) 2024-11-27 08:42:37 +01:00
Klaas Schoute
67ba44c3fa Use entity description class for Garages Amsterdam (#131672) 2024-11-27 08:42:19 +01:00
Bouwe Westerdijk
8bb0fab732 Bump plugwise to v1.6.0 and adapt (#131659) 2024-11-27 08:34:15 +01:00
dependabot[bot]
81d0bcde53 Bump docker/build-push-action from 6.9.0 to 6.10.0 (#131685)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-27 08:26:50 +01:00
J. Nick Koston
b8f81abbed Bump zeroconf to 0.136.2 (#131681) 2024-11-27 08:26:28 +01:00
Paulus Schoutsen
7e03100af2 Allow an LLM to see script response values (#131683) 2024-11-26 23:51:21 -06:00
Michael Hansen
46fe3dcbf1 Add wake word select for ESPHome Assist satellite (#131309)
* Add wake word select

* Fix linting

* Move to ESPHome

* Clean up and add more tests

* Update homeassistant/components/esphome/select.py

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2024-11-26 22:59:49 -05:00
Markus Jacobsen
a97eeaf189 Add Bang & Olufsen diagnostics (#131538)
* Add diagnostics

* Add tests for diagnostics

* Add media_player diagnostics

* Use media_player entity's state instead of registryentry

* Update tests

* Reorganize code
Remove context from media_player state

* Fix dict being read only
Simplify naming
Update test snapshot

* Update test snapshot
2024-11-26 20:56:36 -05:00
Jozef Kruszynski
40a4ff1c84 Adds media_browser functionality to the music assistant integration (#131577)
* Add test fixtures for all library loading

* Add media browser

* Add tests for media_browser
2024-11-26 20:52:08 -05:00
Markus Jacobsen
f04c50c59e Fix Bang & Olufsen WebSocket debug log and test (#131671)
* Fix test and debug message

* Reorder dict order
2024-11-26 20:48:46 -05:00
J. Nick Koston
dc62ef8bef Bump PySwitchbot to 0.54.0 (#131664) 2024-11-26 18:03:24 -06:00
Michael
70c8c57401 Dump ffmpeg stderr to ESPhome debug log (#130808)
* dump the stderr from ffmpeg to debug log

* add pid to indentify the ffmpeg process

* be more explosive :)

* move stderr task into _write_ffmpeg_data
2024-11-27 00:09:04 +01:00
J. Nick Koston
ce20670d84 Add a constraint for aiofiles to ensure it does not get downgraded (#131666) 2024-11-26 16:04:39 -06:00
Steven B.
4093a68cc0 Bump tplink python-kasa dependency to 0.8.0 (#131249) 2024-11-26 15:04:42 -06:00
prabhjotsbhatia-ca
1e6b96131a Bump androidtv to 0.0.75 (#131642) 2024-11-26 21:57:57 +01:00
Thomas55555
055c38a3c8 Don't enable number of collisions by default for Husqvarna Automower (#131665) 2024-11-26 21:38:46 +01:00
Duco Sebel
a0893bb9f7 Mark HomeWizard quality scale as platinum (#131663) 2024-11-26 21:33:45 +01:00
epenet
859daefeb8 Record current quality scale in renault (#131394) 2024-11-26 21:32:51 +01:00
dontinelli
06f9678414 Add quality scale for solarlog (#131440)
Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com>
2024-11-26 21:24:57 +01:00
Josef Zweck
a7113cff68 Record current IQS state for acaia (#131086) 2024-11-26 21:14:52 +01:00
Thomas55555
7a107cac41 Add PARALLEL_UPDATES to Husqvarna Automower (#131662) 2024-11-26 21:09:45 +01:00
Steven B.
f3964596de tplink: forward compatible typing and test changes for kasa 0.8 (#131623) 2024-11-26 13:50:26 -06:00
Manu
2edcda47b0 Add diagnostics platform to Habitica (#131489) 2024-11-26 20:02:01 +01:00
Marco Aceti
6e8f3d9393 Add missing sensors to Tuya CO2 Detector (#131313) 2024-11-26 20:00:13 +01:00
Franck Nijhof
f095aea5c3 Record current IQS state for Stookwijzer (#131592)
* Record current IQS state for Stookwijzer

* Also mark test coverage

* Process review comment
2024-11-26 19:59:19 +01:00
blackovercoat
35f6ae0759 Add support for single phase power meter aqcz in Tuya (#126470) 2024-11-26 19:38:52 +01:00
Andrew Jackson
132a8cc31b Detect ingress host used when adding a Mealie integration (#130418)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2024-11-26 19:30:05 +01:00
Martin Hjelmare
ccbbcbb264 Make set value template number option required (#131625) 2024-11-26 19:27:59 +01:00
Jake Martin
dfa7ababfb Raise HomeAssistantError if update fails (#129727) 2024-11-26 19:27:17 +01:00
Alexandre CUER
f1655c5d1a Use SensorEntityDescription in emoncms (#130451) 2024-11-26 19:25:00 +01:00
Per Øyvind Øygard
7d5ba342c6 Add base entity class for Touchline zones (#131094)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-11-26 19:23:18 +01:00
Duco Sebel
a252faf9af Add reconfiguration flow in HomeWizard (#131535) 2024-11-26 19:20:50 +01:00
Norbert Rittel
7ba0f54412 Clarify 'item' and 'rename' descriptions of 'update_item' action (#131336) 2024-11-26 19:19:27 +01:00
Duco Sebel
a9cab28474 Add DHCP configuration update in HomeWizard (#131547) 2024-11-26 19:17:04 +01:00
Jan-Philipp Benecke
a5becfaff0 Add more supported lines to London Underground (#131650) 2024-11-26 19:03:50 +01:00
Jan-Philipp Benecke
e31d398811 Add binary sensor to SABnzbd (#131651) 2024-11-26 19:01:19 +01:00
Michael Hansen
192ffc09ee Add area slot to response for cancel all timers (#131638)
Add area slot to response
2024-11-26 10:58:39 -06:00
Alexey ALERT Rubashёff
15bf0c728c Sync overkiz Atlantic Water Heater datetime before switching the away mode on (#127408)
Set device datetime before turning on the away mode
2024-11-26 17:45:28 +01:00
Jan-Philipp Benecke
883c6121cf Prevent changing email address in inexogy reauth (#131632)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-11-26 17:17:10 +01:00
jesperraemaekers
bf9e7e4a0c Bump Weheat wh-python to 2024.11.26 (#131630) 2024-11-26 17:00:51 +01:00
Jan-Philipp Benecke
1a71fbe427 Add intent to cancel all timers (#130873)
* Add intent to cancel all timers

* Add intent to llm test
2024-11-26 09:59:41 -06:00
starkillerOG
a2ebfe6e83 Add Reolink binning mode select entity (#131570) 2024-11-26 16:19:41 +01:00
Steven B.
0e88e22fd2 Bump ring_doorbell to 0.9.13 (#131627) 2024-11-26 16:14:39 +01:00
Simon Lamon
ee74a35417 Support time entities in time conditions (#124575)
Co-authored-by: Mark Bergsma <mark@wikked.net>
2024-11-26 15:37:31 +01:00
Steven B.
147679f803 Add live view camera entity to ring integration (#127579) 2024-11-26 15:20:25 +01:00
Jan-Philipp Benecke
9510ef56f9 Add configuration url to SABnzbd device info (#131617) 2024-11-26 08:39:21 -05:00
dotvav
1fc3194613 Add diagnostics to Palazzetti (#131608) 2024-11-26 14:07:37 +01:00
Duco Sebel
1ddc8a35c2 Add test to validate HomeWizard updates discovery info (#131540) 2024-11-26 13:14:59 +01:00
Klaas Schoute
f5d323679f Fix bug on creating entities with unknown state - Garages Amsterdam (#131619) 2024-11-26 13:07:32 +01:00
Michael
b0b72326d8 Add Update syrupy snapshots VScode task (#131536)
* add Update syrupy snapshots task

* don't use xdist
2024-11-26 13:02:17 +01:00
Lenn
41c7cc6e81 Bump motionblindsble to 0.1.3 (#131613) 2024-11-26 12:54:50 +01:00
Franck Nijhof
551d778a31 Merge branch 'master' into dev 2024-11-26 12:48:56 +01:00
Jan-Philipp Benecke
3af751c129 Fix SABnzbd number icon (#131615) 2024-11-26 12:40:02 +01:00
Klaas Schoute
9a999e8742 Use ConfigEntry runtime_data in Garages Amsterdam (#131611) 2024-11-26 12:30:50 +01:00
Franck Nijhof
0644d782cd 2024.11.3 (#131248) 2024-11-22 11:55:45 +01:00
Franck Nijhof
4ef50ffd88 Bump version to 2024.11.3 2024-11-22 11:05:59 +01:00
starkillerOG
bfcd4194f3 Bump reolink_aio to 0.11.2 (#131237) 2024-11-22 11:05:37 +01:00
rappenze
2f05240e4c Fix fibaro cover state is not always correct (#131206) 2024-11-22 11:05:34 +01:00
starkillerOG
44ad8081a3 Reolink log fast poll errors once (#131203) 2024-11-22 11:05:30 +01:00
Jesse Hills
780eaa8379 Fix typo in ESPHome repair text (#131200) 2024-11-22 11:05:26 +01:00
Norbert Rittel
75dcdfb087 Fix cast translation string (#131156) 2024-11-22 11:05:23 +01:00
Norbert Rittel
c88ff2ca44 Fix typo in name of "Alarm arm home instant" action (#131151) 2024-11-22 11:05:19 +01:00
Norbert Rittel
402c668f05 Replace "service" with "action" in zha:reconfigure_device (#131111)
Replace "service" with "action" in one description

As services are now actions in HA this needs to be fixed.
2024-11-22 11:05:14 +01:00
Álvaro Fernández Rojas
93b4570c04 Update aioairzone to v0.9.7 (#131033) 2024-11-22 10:57:08 +01:00
Álvaro Fernández Rojas
50a610914b Bump aioairzone to 0.9.6 (#130559)
* Update aioairzone to v0.9.6

Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>

* Remove _async_migrator_mac_empty and improve tests

Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>

* Remove WebServer empty mac fixes as requested by @epenet

Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>

---------

Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2024-11-22 10:57:04 +01:00
G Johansson
8db18181d0 Bump holidays to 0.61 (#130984) 2024-11-22 10:52:43 +01:00
G Johansson
335124acc6 Add missing catholic category in workday (#130983) 2024-11-22 10:52:40 +01:00
Norbert Rittel
24ccb9b894 Add more UI user-friendly description to six Supervisor actions (#130971) 2024-11-22 10:52:36 +01:00
Jan-Philipp Benecke
a75ce850b8 Strip whitespaces from host in ping config flow (#130970) 2024-11-22 10:52:33 +01:00
Renat Sibgatulin
4753510ace Bump aioairq to 0.4.3 (#130963) 2024-11-22 10:52:30 +01:00
ElmaxSrl
fc607ea7e5 Update elmax_api to v0.0.6.1 (#130917)
Co-authored-by: Alberto Geniola <albertogeniola@gmail.com>
2024-11-22 10:52:27 +01:00
Sergio Conde Gómez
477141c22a Unscape HTML Entities from RSS feeds (#130915)
* Unscape HTML Entities from RSS feeds

* Improve tests
2024-11-22 10:52:23 +01:00
Charles Yuan
aaa36adbcc Fixed Small Inaccuracy in Description String for myUplink (#130900) 2024-11-22 10:52:20 +01:00
J. Nick Koston
9447180c04 Bump bluetooth-adapters to 0.20.2 (#130877) 2024-11-22 10:52:17 +01:00
epenet
6853234f9d Pass config_entry explicitly in rachio (#130865) 2024-11-22 10:52:14 +01:00
G Johansson
6944ba0333 Use default device sensors also for AirQ devices in Sensibo (#130841) 2024-11-22 10:52:10 +01:00
starkillerOG
04bc041174 Reolink fix dev/entity id migration (#130836) 2024-11-22 10:52:07 +01:00
Glenn Waters
a024acf096 UPB integration: Change unique ID from int to string. (#130832) 2024-11-22 10:52:04 +01:00
hahn-th
5b1aca53ac Bump homematicip to 1.1.3 (#130824) 2024-11-22 10:52:00 +01:00
Michael
a588ced2e3 Fix unexpected stop of media playback via ffmpeg proxy for ESPhome devices (#130788)
disable writing progress stats to stderr in ffmpeg command
2024-11-22 10:51:57 +01:00
Franck Nijhof
876112ff54 Update twentemilieu to 2.1.0 (#130752) 2024-11-22 10:51:54 +01:00
Jan Bouwhuis
a48f88033d Fix file uploads in MQTT config flow not processed in executor (#130746)
Process file uploads in MQTT config flow in executor
2024-11-22 10:51:49 +01:00
Patrick
5deba1766e Fix and bump apsystems-ez1 to 2.4.0 (#130740) 2024-11-22 10:51:45 +01:00
Davin Kevin
4863243f5a Prevent endless loop in recorder when using a filter and there are no more states to purge (#126149)
Co-authored-by: J. Nick Koston <nick@koston.org>
2024-11-22 10:51:35 +01:00
Franck Nijhof
847afabed1 2024.11.2 (#130713) 2024-11-15 20:16:10 +01:00
Franck Nijhof
ac270e19be Bump version to 2024.11.2 2024-11-15 19:35:42 +01:00
Matt Zimmerman
ca40b96a89 Bump python-smarttub to 0.0.38 (#130679) 2024-11-15 19:35:14 +01:00
epenet
045e285bfe Fix missing translations in onewire (#130673) 2024-11-15 19:35:11 +01:00
epenet
8d6f2e78f5 Fix missing translations in generic (#130672) 2024-11-15 19:35:07 +01:00
epenet
9e4d26137e Fix missing translations in madvr (#130656) 2024-11-15 19:35:04 +01:00
epenet
f74bfdc974 Fix missing translations in toon (#130655) 2024-11-15 19:35:00 +01:00
epenet
1cabcdf257 Fix missing translations in tradfri (#130654)
* Fix missing translations in tradfri

* Simplify
2024-11-15 19:34:57 +01:00
epenet
c6931d656e Fix missing translations in utility_meter (#130652) 2024-11-15 19:34:54 +01:00
epenet
942830505a Fix missing translations in vilfo (#130650) 2024-11-15 19:34:51 +01:00
Jan-Philipp Benecke
880f28e28a Remove dumping config entry to log in setup of roborock (#130648) 2024-11-15 19:34:48 +01:00
Johan Nenzén
f406ffa75a Bump pyplaato to 0.0.19 (#130641)
Bumps version of pyplaato to 0.0.19
2024-11-15 19:34:44 +01:00
epenet
0d695c843f Add missing translation string to philips_js (#130637) 2024-11-15 19:34:41 +01:00
epenet
5f09eb97e1 Add missing translation string to lg_netcast (#130635) 2024-11-15 19:34:38 +01:00
epenet
6d561ca373 Add missing translation string to hvv_departures (#130634) 2024-11-15 19:34:34 +01:00
Alistair Galbraith
663ebe199d Fix scene loading issue (#130627) 2024-11-15 19:34:31 +01:00
Keilin Bickar
8b9c4db2b3 Bump sense-energy to 0.13.4 (#130625) 2024-11-15 19:34:27 +01:00
epenet
e478b9b599 Add missing translation string to smarty (#130624) 2024-11-15 19:34:23 +01:00
Robert Resch
5acdf58976 Fix hassfest by adding go2rtc reqs (#130602) 2024-11-15 19:33:09 +01:00
starkillerOG
6d861e7f47 Bump reolink-aio to 0.11.1 (#130600) 2024-11-15 19:32:30 +01:00
Johan Nenzén
281a8eda31 Fixes webhook schema for different temp and volume units (#130578) 2024-11-15 19:32:26 +01:00
Simone Chemelli
1bc005d0d4 Update uptime deviation for Vodafone Station (#130571)
Update sensor.py
2024-11-15 19:32:23 +01:00
puddly
95d60987ab Bump ZHA dependencies (#130563) 2024-11-15 19:32:19 +01:00
J. Nick Koston
53e38454b2 Fix non-thread-safe operation in powerview number (#130557) 2024-11-15 19:32:16 +01:00
Brig Lamoreaux
876b86cd3d fix translation in srp_energy (#130540) 2024-11-15 19:32:13 +01:00
Robert Resch
cb104935ea Add go2rtc recommended version (#130508) 2024-11-15 19:32:10 +01:00
Joost Lekkerkerker
4c24e26926 Bump aiowithings to 3.1.3 (#130504) 2024-11-15 19:32:06 +01:00
Robert Resch
4b13d8bc47 Bump go2rtc-client to 0.1.1 (#130498) 2024-11-15 19:30:50 +01:00
Tony
433e3718f8 Bump aioruckus to 0.42 (#130487) 2024-11-15 19:28:38 +01:00
Sheldon Ip
1e3c2c0631 Fix translations in subaru (#130486) 2024-11-15 19:28:34 +01:00
starkillerOG
3a2f996c13 Bump reolink_aio to 0.11.0 (#130481) 2024-11-15 19:28:30 +01:00
G Johansson
e4cb3c67d9 Fix legacy _attr_state handling in AlarmControlPanel (#130479) 2024-11-15 19:28:27 +01:00
puddly
8a22433168 Ensure ZHA setup works with container installs (#130470) 2024-11-15 19:28:23 +01:00
Joost Lekkerkerker
0976476d16 Bump aiowithings to 3.1.2 (#130469) 2024-11-15 19:28:19 +01:00
Kelvin Dekker
28f46a0f88 Fix typo in file strings (#130465) 2024-11-15 19:28:16 +01:00
G Johansson
8b173656e7 Fix translation in statistics (#130455)
* Fix translation in statistics

* Update homeassistant/components/statistics/strings.json
2024-11-15 19:28:12 +01:00
Joost Lekkerkerker
08f6f2759b Add title to water heater component (#130446) 2024-11-15 19:28:09 +01:00
Steven B.
f4798d27c7 Do not trigger events for updated ring events (#130430) 2024-11-15 19:28:05 +01:00
Steven B.
103a84b4bd Bump ring-doorbell to 0.9.12 (#130419) 2024-11-15 19:28:01 +01:00
Steven B.
4d3502e061 Bump ring library ring-doorbell to 0.9.9 (#129966) 2024-11-15 19:26:59 +01:00
J. Nick Koston
79329e16cf Fix missing title placeholders in powerwall reauth (#130389) 2024-11-15 19:24:37 +01:00
Daniel Hjelseth Høyer
929164251a Bump Tibber 0.30.8 (#130388) 2024-11-15 19:24:34 +01:00
Joost Lekkerkerker
300724443a Bump spotifyaio to 0.8.8 (#130372) 2024-11-15 19:24:30 +01:00
Robert Resch
70ef3a355c Go2rtc bump and set ffmpeg logs to debug (#130371) 2024-11-15 19:24:26 +01:00
Jan Bouwhuis
83162c1461 Fix typo in go2rtc (#130165)
Fix typo in original
2024-11-15 19:24:20 +01:00
Jan Bouwhuis
a12c76dbdd Use f-strings in go2rtc code and test and do not use abbreviation (#130158) 2024-11-15 19:22:09 +01:00
Noah Husby
9292b6da3d Disable brightness from devices with no display in Cambridge Audio (#130369) 2024-11-15 19:03:04 +01:00
Simon Lamon
8d05183de2 Add Spotify and Tidal to playingmode mapping (#130351) 2024-11-15 19:03:01 +01:00
Simon Lamon
a86ff41bbc Add seek support to LinkPlay (#130349) 2024-11-15 19:02:58 +01:00
Simon Lamon
ce92f3de44 Bump python-linkplay to 0.0.20 (#130348) 2024-11-15 19:02:54 +01:00
LG-ThinQ-Integration
465d8b2ee2 Fix fan's warning TURN_ON, TURN_OFF (#130327)
Co-authored-by: yunseon.park <yunseon.park@lge.com>
2024-11-15 19:02:51 +01:00
G Johansson
218eedfd93 Fix Homekit error handling alarm state unknown or unavailable (#130311) 2024-11-15 19:02:47 +01:00
Simone Chemelli
afec354b84 Avoid Shelly data update during shutdown (#130301) 2024-11-15 19:02:44 +01:00
Allen Porter
282f92e5f3 Ignore WebRTC candidates for nest cameras (#130294) 2024-11-15 19:02:41 +01:00
David Knowles
f6cd74e2d7 Make Hydrawise poll non-critical data less frequently (#130289) 2024-11-15 19:02:37 +01:00
Åke Strandberg
f821ddeab8 Add more f-series models to myuplink (#130283) 2024-11-15 19:02:34 +01:00
Allen Porter
d408b7ac62 Improve nest camera stream expiration to be defensive against errors (#130265) 2024-11-15 19:02:31 +01:00
Michael
83baa1a788 Fix translation key for done response in conversation (#130247) 2024-11-15 19:02:27 +01:00
Max Shcherbina
07a8cf14cd Update generic thermostat strings for clarity and accuracy (#130243) 2024-11-15 19:02:24 +01:00
Olivier Corradi
9f447af468 Rename "CO2 Signal" display name to Electricity Maps for consistency (#130242)
* Update strings.json for Electricity Maps

* Update strings.json

* Update config_flow.py

* Update test_config_flow.py

* Fix test
2024-11-15 19:02:20 +01:00
Allen Porter
c399d8f571 Bump google-nest-sdm to 6.1.5 (#130229) 2024-11-15 19:02:17 +01:00
jjlawren
4ea9574229 Bump SoCo to 0.30.6 (#130223) 2024-11-15 19:02:14 +01:00
Daniel Hjelseth Høyer
592b8ed0a0 Bump pyTibber (#130216) 2024-11-15 19:02:10 +01:00
Simone Chemelli
6b91c0810a Fix uptime sensor for Vodafone Station (#130215) 2024-11-15 19:02:06 +01:00
Max Shcherbina
9579e4a9c1 Fix wording in Google Calendar create_event strings for consistency (#130183) 2024-11-15 19:00:06 +01:00
IceBotYT
7f4f90f06d Bump nice-go to 0.3.10 (#130173)
Bump Nice G.O. to 0.3.10
2024-11-15 19:00:02 +01:00
Sheldon Ip
701a901fe4 Fix translations in ollama (#130164) 2024-11-15 18:59:59 +01:00
Simon Lamon
f914642e31 No longer thrown an error when device is offline in linkplay (#130161) 2024-11-15 18:59:55 +01:00
Simon Lamon
32dc9fc238 Allow dynamic max preset in linkplay play preset (#130160) 2024-11-15 18:59:52 +01:00
Simon Lamon
b27e0f9fe7 Bump python-linkplay to v0.0.18 (#130159) 2024-11-15 18:59:47 +01:00
Thomas55555
f040060b3c Fix RecursionError in Husqvarna Automower coordinator (#123085)
* reach maximum recursion depth exceeded in tests

* second background task

* Update homeassistant/components/husqvarna_automower/coordinator.py

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

* Update homeassistant/components/husqvarna_automower/coordinator.py

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

* test

* modify test

* tests

* use correct exception

* reset mock

* use recursion_limit

* remove unneeded ticks

* test TimeoutException

* set lower recursionlimit

* remove not that important comment and move the other

* test that we connect and listen successfully

* Simulate hass shutting down

* skip testing against the recursion limit

* Update homeassistant/components/husqvarna_automower/coordinator.py

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

* mock

* Remove comment

* Revert "mock"

This reverts commit e8ddaea3d7.

* Move patch to decorator

* Make execution of patched methods predictable

* Parametrize test, make mocked start_listening block

* Apply suggestions from code review

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Erik <erik@montnemery.com>
2024-11-15 18:47:59 +01:00
J. Nick Koston
cc45793896 Bump aiohttp to 3.10.11 (#130483)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-11-15 09:42:20 +01:00
Franck Nijhof
ab0556227c 2024.11.1 (#130156) 2024-11-08 19:42:10 +01:00
Franck Nijhof
c16fb9c93d Bump version to 2024.11.1 2024-11-08 18:58:21 +01:00
Jan Bouwhuis
da8fc7a2fc Refrase imap fetch service description string (#130152) 2024-11-08 18:58:07 +01:00
Allen Porter
864b4d86f2 Fix bugs in nest stream expiration handling (#130150) 2024-11-08 18:58:04 +01:00
Louis Christ
1bb0ced7c0 Fix volume_up not working in some cases in bluesound integration (#130146) 2024-11-08 18:58:00 +01:00
Martin Hjelmare
2fe4fc908b Bump ha-ffmpeg to 3.2.2 (#130142) 2024-11-08 18:57:25 +01:00
Joost Lekkerkerker
aa2c3b046f Bump spotifyaio to 0.8.7 (#130140) 2024-11-08 18:56:15 +01:00
Robert Resch
22822cb8aa Add go2rtc workaround for HA managed one until upstream fixes it (#130139) 2024-11-08 18:56:12 +01:00
Shai Ungar
b71383c997 Fix issue when timestamp is None (#130133)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-11-08 18:56:09 +01:00
Bram Kragten
b0b163df48 Update frontend to 20241106.2 (#130128) 2024-11-08 18:56:06 +01:00
Luke Lashley
35539dbf60 Bump python-roborock to 2.7.2 (#130100) 2024-11-08 18:56:02 +01:00
Bram Kragten
09d03e8edf Update frontend to 20241106.1 (#130086) 2024-11-08 18:55:59 +01:00
Kelvin Dekker
46e37f3bdd Fix typo in insteon strings (#130085) 2024-11-08 18:55:55 +01:00
Klaas Schoute
0206c149cf Force int value on port in P1Monitor (#130084) 2024-11-08 18:55:52 +01:00
Josef Zweck
29620ef977 Add missing string to tedee plus test (#130081) 2024-11-08 18:55:49 +01:00
Erik Montnemery
9012b113ad Don't create repairs asking user to remove duplicate flipr config entries (#130058)
* Don't create repairs asking user to remove duplicate flipr config entries

* Improve comments
2024-11-08 18:55:46 +01:00
Allen Porter
5f5f6cc3d5 Fix KeyError in nest integration when the old key format does not exist (#130057)
* Fix bug in nest setup when the old key format does not exist

* Further simplify the entry.data check

* Update homeassistant/components/nest/api.py

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

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-11-08 18:55:42 +01:00
Erik Montnemery
7ff501f3ec Don't create repairs asking user to remove duplicate ignored config entries (#130056) 2024-11-08 18:55:39 +01:00
sean t
b0f110b9ab Bump agent-py to 0.0.24 (#130018)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-11-08 18:55:36 +01:00
epenet
2692bc23a5 Add missing placeholder description to twitch (#130013) 2024-11-08 18:55:33 +01:00
Allen Porter
1beac5f0f8 Bump google-nest-sdm to 6.1.4 (#130005)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-11-08 18:55:29 +01:00
Keilin Bickar
ec7ba1b7fd Update sense energy library to 0.13.3 (#129998) 2024-11-08 18:55:25 +01:00
Brett Adams
5bd1b0dd9c Fix Trunks in Teslemetry and Tesla Fleet (#129986) 2024-11-08 18:55:21 +01:00
Michael Hansen
a2ad4c9cfd Bump intents to 2024.11.6 (#129982) 2024-11-08 18:52:43 +01:00
333 changed files with 12028 additions and 3233 deletions

View File

@@ -509,7 +509,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
@@ -522,7 +522,7 @@ jobs:
- name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile

View File

@@ -143,7 +143,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-dev"
skip-binary: aiohttp;multidict;yarl
skip-binary: aiohttp;multidict;propcache;yarl;SQLAlchemy
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements.txt"

16
.vscode/tasks.json vendored
View File

@@ -87,6 +87,22 @@
},
"problemMatcher": []
},
{
"label": "Update syrupy snapshots",
"detail": "Update syrupy snapshots for a given integration.",
"type": "shell",
"command": "python3 -m pytest ./tests/components/${input:integrationName} --snapshot-update",
"dependsOn": ["Compile English translations"],
"group": {
"kind": "test",
"isDefault": true
},
"presentation": {
"reveal": "always",
"panel": "new"
},
"problemMatcher": []
},
{
"label": "Generate Requirements",
"type": "shell",

View File

@@ -42,7 +42,7 @@ class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
if user_input is not None:
mac = format_mac(user_input[CONF_ADDRESS])
mac = user_input[CONF_ADDRESS]
try:
is_new_style_scale = await is_new_scale(mac)
except AcaiaDeviceNotFound:
@@ -53,12 +53,12 @@ class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
except AcaiaUnknownDevice:
return self.async_abort(reason="unsupported_device")
else:
await self.async_set_unique_id(mac)
await self.async_set_unique_id(format_mac(mac))
self._abort_if_unique_id_configured()
if not errors:
return self.async_create_entry(
title=self._discovered_devices[user_input[CONF_ADDRESS]],
title=self._discovered_devices[mac],
data={
CONF_ADDRESS: mac,
CONF_IS_NEW_STYLE_SCALE: is_new_style_scale,
@@ -99,10 +99,10 @@ class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle a discovered Bluetooth device."""
self._discovered[CONF_ADDRESS] = mac = format_mac(discovery_info.address)
self._discovered[CONF_ADDRESS] = discovery_info.address
self._discovered[CONF_NAME] = discovery_info.name
await self.async_set_unique_id(mac)
await self.async_set_unique_id(format_mac(discovery_info.address))
self._abort_if_unique_id_configured()
try:

View File

@@ -2,7 +2,7 @@
from dataclasses import dataclass
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import DeviceInfo, format_mac
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -25,10 +25,11 @@ class AcaiaEntity(CoordinatorEntity[AcaiaCoordinator]):
super().__init__(coordinator)
self.entity_description = entity_description
self._scale = coordinator.scale
self._attr_unique_id = f"{self._scale.mac}_{entity_description.key}"
formatted_mac = format_mac(self._scale.mac)
self._attr_unique_id = f"{formatted_mac}_{entity_description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._scale.mac)},
identifiers={(DOMAIN, formatted_mac)},
manufacturer="Acaia",
model=self._scale.model,
suggested_area="Kitchen",

View File

@@ -25,5 +25,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["aioacaia"],
"requirements": ["aioacaia==0.1.9"]
"requirements": ["aioacaia==0.1.10"]
}

View File

@@ -0,0 +1,106 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
No custom actions are defined.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
No custom actions are defined.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: todo
entity-event-setup:
status: exempt
comment: |
No explicit event subscriptions.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup:
status: exempt
comment: |
Device is expected to be offline most of the time, but needs to connect quickly once available.
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: |
No custom actions are defined.
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable:
status: done
comment: |
Handled by coordinator.
parallel-updates: done
reauthentication-flow:
status: exempt
comment: |
No authentication required.
test-coverage: done
# Gold
devices: done
diagnostics: done
discovery-update-info:
status: exempt
comment: |
No IP discovery.
discovery:
status: done
comment: |
Bluetooth discovery.
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:
status: exempt
comment: |
Device type integration.
entity-category: done
entity-device-class: done
entity-disabled-by-default:
status: exempt
comment: |
No noisy/non-essential entities.
entity-translations: done
exception-translations:
status: exempt
comment: |
No custom exceptions.
icon-translations: done
reconfiguration-flow:
status: exempt
comment: |
Only parameter that could be changed (MAC = unique_id) would force a new config entry.
repair-issues:
status: exempt
comment: |
No repairs/issues.
stale-devices:
status: exempt
comment: |
Device type integration.
# Platinum
async-dependency: done
inject-websession:
status: exempt
comment: |
Bluetooth connection.
strict-typing: done

View File

@@ -37,7 +37,7 @@ STATE_KEY_POSITION = "position"
PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_ADS_VAR): cv.string,
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_ADS_VAR_POSITION): cv.string,
vol.Optional(CONF_ADS_VAR_SET_POS): cv.string,
vol.Optional(CONF_ADS_VAR_CLOSE): cv.string,

View File

@@ -35,6 +35,7 @@ from homeassistant.helpers.deprecation import (
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity_platform import EntityPlatform
from homeassistant.helpers.frame import ReportBehavior, report_usage
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
@@ -163,7 +164,6 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
_alarm_control_panel_option_default_code: str | None = None
__alarm_legacy_state: bool = False
__alarm_legacy_state_reported: bool = False
def __init_subclass__(cls, **kwargs: Any) -> None:
"""Post initialisation processing."""
@@ -180,9 +180,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
unless already reported.
"""
if name == "_attr_state":
if self.__alarm_legacy_state_reported is not True:
self._report_deprecated_alarm_state_handling()
self.__alarm_legacy_state_reported = True
self._report_deprecated_alarm_state_handling()
return super().__setattr__(name, value)
@callback
@@ -194,7 +192,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
) -> None:
"""Start adding an entity to a platform."""
super().add_to_platform_start(hass, platform, parallel_updates)
if self.__alarm_legacy_state and not self.__alarm_legacy_state_reported:
if self.__alarm_legacy_state:
self._report_deprecated_alarm_state_handling()
@callback
@@ -203,19 +201,16 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
Integrations should implement alarm_state instead of using state directly.
"""
self.__alarm_legacy_state_reported = True
if "custom_components" in type(self).__module__:
# Do not report on core integrations as they have been fixed.
report_issue = "report it to the custom integration author."
_LOGGER.warning(
"Entity %s (%s) is setting state directly"
" which will stop working in HA Core 2025.11."
" Entities should implement the 'alarm_state' property and"
" return its state using the AlarmControlPanelState enum, please %s",
self.entity_id,
type(self),
report_issue,
)
report_usage(
"is setting state directly."
f" Entity {self.entity_id} ({type(self)}) should implement the 'alarm_state'"
" property and return its state using the AlarmControlPanelState enum",
core_integration_behavior=ReportBehavior.ERROR,
custom_integration_behavior=ReportBehavior.LOG,
breaks_in_ha_version="2025.11",
integration_domain=self.platform.platform_name if self.platform else None,
exclude_integrations={DOMAIN},
)
@final
@property

View File

@@ -9,7 +9,7 @@
"loggers": ["adb_shell", "androidtv", "pure_python_adb"],
"requirements": [
"adb-shell[async]==0.4.4",
"androidtv[async]==0.0.73",
"androidtv[async]==0.0.75",
"pure-python-adb[async]==0.3.0.dev0"
]
}

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
"iot_class": "local_push",
"loggers": ["pyatv", "srptools"],
"requirements": ["pyatv==0.15.1"],
"requirements": ["pyatv==0.16.0"],
"zeroconf": [
"_mediaremotetv._tcp.local.",
"_companion-link._tcp.local.",

View File

@@ -1040,7 +1040,7 @@ class PipelineRun:
:= await conversation.async_handle_sentence_triggers(
self.hass, user_input
)
):
) is not None:
# Sentence trigger matched
trigger_response = intent.IntentResponse(
self.pipeline.conversation_language

View File

@@ -0,0 +1,40 @@
"""Support for Bang & Olufsen diagnostics."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
from homeassistant.core import HomeAssistant
import homeassistant.helpers.entity_registry as er
from . import BangOlufsenConfigEntry
from .const import DOMAIN
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: BangOlufsenConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
data: dict = {
"config_entry": config_entry.as_dict(),
"websocket_connected": config_entry.runtime_data.client.websocket_connected,
}
if TYPE_CHECKING:
assert config_entry.unique_id
# Add media_player entity's state
entity_registry = er.async_get(hass)
if entity_id := entity_registry.async_get_entity_id(
MEDIA_PLAYER_DOMAIN, DOMAIN, config_entry.unique_id
):
if state := hass.states.get(entity_id):
state_dict = dict(state.as_dict())
# Remove context as it is not relevant
state_dict.pop("context")
data["media_player"] = state_dict
return data

View File

@@ -204,13 +204,11 @@ class BangOlufsenWebsocket(BangOlufsenBase):
def on_all_notifications_raw(self, notification: BaseWebSocketResponse) -> None:
"""Receive all notifications."""
debug_notification = {
"device_id": self._device.id,
"serial_number": int(self._unique_id),
**notification,
}
_LOGGER.debug("%s", notification)
self.hass.bus.async_fire(
BANG_OLUFSEN_WEBSOCKET_EVENT,
{
"device_id": self._device.id,
"serial_number": int(self._unique_id),
**notification,
},
)
_LOGGER.debug("%s", debug_notification)
self.hass.bus.async_fire(BANG_OLUFSEN_WEBSOCKET_EVENT, debug_notification)

View File

@@ -292,14 +292,6 @@ class BluesoundPlayer(MediaPlayerEntity):
self._last_status_update = dt_util.utcnow()
self._status = status
group_name = status.group_name
if group_name != self._group_name:
_LOGGER.debug("Group name change detected on device: %s", self.id)
self._group_name = group_name
# rebuild ordered list of entity_ids that are in the group, master is first
self._group_list = self.rebuild_bluesound_group()
self.async_write_ha_state()
except PlayerUnreachableError:
self._attr_available = False
@@ -323,6 +315,8 @@ class BluesoundPlayer(MediaPlayerEntity):
self._sync_status = sync_status
self._group_list = self.rebuild_bluesound_group()
if sync_status.master is not None:
self._is_master = False
master_id = f"{sync_status.master.ip}:{sync_status.master.port}"
@@ -619,21 +613,32 @@ class BluesoundPlayer(MediaPlayerEntity):
def rebuild_bluesound_group(self) -> list[str]:
"""Rebuild the list of entities in speaker group."""
if self._group_name is None:
if self.sync_status.master is None and self.sync_status.slaves is None:
return []
device_group = self._group_name.split("+")
player_entities: list[BluesoundPlayer] = self.hass.data[DATA_BLUESOUND]
sorted_entities: list[BluesoundPlayer] = sorted(
self.hass.data[DATA_BLUESOUND],
key=lambda entity: entity.is_master,
reverse=True,
)
return [
entity.sync_status.name
for entity in sorted_entities
if entity.bluesound_device_name in device_group
leader_sync_status: SyncStatus | None = None
if self.sync_status.master is None:
leader_sync_status = self.sync_status
else:
required_id = f"{self.sync_status.master.ip}:{self.sync_status.master.port}"
for x in player_entities:
if x.sync_status.id == required_id:
leader_sync_status = x.sync_status
break
if leader_sync_status is None or leader_sync_status.slaves is None:
return []
follower_ids = [f"{x.ip}:{x.port}" for x in leader_sync_status.slaves]
follower_names = [
x.sync_status.name
for x in player_entities
if x.sync_status.id in follower_ids
]
follower_names.insert(0, leader_sync_status.name)
return follower_names
async def async_unjoin(self) -> None:
"""Unjoin the player from a group."""

View File

@@ -27,9 +27,18 @@ from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_SOURCE, CONF_US
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
from homeassistant.util.ssl import get_default_context
from . import DOMAIN
from .const import CONF_ALLOWED_REGIONS, CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN
from .const import (
CONF_ALLOWED_REGIONS,
CONF_CAPTCHA_REGIONS,
CONF_CAPTCHA_TOKEN,
CONF_CAPTCHA_URL,
CONF_GCID,
CONF_READ_ONLY,
CONF_REFRESH_TOKEN,
)
DATA_SCHEMA = vol.Schema(
{
@@ -41,7 +50,14 @@ DATA_SCHEMA = vol.Schema(
translation_key="regions",
)
),
}
},
extra=vol.REMOVE_EXTRA,
)
CAPTCHA_SCHEMA = vol.Schema(
{
vol.Required(CONF_CAPTCHA_TOKEN): str,
},
extra=vol.REMOVE_EXTRA,
)
@@ -54,6 +70,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
data[CONF_USERNAME],
data[CONF_PASSWORD],
get_region_from_name(data[CONF_REGION]),
hcaptcha_token=data.get(CONF_CAPTCHA_TOKEN),
verify=get_default_context(),
)
try:
@@ -79,15 +97,17 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
data: dict[str, Any] = {}
_existing_entry_data: Mapping[str, Any] | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
errors: dict[str, str] = self.data.pop("errors", {})
if user_input is not None:
if user_input is not None and not errors:
unique_id = f"{user_input[CONF_REGION]}-{user_input[CONF_USERNAME]}"
await self.async_set_unique_id(unique_id)
@@ -96,22 +116,35 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
else:
self._abort_if_unique_id_configured()
# Store user input for later use
self.data.update(user_input)
# North America and Rest of World require captcha token
if (
self.data.get(CONF_REGION) in CONF_CAPTCHA_REGIONS
and CONF_CAPTCHA_TOKEN not in self.data
):
return await self.async_step_captcha()
info = None
try:
info = await validate_input(self.hass, user_input)
entry_data = {
**user_input,
CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN),
CONF_GCID: info.get(CONF_GCID),
}
info = await validate_input(self.hass, self.data)
except MissingCaptcha:
errors["base"] = "missing_captcha"
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
finally:
self.data.pop(CONF_CAPTCHA_TOKEN, None)
if info:
entry_data = {
**self.data,
CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN),
CONF_GCID: info.get(CONF_GCID),
}
if self.source == SOURCE_REAUTH:
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data=entry_data
@@ -128,7 +161,7 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
schema = self.add_suggested_values_to_schema(
DATA_SCHEMA,
self._existing_entry_data,
self._existing_entry_data or self.data,
)
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
@@ -147,6 +180,22 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
self._existing_entry_data = self._get_reconfigure_entry().data
return await self.async_step_user()
async def async_step_captcha(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Show captcha form."""
if user_input and user_input.get(CONF_CAPTCHA_TOKEN):
self.data[CONF_CAPTCHA_TOKEN] = user_input[CONF_CAPTCHA_TOKEN].strip()
return await self.async_step_user(self.data)
return self.async_show_form(
step_id="captcha",
data_schema=CAPTCHA_SCHEMA,
description_placeholders={
"captcha_url": CONF_CAPTCHA_URL.format(region=self.data[CONF_REGION])
},
)
@staticmethod
@callback
def async_get_options_flow(

View File

@@ -8,10 +8,15 @@ ATTR_DIRECTION = "direction"
ATTR_VIN = "vin"
CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"]
CONF_CAPTCHA_REGIONS = ["north_america", "rest_of_world"]
CONF_READ_ONLY = "read_only"
CONF_ACCOUNT = "account"
CONF_REFRESH_TOKEN = "refresh_token"
CONF_GCID = "gcid"
CONF_CAPTCHA_TOKEN = "captcha_token"
CONF_CAPTCHA_URL = (
"https://bimmer-connected.readthedocs.io/en/stable/captcha/{region}.html"
)
DATA_HASS_CONFIG = "hass_config"

View File

@@ -84,11 +84,6 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
if self.account.refresh_token != old_refresh_token:
self._update_config_entry_refresh_token(self.account.refresh_token)
_LOGGER.debug(
"bimmer_connected: refresh token %s > %s",
old_refresh_token,
self.account.refresh_token,
)
def _update_config_entry_refresh_token(self, refresh_token: str | None) -> None:
"""Update or delete the refresh_token in the Config Entry."""

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"iot_class": "cloud_polling",
"loggers": ["bimmer_connected"],
"requirements": ["bimmer-connected[china]==0.16.4"]
"requirements": ["bimmer-connected[china]==0.17.2"]
}

View File

@@ -7,6 +7,16 @@
"password": "[%key:common::config_flow::data::password%]",
"region": "ConnectedDrive Region"
}
},
"captcha": {
"title": "Are you a robot?",
"description": "A captcha is required for BMW login. Visit the external website to complete the challenge and submit the form. Copy the resulting token into the field below.\n\n{captcha_url}\n\nNo data will be exposed outside of your Home Assistant instance.",
"data": {
"captcha_token": "Captcha token"
},
"data_description": {
"captcha_token": "One-time token retrieved from the captcha challenge."
}
}
},
"error": {

View File

@@ -70,6 +70,8 @@ from .const import ( # noqa: F401
ATTR_MIN_TEMP,
ATTR_PRESET_MODE,
ATTR_PRESET_MODES,
ATTR_SWING_HORIZONTAL_MODE,
ATTR_SWING_HORIZONTAL_MODES,
ATTR_SWING_MODE,
ATTR_SWING_MODES,
ATTR_TARGET_TEMP_HIGH,
@@ -101,6 +103,7 @@ from .const import ( # noqa: F401
SERVICE_SET_HUMIDITY,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_SWING_HORIZONTAL_MODE,
SERVICE_SET_SWING_MODE,
SERVICE_SET_TEMPERATURE,
SWING_BOTH,
@@ -219,6 +222,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"async_handle_set_swing_mode_service",
[ClimateEntityFeature.SWING_MODE],
)
component.async_register_entity_service(
SERVICE_SET_SWING_HORIZONTAL_MODE,
{vol.Required(ATTR_SWING_HORIZONTAL_MODE): cv.string},
"async_handle_set_swing_horizontal_mode_service",
[ClimateEntityFeature.SWING_HORIZONTAL_MODE],
)
return True
@@ -256,6 +265,8 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
"fan_modes",
"swing_mode",
"swing_modes",
"swing_horizontal_mode",
"swing_horizontal_modes",
"supported_features",
"min_temp",
"max_temp",
@@ -300,6 +311,8 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
_attr_supported_features: ClimateEntityFeature = ClimateEntityFeature(0)
_attr_swing_mode: str | None
_attr_swing_modes: list[str] | None
_attr_swing_horizontal_mode: str | None
_attr_swing_horizontal_modes: list[str] | None
_attr_target_humidity: float | None = None
_attr_target_temperature_high: float | None
_attr_target_temperature_low: float | None
@@ -513,6 +526,9 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if ClimateEntityFeature.SWING_MODE in supported_features:
data[ATTR_SWING_MODES] = self.swing_modes
if ClimateEntityFeature.SWING_HORIZONTAL_MODE in supported_features:
data[ATTR_SWING_HORIZONTAL_MODES] = self.swing_horizontal_modes
return data
@final
@@ -564,6 +580,9 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if ClimateEntityFeature.SWING_MODE in supported_features:
data[ATTR_SWING_MODE] = self.swing_mode
if ClimateEntityFeature.SWING_HORIZONTAL_MODE in supported_features:
data[ATTR_SWING_HORIZONTAL_MODE] = self.swing_horizontal_mode
if ClimateEntityFeature.AUX_HEAT in supported_features:
data[ATTR_AUX_HEAT] = STATE_ON if self.is_aux_heat else STATE_OFF
if (
@@ -691,11 +710,27 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""
return self._attr_swing_modes
@cached_property
def swing_horizontal_mode(self) -> str | None:
"""Return the horizontal swing setting.
Requires ClimateEntityFeature.SWING_HORIZONTAL_MODE.
"""
return self._attr_swing_horizontal_mode
@cached_property
def swing_horizontal_modes(self) -> list[str] | None:
"""Return the list of available horizontal swing modes.
Requires ClimateEntityFeature.SWING_HORIZONTAL_MODE.
"""
return self._attr_swing_horizontal_modes
@final
@callback
def _valid_mode_or_raise(
self,
mode_type: Literal["preset", "swing", "fan", "hvac"],
mode_type: Literal["preset", "horizontal_swing", "swing", "fan", "hvac"],
mode: str | HVACMode,
modes: list[str] | list[HVACMode] | None,
) -> None:
@@ -793,6 +828,26 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Set new target swing operation."""
await self.hass.async_add_executor_job(self.set_swing_mode, swing_mode)
@final
async def async_handle_set_swing_horizontal_mode_service(
self, swing_horizontal_mode: str
) -> None:
"""Validate and set new horizontal swing mode."""
self._valid_mode_or_raise(
"horizontal_swing", swing_horizontal_mode, self.swing_horizontal_modes
)
await self.async_set_swing_horizontal_mode(swing_horizontal_mode)
def set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None:
"""Set new target horizontal swing operation."""
raise NotImplementedError
async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None:
"""Set new target horizontal swing operation."""
await self.hass.async_add_executor_job(
self.set_swing_horizontal_mode, swing_horizontal_mode
)
@final
async def async_handle_set_preset_mode_service(self, preset_mode: str) -> None:
"""Validate and set new preset mode."""

View File

@@ -92,6 +92,10 @@ SWING_BOTH = "both"
SWING_VERTICAL = "vertical"
SWING_HORIZONTAL = "horizontal"
# Possible horizontal swing state
SWING_HORIZONTAL_ON = "on"
SWING_HORIZONTAL_OFF = "off"
class HVACAction(StrEnum):
"""HVAC action for climate devices."""
@@ -134,6 +138,8 @@ ATTR_HVAC_MODES = "hvac_modes"
ATTR_HVAC_MODE = "hvac_mode"
ATTR_SWING_MODES = "swing_modes"
ATTR_SWING_MODE = "swing_mode"
ATTR_SWING_HORIZONTAL_MODE = "swing_horizontal_mode"
ATTR_SWING_HORIZONTAL_MODES = "swing_horizontal_modes"
ATTR_TARGET_TEMP_HIGH = "target_temp_high"
ATTR_TARGET_TEMP_LOW = "target_temp_low"
ATTR_TARGET_TEMP_STEP = "target_temp_step"
@@ -153,6 +159,7 @@ SERVICE_SET_PRESET_MODE = "set_preset_mode"
SERVICE_SET_HUMIDITY = "set_humidity"
SERVICE_SET_HVAC_MODE = "set_hvac_mode"
SERVICE_SET_SWING_MODE = "set_swing_mode"
SERVICE_SET_SWING_HORIZONTAL_MODE = "set_swing_horizontal_mode"
SERVICE_SET_TEMPERATURE = "set_temperature"
@@ -168,6 +175,7 @@ class ClimateEntityFeature(IntFlag):
AUX_HEAT = 64
TURN_OFF = 128
TURN_ON = 256
SWING_HORIZONTAL_MODE = 512
# These SUPPORT_* constants are deprecated as of Home Assistant 2022.5.

View File

@@ -51,6 +51,13 @@
"on": "mdi:arrow-oscillating",
"vertical": "mdi:arrow-up-down"
}
},
"swing_horizontal_mode": {
"default": "mdi:circle-medium",
"state": {
"off": "mdi:arrow-oscillating-off",
"on": "mdi:arrow-expand-horizontal"
}
}
}
}
@@ -65,6 +72,9 @@
"set_swing_mode": {
"service": "mdi:arrow-oscillating"
},
"set_swing_horizontal_mode": {
"service": "mdi:arrow-expand-horizontal"
},
"set_temperature": {
"service": "mdi:thermometer"
},

View File

@@ -14,6 +14,7 @@ from .const import (
ATTR_HUMIDITY,
ATTR_HVAC_MODE,
ATTR_PRESET_MODE,
ATTR_SWING_HORIZONTAL_MODE,
ATTR_SWING_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
@@ -23,6 +24,7 @@ from .const import (
SERVICE_SET_HUMIDITY,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_SWING_HORIZONTAL_MODE,
SERVICE_SET_SWING_MODE,
SERVICE_SET_TEMPERATURE,
)
@@ -76,6 +78,14 @@ async def _async_reproduce_states(
):
await call_service(SERVICE_SET_SWING_MODE, [ATTR_SWING_MODE])
if (
ATTR_SWING_HORIZONTAL_MODE in state.attributes
and state.attributes[ATTR_SWING_HORIZONTAL_MODE] is not None
):
await call_service(
SERVICE_SET_SWING_HORIZONTAL_MODE, [ATTR_SWING_HORIZONTAL_MODE]
)
if (
ATTR_FAN_MODE in state.attributes
and state.attributes[ATTR_FAN_MODE] is not None

View File

@@ -131,7 +131,20 @@ set_swing_mode:
fields:
swing_mode:
required: true
example: "horizontal"
example: "on"
selector:
text:
set_swing_horizontal_mode:
target:
entity:
domain: climate
supported_features:
- climate.ClimateEntityFeature.SWING_HORIZONTAL_MODE
fields:
swing_horizontal_mode:
required: true
example: "on"
selector:
text:

View File

@@ -19,6 +19,7 @@ from . import (
ATTR_HUMIDITY,
ATTR_HVAC_ACTION,
ATTR_PRESET_MODE,
ATTR_SWING_HORIZONTAL_MODE,
ATTR_SWING_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
@@ -34,6 +35,7 @@ SIGNIFICANT_ATTRIBUTES: set[str] = {
ATTR_HVAC_ACTION,
ATTR_PRESET_MODE,
ATTR_SWING_MODE,
ATTR_SWING_HORIZONTAL_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
ATTR_TEMPERATURE,
@@ -70,6 +72,7 @@ def async_check_significant_change(
ATTR_HVAC_ACTION,
ATTR_PRESET_MODE,
ATTR_SWING_MODE,
ATTR_SWING_HORIZONTAL_MODE,
]:
return True

View File

@@ -123,6 +123,16 @@
"swing_modes": {
"name": "Swing modes"
},
"swing_horizontal_mode": {
"name": "Horizontal swing mode",
"state": {
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]"
}
},
"swing_horizontal_modes": {
"name": "Horizontal swing modes"
},
"target_temp_high": {
"name": "Upper target temperature"
},
@@ -221,6 +231,16 @@
}
}
},
"set_swing_horizontal_mode": {
"name": "Set horizontal swing mode",
"description": "Sets horizontal swing operation mode.",
"fields": {
"swing_horizontal_mode": {
"name": "Horizontal swing mode",
"description": "Horizontal swing operation mode."
}
}
},
"turn_on": {
"name": "[%key:common::action::turn_on%]",
"description": "Turns climate device on."
@@ -264,6 +284,9 @@
"not_valid_swing_mode": {
"message": "Swing mode {mode} is not valid. Valid swing modes are: {modes}."
},
"not_valid_horizontal_swing_mode": {
"message": "Horizontal swing mode {mode} is not valid. Valid horizontal swing modes are: {modes}."
},
"not_valid_fan_mode": {
"message": "Fan mode {mode} is not valid. Valid fan modes are: {modes}."
},

View File

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

View File

@@ -43,6 +43,7 @@ async def async_setup_entry(
target_humidity=None,
current_humidity=None,
swing_mode=None,
swing_horizontal_mode=None,
hvac_mode=HVACMode.HEAT,
hvac_action=HVACAction.HEATING,
target_temp_high=None,
@@ -60,6 +61,7 @@ async def async_setup_entry(
target_humidity=67.4,
current_humidity=54.2,
swing_mode="off",
swing_horizontal_mode="auto",
hvac_mode=HVACMode.COOL,
hvac_action=HVACAction.COOLING,
target_temp_high=None,
@@ -78,6 +80,7 @@ async def async_setup_entry(
target_humidity=None,
current_humidity=None,
swing_mode="auto",
swing_horizontal_mode=None,
hvac_mode=HVACMode.HEAT_COOL,
hvac_action=None,
target_temp_high=24,
@@ -109,6 +112,7 @@ class DemoClimate(ClimateEntity):
target_humidity: float | None,
current_humidity: float | None,
swing_mode: str | None,
swing_horizontal_mode: str | None,
hvac_mode: HVACMode,
hvac_action: HVACAction | None,
target_temp_high: float | None,
@@ -129,6 +133,8 @@ class DemoClimate(ClimateEntity):
self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY
if swing_mode is not None:
self._attr_supported_features |= ClimateEntityFeature.SWING_MODE
if swing_horizontal_mode is not None:
self._attr_supported_features |= ClimateEntityFeature.SWING_HORIZONTAL_MODE
if HVACMode.HEAT_COOL in hvac_modes or HVACMode.AUTO in hvac_modes:
self._attr_supported_features |= (
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
@@ -147,9 +153,11 @@ class DemoClimate(ClimateEntity):
self._hvac_action = hvac_action
self._hvac_mode = hvac_mode
self._current_swing_mode = swing_mode
self._current_swing_horizontal_mode = swing_horizontal_mode
self._fan_modes = ["on_low", "on_high", "auto_low", "auto_high", "off"]
self._hvac_modes = hvac_modes
self._swing_modes = ["auto", "1", "2", "3", "off"]
self._swing_horizontal_modes = ["auto", "rangefull", "off"]
self._target_temperature_high = target_temp_high
self._target_temperature_low = target_temp_low
self._attr_device_info = DeviceInfo(
@@ -242,6 +250,16 @@ class DemoClimate(ClimateEntity):
"""List of available swing modes."""
return self._swing_modes
@property
def swing_horizontal_mode(self) -> str | None:
"""Return the swing setting."""
return self._current_swing_horizontal_mode
@property
def swing_horizontal_modes(self) -> list[str]:
"""List of available swing modes."""
return self._swing_horizontal_modes
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperatures."""
if kwargs.get(ATTR_TEMPERATURE) is not None:
@@ -266,6 +284,11 @@ class DemoClimate(ClimateEntity):
self._current_swing_mode = swing_mode
self.async_write_ha_state()
async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None:
"""Set new swing mode."""
self._current_swing_horizontal_mode = swing_horizontal_mode
self.async_write_ha_state()
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new fan mode."""
self._current_fan_mode = fan_mode

View File

@@ -19,6 +19,13 @@
"auto": "mdi:arrow-oscillating",
"off": "mdi:arrow-oscillating-off"
}
},
"swing_horizontal_mode": {
"state": {
"rangefull": "mdi:pan-horizontal",
"auto": "mdi:compare-horizontal",
"off": "mdi:arrow-oscillating-off"
}
}
}
}

View File

@@ -42,6 +42,13 @@
"auto": "Auto",
"off": "[%key:common::state::off%]"
}
},
"swing_horizontal_mode": {
"state": {
"rangefull": "Full range",
"auto": "Auto",
"off": "[%key:common::state::off%]"
}
}
}
}

View File

@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/denonavr",
"iot_class": "local_push",
"loggers": ["denonavr"],
"requirements": ["denonavr==1.0.0"],
"requirements": ["denonavr==1.0.1"],
"ssdp": [
{
"manufacturer": "Denon",

View File

@@ -6,11 +6,17 @@
"description": "[%key:common::config_flow::description::confirm_setup%]",
"data": {
"ip_address": "[%key:common::config_flow::data::ip%]"
},
"data_description": {
"ip_address": "IP address of your devolo Home Network device. This can be found in the devolo Home Network App on the device dashboard."
}
},
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"password": "Password you protected the device with."
}
},
"zeroconf_confirm": {

View File

@@ -11,12 +11,7 @@ from pydiscovergy.authentication import BasicAuth
import pydiscovergy.error as discovergyError
import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_REAUTH,
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
)
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.selector import (
@@ -57,35 +52,14 @@ class DiscovergyConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
_existing_entry: ConfigEntry
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id="user",
data_schema=CONFIG_SCHEMA,
)
return await self._validate_and_save(user_input)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle the initial step."""
self._existing_entry = self._get_reauth_entry()
return await self.async_step_reauth_confirm()
return await self.async_step_user()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the reauth step."""
return await self._validate_and_save(user_input, step_id="reauth_confirm")
async def _validate_and_save(
self, user_input: Mapping[str, Any] | None = None, step_id: str = "user"
async def async_step_user(
self, user_input: Mapping[str, Any] | None = None
) -> ConfigFlowResult:
"""Validate user input and create config entry."""
errors = {}
@@ -106,17 +80,17 @@ class DiscovergyConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected error occurred while getting meters")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(user_input[CONF_EMAIL].lower())
if self.source == SOURCE_REAUTH:
self._abort_if_unique_id_mismatch(reason="account_mismatch")
return self.async_update_reload_and_abort(
entry=self._existing_entry,
data={
CONF_EMAIL: user_input[CONF_EMAIL],
entry=self._get_reauth_entry(),
data_updates={
CONF_PASSWORD: user_input[CONF_PASSWORD],
},
)
# set unique id to title which is the account email
await self.async_set_unique_id(user_input[CONF_EMAIL].lower())
self._abort_if_unique_id_configured()
return self.async_create_entry(
@@ -124,10 +98,10 @@ class DiscovergyConfigFlow(ConfigFlow, domain=DOMAIN):
)
return self.async_show_form(
step_id=step_id,
step_id="user",
data_schema=self.add_suggested_values_to_schema(
CONFIG_SCHEMA,
self._existing_entry.data
self._get_reauth_entry().data
if self.source == SOURCE_REAUTH
else user_input,
),

View File

@@ -6,12 +6,6 @@
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
}
},
"reauth_confirm": {
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
@@ -21,6 +15,7 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"account_mismatch": "The inexogy account authenticated with, does not match the account needed re-authentication.",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},

View File

@@ -10,16 +10,31 @@ from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
CONF_API_KEY,
CONF_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_URL,
CONF_VALUE_TEMPLATE,
PERCENTAGE,
UnitOfApparentPower,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfFrequency,
UnitOfPower,
UnitOfPressure,
UnitOfSoundPressure,
UnitOfSpeed,
UnitOfTemperature,
UnitOfVolume,
UnitOfVolumeFlowRate,
)
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResultType
@@ -41,6 +56,146 @@ from .const import (
)
from .coordinator import EmoncmsCoordinator
SENSORS: dict[str | None, SensorEntityDescription] = {
"kWh": SensorEntityDescription(
key="energy|kWh",
translation_key="energy",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
"Wh": SensorEntityDescription(
key="energy|Wh",
translation_key="energy",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
"kW": SensorEntityDescription(
key="power|kW",
translation_key="power",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
),
"W": SensorEntityDescription(
key="power|W",
translation_key="power",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
),
"V": SensorEntityDescription(
key="voltage",
translation_key="voltage",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
),
"A": SensorEntityDescription(
key="current",
translation_key="current",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
),
"VA": SensorEntityDescription(
key="apparent_power",
translation_key="apparent_power",
device_class=SensorDeviceClass.APPARENT_POWER,
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
state_class=SensorStateClass.MEASUREMENT,
),
"°C": SensorEntityDescription(
key="temperature|celsius",
translation_key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
),
"°F": SensorEntityDescription(
key="temperature|fahrenheit",
translation_key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
state_class=SensorStateClass.MEASUREMENT,
),
"K": SensorEntityDescription(
key="temperature|kelvin",
translation_key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.KELVIN,
state_class=SensorStateClass.MEASUREMENT,
),
"Hz": SensorEntityDescription(
key="frequency",
translation_key="frequency",
device_class=SensorDeviceClass.FREQUENCY,
native_unit_of_measurement=UnitOfFrequency.HERTZ,
state_class=SensorStateClass.MEASUREMENT,
),
"hPa": SensorEntityDescription(
key="pressure",
translation_key="pressure",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.HPA,
state_class=SensorStateClass.MEASUREMENT,
),
"dB": SensorEntityDescription(
key="decibel",
translation_key="decibel",
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
native_unit_of_measurement=UnitOfSoundPressure.DECIBEL,
state_class=SensorStateClass.MEASUREMENT,
),
"": SensorEntityDescription(
key="volume|cubic_meter",
translation_key="volume",
device_class=SensorDeviceClass.VOLUME_STORAGE,
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
state_class=SensorStateClass.MEASUREMENT,
),
"m³/h": SensorEntityDescription(
key="flow|cubic_meters_per_hour",
translation_key="flow",
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
state_class=SensorStateClass.MEASUREMENT,
),
"l/m": SensorEntityDescription(
key="flow|liters_per_minute",
translation_key="flow",
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
native_unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_MINUTE,
state_class=SensorStateClass.MEASUREMENT,
),
"m/s": SensorEntityDescription(
key="speed|meters_per_second",
translation_key="speed",
device_class=SensorDeviceClass.SPEED,
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
state_class=SensorStateClass.MEASUREMENT,
),
"µg/m³": SensorEntityDescription(
key="concentration|microgram_per_cubic_meter",
translation_key="concentration",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
"ppm": SensorEntityDescription(
key="concentration|microgram_parts_per_million",
translation_key="concentration",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
),
"%": SensorEntityDescription(
key="percent",
translation_key="percent",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
}
ATTR_FEEDID = "FeedId"
ATTR_FEEDNAME = "FeedName"
ATTR_LASTUPDATETIME = "LastUpdated"
@@ -173,6 +328,8 @@ async def async_setup_entry(
class EmonCmsSensor(CoordinatorEntity[EmoncmsCoordinator], SensorEntity):
"""Implementation of an Emoncms sensor."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: EmoncmsCoordinator,
@@ -187,33 +344,15 @@ class EmonCmsSensor(CoordinatorEntity[EmoncmsCoordinator], SensorEntity):
elem = {}
if self.coordinator.data:
elem = self.coordinator.data[self.idx]
self._attr_name = f"{name} {elem[FEED_NAME]}"
self._attr_native_unit_of_measurement = unit_of_measurement
self._attr_translation_placeholders = {
"emoncms_details": f"{elem[FEED_TAG]} {elem[FEED_NAME]}",
}
self._attr_unique_id = f"{unique_id}-{elem[FEED_ID]}"
if unit_of_measurement in ("kWh", "Wh"):
self._attr_device_class = SensorDeviceClass.ENERGY
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
elif unit_of_measurement == "W":
self._attr_device_class = SensorDeviceClass.POWER
self._attr_state_class = SensorStateClass.MEASUREMENT
elif unit_of_measurement == "V":
self._attr_device_class = SensorDeviceClass.VOLTAGE
self._attr_state_class = SensorStateClass.MEASUREMENT
elif unit_of_measurement == "A":
self._attr_device_class = SensorDeviceClass.CURRENT
self._attr_state_class = SensorStateClass.MEASUREMENT
elif unit_of_measurement == "VA":
self._attr_device_class = SensorDeviceClass.APPARENT_POWER
self._attr_state_class = SensorStateClass.MEASUREMENT
elif unit_of_measurement in ("°C", "°F", "K"):
self._attr_device_class = SensorDeviceClass.TEMPERATURE
self._attr_state_class = SensorStateClass.MEASUREMENT
elif unit_of_measurement == "Hz":
self._attr_device_class = SensorDeviceClass.FREQUENCY
self._attr_state_class = SensorStateClass.MEASUREMENT
elif unit_of_measurement == "hPa":
self._attr_device_class = SensorDeviceClass.PRESSURE
self._attr_state_class = SensorStateClass.MEASUREMENT
description = SENSORS.get(unit_of_measurement)
if description is not None:
self.entity_description = description
else:
self._attr_native_unit_of_measurement = unit_of_measurement
self._update_attributes(elem)
def _update_attributes(self, elem: dict[str, Any]) -> None:

View File

@@ -24,6 +24,52 @@
"already_configured": "This server is already configured"
}
},
"entity": {
"sensor": {
"energy": {
"name": "Energy {emoncms_details}"
},
"power": {
"name": "Power {emoncms_details}"
},
"percent": {
"name": "Percentage {emoncms_details}"
},
"voltage": {
"name": "Voltage {emoncms_details}"
},
"current": {
"name": "Current {emoncms_details}"
},
"apparent_power": {
"name": "Apparent power {emoncms_details}"
},
"temperature": {
"name": "Temperature {emoncms_details}"
},
"frequency": {
"name": "Frequency {emoncms_details}"
},
"pressure": {
"name": "Pressure {emoncms_details}"
},
"decibel": {
"name": "Decibel {emoncms_details}"
},
"volume": {
"name": "Volume {emoncms_details}"
},
"flow": {
"name": "Flow rate {emoncms_details}"
},
"speed": {
"name": "Speed {emoncms_details}"
},
"concentration": {
"name": "Concentration {emoncms_details}"
}
}
},
"options": {
"error": {
"api_error": "[%key:component::emoncms::config::error::api_error%]"

View File

@@ -95,11 +95,7 @@ async def async_setup_entry(
if entry_data.device_info.voice_assistant_feature_flags_compat(
entry_data.api_version
):
async_add_entities(
[
EsphomeAssistSatellite(entry, entry_data),
]
)
async_add_entities([EsphomeAssistSatellite(entry, entry_data)])
class EsphomeAssistSatellite(
@@ -198,6 +194,9 @@ class EsphomeAssistSatellite(
self._satellite_config.max_active_wake_words = config.max_active_wake_words
_LOGGER.debug("Received satellite configuration: %s", self._satellite_config)
# Inform listeners that config has been updated
self.entry_data.async_assist_satellite_config_updated(self._satellite_config)
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
await super().async_added_to_hass()
@@ -254,6 +253,13 @@ class EsphomeAssistSatellite(
# Will use media player for TTS/announcements
self._update_tts_format()
# Update wake word select when config is updated
self.async_on_remove(
self.entry_data.async_register_assist_satellite_set_wake_word_callback(
self.async_set_wake_word
)
)
async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass."""
await super().async_will_remove_from_hass()
@@ -478,6 +484,17 @@ class EsphomeAssistSatellite(
"""Handle announcement finished message (also sent for TTS)."""
self.tts_response_finished()
@callback
def async_set_wake_word(self, wake_word_id: str) -> None:
"""Set active wake word and update config on satellite."""
self._satellite_config.active_wake_words = [wake_word_id]
self.config_entry.async_create_background_task(
self.hass,
self.async_set_configuration(self._satellite_config),
"esphome_voice_assistant_set_config",
)
_LOGGER.debug("Setting active wake word: %s", wake_word_id)
def _update_tts_format(self) -> None:
"""Update the TTS format from the first media player."""
for supported_format in chain(*self.entry_data.media_player_formats.values()):

View File

@@ -48,6 +48,7 @@ from aioesphomeapi import (
from aioesphomeapi.model import ButtonInfo
from bleak_esphome.backend.device import ESPHomeBluetoothDevice
from homeassistant.components.assist_satellite import AssistSatelliteConfiguration
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
@@ -152,6 +153,12 @@ class RuntimeEntryData:
media_player_formats: dict[str, list[MediaPlayerSupportedFormat]] = field(
default_factory=lambda: defaultdict(list)
)
assist_satellite_config_update_callbacks: list[
Callable[[AssistSatelliteConfiguration], None]
] = field(default_factory=list)
assist_satellite_set_wake_word_callbacks: list[Callable[[str], None]] = field(
default_factory=list
)
@property
def name(self) -> str:
@@ -504,3 +511,35 @@ class RuntimeEntryData:
# We use this to determine if a deep sleep device should
# be marked as unavailable or not.
self.expected_disconnect = True
@callback
def async_register_assist_satellite_config_updated_callback(
self,
callback_: Callable[[AssistSatelliteConfiguration], None],
) -> CALLBACK_TYPE:
"""Register to receive callbacks when the Assist satellite's configuration is updated."""
self.assist_satellite_config_update_callbacks.append(callback_)
return lambda: self.assist_satellite_config_update_callbacks.remove(callback_)
@callback
def async_assist_satellite_config_updated(
self, config: AssistSatelliteConfiguration
) -> None:
"""Notify listeners that the Assist satellite configuration has been updated."""
for callback_ in self.assist_satellite_config_update_callbacks.copy():
callback_(config)
@callback
def async_register_assist_satellite_set_wake_word_callback(
self,
callback_: Callable[[str], None],
) -> CALLBACK_TYPE:
"""Register to receive callbacks when the Assist satellite's wake word is set."""
self.assist_satellite_set_wake_word_callbacks.append(callback_)
return lambda: self.assist_satellite_set_wake_word_callbacks.remove(callback_)
@callback
def async_assist_satellite_set_wake_word(self, wake_word_id: str) -> None:
"""Notify listeners that the Assist satellite wake word has been set."""
for callback_ in self.assist_satellite_set_wake_word_callbacks.copy():
callback_(wake_word_id)

View File

@@ -212,6 +212,10 @@ class FFmpegConvertResponse(web.StreamResponse):
assert proc.stdout is not None
assert proc.stderr is not None
stderr_task = self.hass.async_create_background_task(
self._dump_ffmpeg_stderr(proc), "ESPHome media proxy dump stderr"
)
try:
# Pull audio chunks from ffmpeg and pass them to the HTTP client
while (
@@ -230,18 +234,14 @@ class FFmpegConvertResponse(web.StreamResponse):
raise # don't log error
except:
_LOGGER.exception("Unexpected error during ffmpeg conversion")
# Process did not exit successfully
stderr_text = ""
while line := await proc.stderr.readline():
stderr_text += line.decode()
_LOGGER.error("FFmpeg output: %s", stderr_text)
raise
finally:
# Allow conversion info to be removed
self.convert_info.is_finished = True
# stop dumping ffmpeg stderr task
stderr_task.cancel()
# Terminate hangs, so kill is used
if proc.returncode is None:
proc.kill()
@@ -250,6 +250,16 @@ class FFmpegConvertResponse(web.StreamResponse):
if request.transport and not request.transport.is_closing():
await writer.write_eof()
async def _dump_ffmpeg_stderr(
self,
proc: asyncio.subprocess.Process,
) -> None:
assert proc.stdout is not None
assert proc.stderr is not None
while self.hass.is_running and (chunk := await proc.stderr.readline()):
_LOGGER.debug("ffmpeg[%s] output: %s", proc.pid, chunk.decode().rstrip())
class FFmpegProxyView(HomeAssistantView):
"""FFmpeg web view to convert audio and stream back to client."""

View File

@@ -16,7 +16,7 @@
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
"mqtt": ["esphome/discover/#"],
"requirements": [
"aioesphomeapi==27.0.2",
"aioesphomeapi==27.0.3",
"esphome-dashboard-api==1.2.3",
"bleak-esphome==1.1.0"
],

View File

@@ -8,8 +8,11 @@ from homeassistant.components.assist_pipeline.select import (
AssistPipelineSelect,
VadSensitivitySelect,
)
from homeassistant.components.select import SelectEntity
from homeassistant.components.assist_satellite import AssistSatelliteConfiguration
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import restore_state
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
@@ -47,6 +50,7 @@ async def async_setup_entry(
[
EsphomeAssistPipelineSelect(hass, entry_data),
EsphomeVadSensitivitySelect(hass, entry_data),
EsphomeAssistSatelliteWakeWordSelect(hass, entry_data),
]
)
@@ -89,3 +93,77 @@ class EsphomeVadSensitivitySelect(EsphomeAssistEntity, VadSensitivitySelect):
"""Initialize a VAD sensitivity selector."""
EsphomeAssistEntity.__init__(self, entry_data)
VadSensitivitySelect.__init__(self, hass, self._device_info.mac_address)
class EsphomeAssistSatelliteWakeWordSelect(
EsphomeAssistEntity, SelectEntity, restore_state.RestoreEntity
):
"""Wake word selector for esphome devices."""
entity_description = SelectEntityDescription(
key="wake_word",
translation_key="wake_word",
entity_category=EntityCategory.CONFIG,
)
_attr_should_poll = False
_attr_current_option: str | None = None
_attr_options: list[str] = []
def __init__(self, hass: HomeAssistant, entry_data: RuntimeEntryData) -> None:
"""Initialize a wake word selector."""
EsphomeAssistEntity.__init__(self, entry_data)
unique_id_prefix = self._device_info.mac_address
self._attr_unique_id = f"{unique_id_prefix}-wake_word"
# name -> id
self._wake_words: dict[str, str] = {}
@property
def available(self) -> bool:
"""Return if entity is available."""
return bool(self._attr_options)
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
await super().async_added_to_hass()
# Update options when config is updated
self.async_on_remove(
self._entry_data.async_register_assist_satellite_config_updated_callback(
self.async_satellite_config_updated
)
)
async def async_select_option(self, option: str) -> None:
"""Select an option."""
if wake_word_id := self._wake_words.get(option):
# _attr_current_option will be updated on
# async_satellite_config_updated after the device sets the wake
# word.
self._entry_data.async_assist_satellite_set_wake_word(wake_word_id)
def async_satellite_config_updated(
self, config: AssistSatelliteConfiguration
) -> None:
"""Update options with available wake words."""
if (not config.available_wake_words) or (config.max_active_wake_words < 1):
self._attr_current_option = None
self._wake_words.clear()
self.async_write_ha_state()
return
self._wake_words = {w.wake_word: w.id for w in config.available_wake_words}
self._attr_options = sorted(self._wake_words)
if config.active_wake_words:
# Select first active wake word
wake_word_id = config.active_wake_words[0]
for wake_word in config.available_wake_words:
if wake_word.id == wake_word_id:
self._attr_current_option = wake_word.wake_word
else:
# Select first available wake word
self._attr_current_option = config.available_wake_words[0].wake_word
self.async_write_ha_state()

View File

@@ -84,6 +84,12 @@
"aggressive": "[%key:component::assist_pipeline::entity::select::vad_sensitivity::state::aggressive%]",
"relaxed": "[%key:component::assist_pipeline::entity::select::vad_sensitivity::state::relaxed%]"
}
},
"wake_word": {
"name": "Wake word",
"state": {
"okay_nabu": "Okay Nabu"
}
}
},
"climate": {

View File

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

View File

@@ -9,7 +9,6 @@ from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .coordinator import GaragesAmsterdamDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
@@ -17,24 +16,23 @@ PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
type GaragesAmsterdamConfigEntry = ConfigEntry[GaragesAmsterdamDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(
hass: HomeAssistant, entry: GaragesAmsterdamConfigEntry
) -> bool:
"""Set up Garages Amsterdam from a config entry."""
client = ODPAmsterdam(session=async_get_clientsession(hass))
coordinator = GaragesAmsterdamDataUpdateCoordinator(hass, client)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[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: GaragesAmsterdamConfigEntry
) -> bool:
"""Unload Garages Amsterdam config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if len(hass.config_entries.async_entries(DOMAIN)) == 1:
hass.data.pop(DOMAIN)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -2,48 +2,77 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from odp_amsterdam import Garage
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from . import GaragesAmsterdamConfigEntry
from .coordinator import GaragesAmsterdamDataUpdateCoordinator
from .entity import GaragesAmsterdamEntity
BINARY_SENSORS = {
"state",
}
@dataclass(frozen=True, kw_only=True)
class GaragesAmsterdamBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Class describing Garages Amsterdam binary sensor entity."""
is_on: Callable[[Garage], bool]
BINARY_SENSORS: tuple[GaragesAmsterdamBinarySensorEntityDescription, ...] = (
GaragesAmsterdamBinarySensorEntityDescription(
key="state",
translation_key="state",
device_class=BinarySensorDeviceClass.PROBLEM,
is_on=lambda garage: garage.state != "ok",
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: GaragesAmsterdamConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Defer sensor setup to the shared sensor module."""
coordinator: GaragesAmsterdamDataUpdateCoordinator = hass.data[DOMAIN][
entry.entry_id
]
coordinator = entry.runtime_data
async_add_entities(
GaragesAmsterdamBinarySensor(coordinator, entry.data["garage_name"], info_type)
for info_type in BINARY_SENSORS
GaragesAmsterdamBinarySensor(
coordinator=coordinator,
garage_name=entry.data["garage_name"],
description=description,
)
for description in BINARY_SENSORS
)
class GaragesAmsterdamBinarySensor(GaragesAmsterdamEntity, BinarySensorEntity):
"""Binary Sensor representing garages amsterdam data."""
_attr_device_class = BinarySensorDeviceClass.PROBLEM
_attr_name = None
entity_description: GaragesAmsterdamBinarySensorEntityDescription
def __init__(
self,
*,
coordinator: GaragesAmsterdamDataUpdateCoordinator,
garage_name: str,
description: GaragesAmsterdamBinarySensorEntityDescription,
) -> None:
"""Initialize garages amsterdam binary sensor."""
super().__init__(coordinator, garage_name)
self.entity_description = description
self._attr_unique_id = f"{garage_name}-{description.key}"
@property
def is_on(self) -> bool:
"""If the binary sensor is currently on or off."""
return (
getattr(self.coordinator.data[self._garage_name], self._info_type) != "ok"
)
return self.entity_description.is_on(self.coordinator.data[self._garage_name])

View File

@@ -7,7 +7,7 @@ import logging
from typing import Final
DOMAIN: Final = "garages_amsterdam"
ATTRIBUTION = f'{"Data provided by municipality of Amsterdam"}'
ATTRIBUTION = "Data provided by municipality of Amsterdam"
LOGGER = logging.getLogger(__package__)
SCAN_INTERVAL = timedelta(minutes=10)

View File

@@ -19,13 +19,10 @@ class GaragesAmsterdamEntity(CoordinatorEntity[GaragesAmsterdamDataUpdateCoordin
self,
coordinator: GaragesAmsterdamDataUpdateCoordinator,
garage_name: str,
info_type: str,
) -> None:
"""Initialize garages amsterdam entity."""
super().__init__(coordinator)
self._attr_unique_id = f"{garage_name}-{info_type}"
self._garage_name = garage_name
self._info_type = info_type
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, garage_name)},
name=garage_name,

View File

@@ -2,54 +2,93 @@
from __future__ import annotations
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from collections.abc import Callable
from dataclasses import dataclass
from odp_amsterdam import Garage
from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from .const import DOMAIN
from . import GaragesAmsterdamConfigEntry
from .coordinator import GaragesAmsterdamDataUpdateCoordinator
from .entity import GaragesAmsterdamEntity
SENSORS = {
"free_space_short",
"free_space_long",
"short_capacity",
"long_capacity",
}
@dataclass(frozen=True, kw_only=True)
class GaragesAmsterdamSensorEntityDescription(SensorEntityDescription):
"""Class describing Garages Amsterdam sensor entity."""
value_fn: Callable[[Garage], StateType]
SENSORS: tuple[GaragesAmsterdamSensorEntityDescription, ...] = (
GaragesAmsterdamSensorEntityDescription(
key="free_space_short",
translation_key="free_space_short",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda garage: garage.free_space_short,
),
GaragesAmsterdamSensorEntityDescription(
key="free_space_long",
translation_key="free_space_long",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda garage: garage.free_space_long,
),
GaragesAmsterdamSensorEntityDescription(
key="short_capacity",
translation_key="short_capacity",
value_fn=lambda garage: garage.short_capacity,
),
GaragesAmsterdamSensorEntityDescription(
key="long_capacity",
translation_key="long_capacity",
value_fn=lambda garage: garage.long_capacity,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: GaragesAmsterdamConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Defer sensor setup to the shared sensor module."""
coordinator: GaragesAmsterdamDataUpdateCoordinator = hass.data[DOMAIN][
entry.entry_id
]
coordinator = entry.runtime_data
async_add_entities(
GaragesAmsterdamSensor(coordinator, entry.data["garage_name"], info_type)
for info_type in SENSORS
if getattr(coordinator.data[entry.data["garage_name"]], info_type) != ""
GaragesAmsterdamSensor(
coordinator=coordinator,
garage_name=entry.data["garage_name"],
description=description,
)
for description in SENSORS
if description.value_fn(coordinator.data[entry.data["garage_name"]]) is not None
)
class GaragesAmsterdamSensor(GaragesAmsterdamEntity, SensorEntity):
"""Sensor representing garages amsterdam data."""
_attr_native_unit_of_measurement = "cars"
entity_description: GaragesAmsterdamSensorEntityDescription
def __init__(
self,
*,
coordinator: GaragesAmsterdamDataUpdateCoordinator,
garage_name: str,
info_type: str,
description: GaragesAmsterdamSensorEntityDescription,
) -> None:
"""Initialize garages amsterdam sensor."""
super().__init__(coordinator, garage_name, info_type)
self._attr_translation_key = info_type
super().__init__(coordinator, garage_name)
self.entity_description = description
self._attr_unique_id = f"{garage_name}-{description.key}"
@property
def available(self) -> bool:
@@ -59,6 +98,8 @@ class GaragesAmsterdamSensor(GaragesAmsterdamEntity, SensorEntity):
)
@property
def native_value(self) -> str:
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return getattr(self.coordinator.data[self._garage_name], self._info_type)
return self.entity_description.value_fn(
self.coordinator.data[self._garage_name]
)

View File

@@ -3,8 +3,13 @@
"config": {
"step": {
"user": {
"title": "Pick a garage to monitor",
"data": { "garage_name": "Garage name" }
"description": "Select a garage from the list",
"data": {
"garage_name": "Garage name"
},
"data_description": {
"garage_name": "The name of the garage you want to monitor."
}
}
},
"abort": {
@@ -16,16 +21,25 @@
"entity": {
"sensor": {
"free_space_short": {
"name": "Short parking free space"
"name": "Short parking free space",
"unit_of_measurement": "cars"
},
"free_space_long": {
"name": "Long parking free space"
"name": "Long parking free space",
"unit_of_measurement": "cars"
},
"short_capacity": {
"name": "Short parking capacity"
"name": "Short parking capacity",
"unit_of_measurement": "cars"
},
"long_capacity": {
"name": "Long parking capacity"
"name": "Long parking capacity",
"unit_of_measurement": "cars"
}
},
"binary_sensor": {
"state": {
"name": "State"
}
}
}

View File

@@ -0,0 +1,27 @@
"""Diagnostics platform for Habitica integration."""
from __future__ import annotations
from typing import Any
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant
from .const import CONF_API_USER
from .types import HabiticaConfigEntry
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: HabiticaConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
habitica_data = await config_entry.runtime_data.api.user.anonymized.get()
return {
"config_entry_data": {
CONF_URL: config_entry.data[CONF_URL],
CONF_API_USER: config_entry.data[CONF_API_USER],
},
"habitica_data": habitica_data,
}

View File

@@ -174,7 +174,7 @@ def get_attribute_points(
)
return {
"level": min(round(user["stats"]["lvl"] / 2), 50),
"level": min(floor(user["stats"]["lvl"] / 2), 50),
"equipment": equipment,
"class": class_bonus,
"allocated": user["stats"][attribute],

View File

@@ -22,7 +22,7 @@ import homeassistant.util.dt as dt_util
from . import websocket_api
from .const import DOMAIN
from .helpers import entities_may_have_state_changes_after, has_recorder_run_after
from .helpers import entities_may_have_state_changes_after, has_states_before
CONF_ORDER = "use_include_order"
@@ -107,7 +107,10 @@ class HistoryPeriodView(HomeAssistantView):
no_attributes = "no_attributes" in request.query
if (
(end_time and not has_recorder_run_after(hass, end_time))
# has_states_before will return True if there are states older than
# end_time. If it's false, we know there are no states in the
# database up until end_time.
(end_time and not has_states_before(hass, end_time))
or not include_start_time_state
and entity_ids
and not entities_may_have_state_changes_after(

View File

@@ -6,7 +6,6 @@ from collections.abc import Iterable
from datetime import datetime as dt
from homeassistant.components.recorder import get_instance
from homeassistant.components.recorder.models import process_timestamp
from homeassistant.core import HomeAssistant
@@ -26,8 +25,10 @@ def entities_may_have_state_changes_after(
return False
def has_recorder_run_after(hass: HomeAssistant, run_time: dt) -> bool:
"""Check if the recorder has any runs after a specific time."""
return run_time >= process_timestamp(
get_instance(hass).recorder_runs_manager.first.start
)
def has_states_before(hass: HomeAssistant, run_time: dt) -> bool:
"""Check if the recorder has states as old or older than run_time.
Returns True if there may be such states.
"""
oldest_ts = get_instance(hass).states_manager.oldest_ts
return oldest_ts is not None and run_time.timestamp() >= oldest_ts

View File

@@ -39,7 +39,7 @@ from homeassistant.util.async_ import create_eager_task
import homeassistant.util.dt as dt_util
from .const import EVENT_COALESCE_TIME, MAX_PENDING_HISTORY_STATES
from .helpers import entities_may_have_state_changes_after, has_recorder_run_after
from .helpers import entities_may_have_state_changes_after, has_states_before
_LOGGER = logging.getLogger(__name__)
@@ -142,7 +142,10 @@ async def ws_get_history_during_period(
no_attributes = msg["no_attributes"]
if (
(end_time and not has_recorder_run_after(hass, end_time))
# has_states_before will return True if there are states older than
# end_time. If it's false, we know there are no states in the
# database up until end_time.
(end_time and not has_states_before(hass, end_time))
or not include_start_time_state
and entity_ids
and not entities_may_have_state_changes_after(

View File

@@ -4,6 +4,8 @@ from __future__ import annotations
from dataclasses import dataclass
import datetime
import logging
import math
from homeassistant.components.recorder import get_instance, history
from homeassistant.core import Event, EventStateChangedData, HomeAssistant, State
@@ -14,6 +16,8 @@ from .helpers import async_calculate_period, floored_timestamp
MIN_TIME_UTC = datetime.datetime.min.replace(tzinfo=dt_util.UTC)
_LOGGER = logging.getLogger(__name__)
@dataclass
class HistoryStatsState:
@@ -186,8 +190,13 @@ class HistoryStats:
current_state_matches = history_state.state in self._entity_states
state_change_timestamp = history_state.last_changed
if state_change_timestamp > now_timestamp:
if math.floor(state_change_timestamp) > now_timestamp:
# Shouldn't count states that are in the future
_LOGGER.debug(
"Skipping future timestamp %s (now %s)",
state_change_timestamp,
now_timestamp,
)
continue
if previous_state_matches:

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from datetime import timedelta
import logging
import re
from typing import Any, cast
from requests import HTTPError
@@ -44,6 +45,8 @@ type HomeConnectConfigEntry = ConfigEntry[api.ConfigEntryAuth]
_LOGGER = logging.getLogger(__name__)
RE_CAMEL_CASE = re.compile(r"(?<!^)(?=[A-Z])|(?=\d)(?<=\D)")
SCAN_INTERVAL = timedelta(minutes=1)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -85,6 +88,7 @@ PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.LIGHT,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.TIME,
@@ -336,3 +340,14 @@ def get_dict_from_home_connect_error(err: api.HomeConnectError) -> dict[str, Any
if len(err.args) > 0 and isinstance(err.args[0], str)
else "?",
}
def bsh_key_to_translation_key(bsh_key: str) -> str:
"""Convert a BSH key to a translation key format.
This function takes a BSH key, such as `Dishcare.Dishwasher.Program.Eco50`,
and converts it to a translation key format, such as `dishcare_dishwasher_bsh_key_eco50`.
"""
return "_".join(
RE_CAMEL_CASE.sub("_", split) for split in bsh_key.split(".")
).lower()

View File

@@ -5,10 +5,23 @@ DOMAIN = "home_connect"
OAUTH2_AUTHORIZE = "https://api.home-connect.com/security/oauth/authorize"
OAUTH2_TOKEN = "https://api.home-connect.com/security/oauth/token"
APPLIANCES_WITH_PROGRAMS = (
"CleaningRobot",
"CoffeeMaker",
"Dishwasher",
"Dryer",
"Hood",
"Oven",
"WarmingDrawer",
"Washer",
"WasherDryer",
)
BSH_POWER_STATE = "BSH.Common.Setting.PowerState"
BSH_POWER_ON = "BSH.Common.EnumType.PowerState.On"
BSH_POWER_OFF = "BSH.Common.EnumType.PowerState.Off"
BSH_POWER_STANDBY = "BSH.Common.EnumType.PowerState.Standby"
BSH_SELECTED_PROGRAM = "BSH.Common.Root.SelectedProgram"
BSH_ACTIVE_PROGRAM = "BSH.Common.Root.ActiveProgram"
BSH_REMOTE_CONTROL_ACTIVATION_STATE = "BSH.Common.Status.RemoteControlActive"
BSH_REMOTE_START_ALLOWANCE_STATE = "BSH.Common.Status.RemoteControlStartAllowed"

View File

@@ -0,0 +1,300 @@
"""Provides a select platform for Home Connect."""
import contextlib
import logging
from homeconnect.api import HomeConnectError
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import (
HomeConnectConfigEntry,
bsh_key_to_translation_key,
get_dict_from_home_connect_error,
)
from .api import HomeConnectDevice
from .const import (
APPLIANCES_WITH_PROGRAMS,
ATTR_VALUE,
BSH_ACTIVE_PROGRAM,
BSH_SELECTED_PROGRAM,
DOMAIN,
)
from .entity import HomeConnectEntity
_LOGGER = logging.getLogger(__name__)
TRANSLATION_KEYS_PROGRAMS_MAP = {
bsh_key_to_translation_key(program): program
for program in (
"ConsumerProducts.CleaningRobot.Program.Cleaning.CleanAll",
"ConsumerProducts.CleaningRobot.Program.Cleaning.CleanMap",
"ConsumerProducts.CleaningRobot.Program.Basic.GoHome",
"ConsumerProducts.CoffeeMaker.Program.Beverage.Ristretto",
"ConsumerProducts.CoffeeMaker.Program.Beverage.Espresso",
"ConsumerProducts.CoffeeMaker.Program.Beverage.EspressoDoppio",
"ConsumerProducts.CoffeeMaker.Program.Beverage.Coffee",
"ConsumerProducts.CoffeeMaker.Program.Beverage.XLCoffee",
"ConsumerProducts.CoffeeMaker.Program.Beverage.CaffeGrande",
"ConsumerProducts.CoffeeMaker.Program.Beverage.EspressoMacchiato",
"ConsumerProducts.CoffeeMaker.Program.Beverage.Cappuccino",
"ConsumerProducts.CoffeeMaker.Program.Beverage.LatteMacchiato",
"ConsumerProducts.CoffeeMaker.Program.Beverage.CaffeLatte",
"ConsumerProducts.CoffeeMaker.Program.Beverage.MilkFroth",
"ConsumerProducts.CoffeeMaker.Program.Beverage.WarmMilk",
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.KleinerBrauner",
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.GrosserBrauner",
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Verlaengerter",
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.VerlaengerterBraun",
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.WienerMelange",
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.FlatWhite",
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Cortado",
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.CafeCortado",
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.CafeConLeche",
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.CafeAuLait",
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Doppio",
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Kaapi",
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.KoffieVerkeerd",
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Galao",
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Garoto",
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Americano",
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.RedEye",
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.BlackEye",
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.DeadEye",
"ConsumerProducts.CoffeeMaker.Program.Beverage.HotWater",
"Dishcare.Dishwasher.Program.PreRinse",
"Dishcare.Dishwasher.Program.Auto1",
"Dishcare.Dishwasher.Program.Auto2",
"Dishcare.Dishwasher.Program.Auto3",
"Dishcare.Dishwasher.Program.Eco50",
"Dishcare.Dishwasher.Program.Quick45",
"Dishcare.Dishwasher.Program.Intensiv70",
"Dishcare.Dishwasher.Program.Normal65",
"Dishcare.Dishwasher.Program.Glas40",
"Dishcare.Dishwasher.Program.GlassCare",
"Dishcare.Dishwasher.Program.NightWash",
"Dishcare.Dishwasher.Program.Quick65",
"Dishcare.Dishwasher.Program.Normal45",
"Dishcare.Dishwasher.Program.Intensiv45",
"Dishcare.Dishwasher.Program.AutoHalfLoad",
"Dishcare.Dishwasher.Program.IntensivPower",
"Dishcare.Dishwasher.Program.MagicDaily",
"Dishcare.Dishwasher.Program.Super60",
"Dishcare.Dishwasher.Program.Kurz60",
"Dishcare.Dishwasher.Program.ExpressSparkle65",
"Dishcare.Dishwasher.Program.MachineCare",
"Dishcare.Dishwasher.Program.SteamFresh",
"Dishcare.Dishwasher.Program.MaximumCleaning",
"Dishcare.Dishwasher.Program.MixedLoad",
"LaundryCare.Dryer.Program.Cotton",
"LaundryCare.Dryer.Program.Synthetic",
"LaundryCare.Dryer.Program.Mix",
"LaundryCare.Dryer.Program.Blankets",
"LaundryCare.Dryer.Program.BusinessShirts",
"LaundryCare.Dryer.Program.DownFeathers",
"LaundryCare.Dryer.Program.Hygiene",
"LaundryCare.Dryer.Program.Jeans",
"LaundryCare.Dryer.Program.Outdoor",
"LaundryCare.Dryer.Program.SyntheticRefresh",
"LaundryCare.Dryer.Program.Towels",
"LaundryCare.Dryer.Program.Delicates",
"LaundryCare.Dryer.Program.Super40",
"LaundryCare.Dryer.Program.Shirts15",
"LaundryCare.Dryer.Program.Pillow",
"LaundryCare.Dryer.Program.AntiShrink",
"LaundryCare.Dryer.Program.MyTime.MyDryingTime",
"LaundryCare.Dryer.Program.TimeCold",
"LaundryCare.Dryer.Program.TimeWarm",
"LaundryCare.Dryer.Program.InBasket",
"LaundryCare.Dryer.Program.TimeColdFix.TimeCold20",
"LaundryCare.Dryer.Program.TimeColdFix.TimeCold30",
"LaundryCare.Dryer.Program.TimeColdFix.TimeCold60",
"LaundryCare.Dryer.Program.TimeWarmFix.TimeWarm30",
"LaundryCare.Dryer.Program.TimeWarmFix.TimeWarm40",
"LaundryCare.Dryer.Program.TimeWarmFix.TimeWarm60",
"LaundryCare.Dryer.Program.Dessous",
"Cooking.Common.Program.Hood.Automatic",
"Cooking.Common.Program.Hood.Venting",
"Cooking.Common.Program.Hood.DelayedShutOff",
"Cooking.Oven.Program.HeatingMode.PreHeating",
"Cooking.Oven.Program.HeatingMode.HotAir",
"Cooking.Oven.Program.HeatingMode.HotAirEco",
"Cooking.Oven.Program.HeatingMode.HotAirGrilling",
"Cooking.Oven.Program.HeatingMode.TopBottomHeating",
"Cooking.Oven.Program.HeatingMode.TopBottomHeatingEco",
"Cooking.Oven.Program.HeatingMode.BottomHeating",
"Cooking.Oven.Program.HeatingMode.PizzaSetting",
"Cooking.Oven.Program.HeatingMode.SlowCook",
"Cooking.Oven.Program.HeatingMode.IntensiveHeat",
"Cooking.Oven.Program.HeatingMode.KeepWarm",
"Cooking.Oven.Program.HeatingMode.PreheatOvenware",
"Cooking.Oven.Program.HeatingMode.FrozenHeatupSpecial",
"Cooking.Oven.Program.HeatingMode.Desiccation",
"Cooking.Oven.Program.HeatingMode.Defrost",
"Cooking.Oven.Program.HeatingMode.Proof",
"Cooking.Oven.Program.HeatingMode.HotAir30Steam",
"Cooking.Oven.Program.HeatingMode.HotAir60Steam",
"Cooking.Oven.Program.HeatingMode.HotAir80Steam",
"Cooking.Oven.Program.HeatingMode.HotAir100Steam",
"Cooking.Oven.Program.HeatingMode.SabbathProgramme",
"Cooking.Oven.Program.Microwave.90Watt",
"Cooking.Oven.Program.Microwave.180Watt",
"Cooking.Oven.Program.Microwave.360Watt",
"Cooking.Oven.Program.Microwave.600Watt",
"Cooking.Oven.Program.Microwave.900Watt",
"Cooking.Oven.Program.Microwave.1000Watt",
"Cooking.Oven.Program.Microwave.Max",
"Cooking.Oven.Program.HeatingMode.WarmingDrawer",
"LaundryCare.Washer.Program.Cotton",
"LaundryCare.Washer.Program.Cotton.CottonEco",
"LaundryCare.Washer.Program.Cotton.Eco4060",
"LaundryCare.Washer.Program.Cotton.Colour",
"LaundryCare.Washer.Program.EasyCare",
"LaundryCare.Washer.Program.Mix",
"LaundryCare.Washer.Program.Mix.NightWash",
"LaundryCare.Washer.Program.DelicatesSilk",
"LaundryCare.Washer.Program.Wool",
"LaundryCare.Washer.Program.Sensitive",
"LaundryCare.Washer.Program.Auto30",
"LaundryCare.Washer.Program.Auto40",
"LaundryCare.Washer.Program.Auto60",
"LaundryCare.Washer.Program.Chiffon",
"LaundryCare.Washer.Program.Curtains",
"LaundryCare.Washer.Program.DarkWash",
"LaundryCare.Washer.Program.Dessous",
"LaundryCare.Washer.Program.Monsoon",
"LaundryCare.Washer.Program.Outdoor",
"LaundryCare.Washer.Program.PlushToy",
"LaundryCare.Washer.Program.ShirtsBlouses",
"LaundryCare.Washer.Program.SportFitness",
"LaundryCare.Washer.Program.Towels",
"LaundryCare.Washer.Program.WaterProof",
"LaundryCare.Washer.Program.PowerSpeed59",
"LaundryCare.Washer.Program.Super153045.Super15",
"LaundryCare.Washer.Program.Super153045.Super1530",
"LaundryCare.Washer.Program.DownDuvet.Duvet",
"LaundryCare.Washer.Program.Rinse.RinseSpinDrain",
"LaundryCare.Washer.Program.DrumClean",
"LaundryCare.WasherDryer.Program.Cotton",
"LaundryCare.WasherDryer.Program.Cotton.Eco4060",
"LaundryCare.WasherDryer.Program.Mix",
"LaundryCare.WasherDryer.Program.EasyCare",
"LaundryCare.WasherDryer.Program.WashAndDry60",
"LaundryCare.WasherDryer.Program.WashAndDry90",
)
}
PROGRAMS_TRANSLATION_KEYS_MAP = {
value: key for key, value in TRANSLATION_KEYS_PROGRAMS_MAP.items()
}
PROGRAM_SELECT_ENTITY_DESCRIPTIONS = (
SelectEntityDescription(
key=BSH_ACTIVE_PROGRAM,
translation_key="active_program",
),
SelectEntityDescription(
key=BSH_SELECTED_PROGRAM,
translation_key="selected_program",
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: HomeConnectConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Home Connect select entities."""
def get_entities() -> list[HomeConnectProgramSelectEntity]:
"""Get a list of entities."""
entities: list[HomeConnectProgramSelectEntity] = []
programs_not_found = set()
for device in entry.runtime_data.devices:
if device.appliance.type in APPLIANCES_WITH_PROGRAMS:
with contextlib.suppress(HomeConnectError):
programs = device.appliance.get_programs_available()
if programs:
for program in programs:
if program not in PROGRAMS_TRANSLATION_KEYS_MAP:
programs.remove(program)
if program not in programs_not_found:
_LOGGER.info(
'The program "%s" is not part of the official Home Connect API specification',
program,
)
programs_not_found.add(program)
entities.extend(
HomeConnectProgramSelectEntity(device, programs, desc)
for desc in PROGRAM_SELECT_ENTITY_DESCRIPTIONS
)
return entities
async_add_entities(await hass.async_add_executor_job(get_entities), True)
class HomeConnectProgramSelectEntity(HomeConnectEntity, SelectEntity):
"""Select class for Home Connect programs."""
def __init__(
self,
device: HomeConnectDevice,
programs: list[str],
desc: SelectEntityDescription,
) -> None:
"""Initialize the entity."""
super().__init__(
device,
desc,
)
self._attr_options = [
PROGRAMS_TRANSLATION_KEYS_MAP[program] for program in programs
]
self.start_on_select = desc.key == BSH_ACTIVE_PROGRAM
async def async_update(self) -> None:
"""Update the program selection status."""
program = self.device.appliance.status.get(self.bsh_key, {}).get(ATTR_VALUE)
if not program:
program_translation_key = None
elif not (
program_translation_key := PROGRAMS_TRANSLATION_KEYS_MAP.get(program)
):
_LOGGER.debug(
'The program "%s" is not part of the official Home Connect API specification',
program,
)
self._attr_current_option = program_translation_key
_LOGGER.debug("Updated, new program: %s", self._attr_current_option)
async def async_select_option(self, option: str) -> None:
"""Select new program."""
bsh_key = TRANSLATION_KEYS_PROGRAMS_MAP[option]
_LOGGER.debug(
"Starting program: %s" if self.start_on_select else "Selecting program: %s",
bsh_key,
)
if self.start_on_select:
target = self.device.appliance.start_program
else:
target = self.device.appliance.select_program
try:
await self.hass.async_add_executor_job(target, bsh_key)
except HomeConnectError as err:
if self.start_on_select:
translation_key = "start_program"
else:
translation_key = "select_program"
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key=translation_key,
translation_placeholders={
**get_dict_from_home_connect_error(err),
"program": bsh_key,
},
) from err
self.async_entity_update()

View File

@@ -46,6 +46,9 @@
"turn_off": {
"message": "Error while trying to turn off {entity_id} ({setting_key}): {description}"
},
"select_program": {
"message": "Error while trying to select program {program}: {description}"
},
"start_program": {
"message": "Error while trying to start program {program}: {description}"
},
@@ -267,6 +270,326 @@
"name": "Wine compartment 3 temperature"
}
},
"select": {
"selected_program": {
"name": "Selected program",
"state": {
"consumer_products_cleaning_robot_program_cleaning_clean_all": "Clean all",
"consumer_products_cleaning_robot_program_cleaning_clean_map": "Clean map",
"consumer_products_cleaning_robot_program_basic_go_home": "Go home",
"consumer_products_coffee_maker_program_beverage_ristretto": "Ristretto",
"consumer_products_coffee_maker_program_beverage_espresso": "Espresso",
"consumer_products_coffee_maker_program_beverage_espresso_doppio": "Espresso doppio",
"consumer_products_coffee_maker_program_beverage_coffee": "Coffee",
"consumer_products_coffee_maker_program_beverage_x_l_coffee": "XL coffee",
"consumer_products_coffee_maker_program_beverage_caffe_grande": "Caffe grande",
"consumer_products_coffee_maker_program_beverage_espresso_macchiato": "Espresso macchiato",
"consumer_products_coffee_maker_program_beverage_cappuccino": "Cappuccino",
"consumer_products_coffee_maker_program_beverage_latte_macchiato": "Latte macchiato",
"consumer_products_coffee_maker_program_beverage_caffe_latte": "Caffe latte",
"consumer_products_coffee_maker_program_beverage_milk_froth": "Milk froth",
"consumer_products_coffee_maker_program_beverage_warm_milk": "Warm milk",
"consumer_products_coffee_maker_program_coffee_world_kleiner_brauner": "Kleiner brauner",
"consumer_products_coffee_maker_program_coffee_world_grosser_brauner": "Grosser brauner",
"consumer_products_coffee_maker_program_coffee_world_verlaengerter": "Verlaengerter",
"consumer_products_coffee_maker_program_coffee_world_verlaengerter_braun": "Verlaengerter braun",
"consumer_products_coffee_maker_program_coffee_world_wiener_melange": "Wiener melange",
"consumer_products_coffee_maker_program_coffee_world_flat_white": "Flat white",
"consumer_products_coffee_maker_program_coffee_world_cortado": "Cortado",
"consumer_products_coffee_maker_program_coffee_world_cafe_cortado": "Cafe cortado",
"consumer_products_coffee_maker_program_coffee_world_cafe_con_leche": "Cafe con leche",
"consumer_products_coffee_maker_program_coffee_world_cafe_au_lait": "Cafe au lait",
"consumer_products_coffee_maker_program_coffee_world_doppio": "Doppio",
"consumer_products_coffee_maker_program_coffee_world_kaapi": "Kaapi",
"consumer_products_coffee_maker_program_coffee_world_koffie_verkeerd": "Koffie verkeerd",
"consumer_products_coffee_maker_program_coffee_world_galao": "Galao",
"consumer_products_coffee_maker_program_coffee_world_garoto": "Garoto",
"consumer_products_coffee_maker_program_coffee_world_americano": "Americano",
"consumer_products_coffee_maker_program_coffee_world_red_eye": "Red eye",
"consumer_products_coffee_maker_program_coffee_world_black_eye": "Black eye",
"consumer_products_coffee_maker_program_coffee_world_dead_eye": "Dead eye",
"consumer_products_coffee_maker_program_beverage_hot_water": "Hot water",
"dishcare_dishwasher_program_pre_rinse": "Pre_rinse",
"dishcare_dishwasher_program_auto_1": "Auto 1",
"dishcare_dishwasher_program_auto_2": "Auto 2",
"dishcare_dishwasher_program_auto_3": "Auto 3",
"dishcare_dishwasher_program_eco_50": "Eco 50ºC",
"dishcare_dishwasher_program_quick_45": "Quick 45ºC",
"dishcare_dishwasher_program_intensiv_70": "Intensive 70ºC",
"dishcare_dishwasher_program_normal_65": "Normal 65ºC",
"dishcare_dishwasher_program_glas_40": "Glass 40ºC",
"dishcare_dishwasher_program_glass_care": "Glass care",
"dishcare_dishwasher_program_night_wash": "Night wash",
"dishcare_dishwasher_program_quick_65": "Quick 65ºC",
"dishcare_dishwasher_program_normal_45": "Normal 45ºC",
"dishcare_dishwasher_program_intensiv_45": "Intensive 45ºC",
"dishcare_dishwasher_program_auto_half_load": "Auto half load",
"dishcare_dishwasher_program_intensiv_power": "Intensive power",
"dishcare_dishwasher_program_magic_daily": "Magic daily",
"dishcare_dishwasher_program_super_60": "Super 60ºC",
"dishcare_dishwasher_program_kurz_60": "Kurz 60ºC",
"dishcare_dishwasher_program_express_sparkle_65": "Express sparkle 65ºC",
"dishcare_dishwasher_program_machine_care": "Machine care",
"dishcare_dishwasher_program_steam_fresh": "Steam fresh",
"dishcare_dishwasher_program_maximum_cleaning": "Maximum cleaning",
"dishcare_dishwasher_program_mixed_load": "Mixed load",
"laundry_care_dryer_program_cotton": "Cotton",
"laundry_care_dryer_program_synthetic": "Synthetic",
"laundry_care_dryer_program_mix": "Mix",
"laundry_care_dryer_program_blankets": "Blankets",
"laundry_care_dryer_program_business_shirts": "Business shirts",
"laundry_care_dryer_program_down_feathers": "Down feathers",
"laundry_care_dryer_program_hygiene": "Hygiene",
"laundry_care_dryer_program_jeans": "Jeans",
"laundry_care_dryer_program_outdoor": "Outdoor",
"laundry_care_dryer_program_synthetic_refresh": "Synthetic refresh",
"laundry_care_dryer_program_towels": "Towels",
"laundry_care_dryer_program_delicates": "Delicates",
"laundry_care_dryer_program_super_40": "Super 40ºC",
"laundry_care_dryer_program_shirts_15": "Shirts 15ºC",
"laundry_care_dryer_program_pillow": "Pillow",
"laundry_care_dryer_program_anti_shrink": "Anti shrink",
"laundry_care_dryer_program_my_time_my_drying_time": "My drying time",
"laundry_care_dryer_program_time_cold": "Cold (variable time)",
"laundry_care_dryer_program_time_warm": "Warm (variable time)",
"laundry_care_dryer_program_in_basket": "In basket",
"laundry_care_dryer_program_time_cold_fix_time_cold_20": "Cold (20 min)",
"laundry_care_dryer_program_time_cold_fix_time_cold_30": "Cold (30 min)",
"laundry_care_dryer_program_time_cold_fix_time_cold_60": "Cold (60 min)",
"laundry_care_dryer_program_time_warm_fix_time_warm_30": "Warm (30 min)",
"laundry_care_dryer_program_time_warm_fix_time_warm_40": "Warm (40 min)",
"laundry_care_dryer_program_time_warm_fix_time_warm_60": "Warm (60 min)",
"laundry_care_dryer_program_dessous": "Dessous",
"cooking_common_program_hood_automatic": "Automatic",
"cooking_common_program_hood_venting": "Venting",
"cooking_common_program_hood_delayed_shut_off": "Delayed shut off",
"cooking_oven_program_heating_mode_pre_heating": "Pre-heating",
"cooking_oven_program_heating_mode_hot_air": "Hot air",
"cooking_oven_program_heating_mode_hot_air_eco": "Hot air eco",
"cooking_oven_program_heating_mode_hot_air_grilling": "Hot air grilling",
"cooking_oven_program_heating_mode_top_bottom_heating": "Top bottom heating",
"cooking_oven_program_heating_mode_top_bottom_heating_eco": "Top bottom heating eco",
"cooking_oven_program_heating_mode_bottom_heating": "Bottom heating",
"cooking_oven_program_heating_mode_pizza_setting": "Pizza setting",
"cooking_oven_program_heating_mode_slow_cook": "Slow cook",
"cooking_oven_program_heating_mode_intensive_heat": "Intensive heat",
"cooking_oven_program_heating_mode_keep_warm": "Keep warm",
"cooking_oven_program_heating_mode_preheat_ovenware": "Preheat ovenware",
"cooking_oven_program_heating_mode_frozen_heatup_special": "Special Heat-Up for frozen products",
"cooking_oven_program_heating_mode_desiccation": "Desiccation",
"cooking_oven_program_heating_mode_defrost": "Defrost",
"cooking_oven_program_heating_mode_proof": "Proof",
"cooking_oven_program_heating_mode_hot_air_30_steam": "Hot air + 30 RH",
"cooking_oven_program_heating_mode_hot_air_60_steam": "Hot air + 60 RH",
"cooking_oven_program_heating_mode_hot_air_80_steam": "Hot air + 80 RH",
"cooking_oven_program_heating_mode_hot_air_100_steam": "Hot air + 100 RH",
"cooking_oven_program_heating_mode_sabbath_programme": "Sabbath programme",
"cooking_oven_program_microwave_90_watt": "90 Watt",
"cooking_oven_program_microwave_180_watt": "180 Watt",
"cooking_oven_program_microwave_360_watt": "360 Watt",
"cooking_oven_program_microwave_600_watt": "600 Watt",
"cooking_oven_program_microwave_900_watt": "900 Watt",
"cooking_oven_program_microwave_1000_watt": "1000 Watt",
"cooking_oven_program_microwave_max": "Max",
"cooking_oven_program_heating_mode_warming_drawer": "Warming drawer",
"laundry_care_washer_program_cotton": "Cotton",
"laundry_care_washer_program_cotton_cotton_eco": "Cotton eco",
"laundry_care_washer_program_cotton_eco_4060": "Cotton eco 40/60ºC",
"laundry_care_washer_program_cotton_colour": "Cotton color",
"laundry_care_washer_program_easy_care": "Easy care",
"laundry_care_washer_program_mix": "Mix",
"laundry_care_washer_program_mix_night_wash": "Mix night wash",
"laundry_care_washer_program_delicates_silk": "Delicates silk",
"laundry_care_washer_program_wool": "Wool",
"laundry_care_washer_program_sensitive": "Sensitive",
"laundry_care_washer_program_auto_30": "Auto 30ºC",
"laundry_care_washer_program_auto_40": "Auto 40ºC",
"laundry_care_washer_program_auto_60": "Auto 60ºC",
"laundry_care_washer_program_chiffon": "Chiffon",
"laundry_care_washer_program_curtains": "Curtains",
"laundry_care_washer_program_dark_wash": "Dark wash",
"laundry_care_washer_program_dessous": "Dessous",
"laundry_care_washer_program_monsoon": "Monsoon",
"laundry_care_washer_program_outdoor": "Outdoor",
"laundry_care_washer_program_plush_toy": "Plush toy",
"laundry_care_washer_program_shirts_blouses": "Shirts blouses",
"laundry_care_washer_program_sport_fitness": "Sport fitness",
"laundry_care_washer_program_towels": "Towels",
"laundry_care_washer_program_water_proof": "Water proof",
"laundry_care_washer_program_power_speed_59": "Power speed <60 min",
"laundry_care_washer_program_super_153045_super_15": "Super 15 min",
"laundry_care_washer_program_super_153045_super_1530": "Super 15/30 min",
"laundry_care_washer_program_down_duvet_duvet": "Down duvet",
"laundry_care_washer_program_rinse_rinse_spin_drain": "Rinse spin drain",
"laundry_care_washer_program_drum_clean": "Drum clean",
"laundry_care_washer_dryer_program_cotton": "Cotton",
"laundry_care_washer_dryer_program_cotton_eco_4060": "Cotton eco 40/60 ºC",
"laundry_care_washer_dryer_program_mix": "Mix",
"laundry_care_washer_dryer_program_easy_care": "Easy care",
"laundry_care_washer_dryer_program_wash_and_dry_60": "Wash and dry (60 min)",
"laundry_care_washer_dryer_program_wash_and_dry_90": "Wash and dry (90 min)"
}
},
"active_program": {
"name": "Active program",
"state": {
"consumer_products_cleaning_robot_program_cleaning_clean_all": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_cleaning_robot_program_cleaning_clean_all%]",
"consumer_products_cleaning_robot_program_cleaning_clean_map": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_cleaning_robot_program_cleaning_clean_map%]",
"consumer_products_cleaning_robot_program_basic_go_home": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_cleaning_robot_program_basic_go_home%]",
"consumer_products_coffee_maker_program_beverage_ristretto": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_ristretto%]",
"consumer_products_coffee_maker_program_beverage_espresso": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_espresso%]",
"consumer_products_coffee_maker_program_beverage_espresso_doppio": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_espresso_doppio%]",
"consumer_products_coffee_maker_program_beverage_coffee": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_coffee%]",
"consumer_products_coffee_maker_program_beverage_x_l_coffee": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_x_l_coffee%]",
"consumer_products_coffee_maker_program_beverage_caffe_grande": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_caffe_grande%]",
"consumer_products_coffee_maker_program_beverage_espresso_macchiato": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_espresso_macchiato%]",
"consumer_products_coffee_maker_program_beverage_cappuccino": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_cappuccino%]",
"consumer_products_coffee_maker_program_beverage_latte_macchiato": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_latte_macchiato%]",
"consumer_products_coffee_maker_program_beverage_caffe_latte": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_caffe_latte%]",
"consumer_products_coffee_maker_program_beverage_milk_froth": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_milk_froth%]",
"consumer_products_coffee_maker_program_beverage_warm_milk": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_warm_milk%]",
"consumer_products_coffee_maker_program_coffee_world_kleiner_brauner": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_kleiner_brauner%]",
"consumer_products_coffee_maker_program_coffee_world_grosser_brauner": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_grosser_brauner%]",
"consumer_products_coffee_maker_program_coffee_world_verlaengerter": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_verlaengerter%]",
"consumer_products_coffee_maker_program_coffee_world_verlaengerter_braun": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_verlaengerter_braun%]",
"consumer_products_coffee_maker_program_coffee_world_wiener_melange": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_wiener_melange%]",
"consumer_products_coffee_maker_program_coffee_world_flat_white": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_flat_white%]",
"consumer_products_coffee_maker_program_coffee_world_cortado": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_cortado%]",
"consumer_products_coffee_maker_program_coffee_world_cafe_cortado": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_cafe_cortado%]",
"consumer_products_coffee_maker_program_coffee_world_cafe_con_leche": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_cafe_con_leche%]",
"consumer_products_coffee_maker_program_coffee_world_cafe_au_lait": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_cafe_au_lait%]",
"consumer_products_coffee_maker_program_coffee_world_doppio": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_doppio%]",
"consumer_products_coffee_maker_program_coffee_world_kaapi": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_kaapi%]",
"consumer_products_coffee_maker_program_coffee_world_koffie_verkeerd": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_koffie_verkeerd%]",
"consumer_products_coffee_maker_program_coffee_world_galao": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_galao%]",
"consumer_products_coffee_maker_program_coffee_world_garoto": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_garoto%]",
"consumer_products_coffee_maker_program_coffee_world_americano": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_americano%]",
"consumer_products_coffee_maker_program_coffee_world_red_eye": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_red_eye%]",
"consumer_products_coffee_maker_program_coffee_world_black_eye": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_black_eye%]",
"consumer_products_coffee_maker_program_coffee_world_dead_eye": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_dead_eye%]",
"consumer_products_coffee_maker_program_beverage_hot_water": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_hot_water%]",
"dishcare_dishwasher_program_pre_rinse": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_pre_rinse%]",
"dishcare_dishwasher_program_auto_1": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_auto_1%]",
"dishcare_dishwasher_program_auto_2": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_auto_2%]",
"dishcare_dishwasher_program_auto_3": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_auto_3%]",
"dishcare_dishwasher_program_eco_50": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_eco_50%]",
"dishcare_dishwasher_program_quick_45": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_quick_45%]",
"dishcare_dishwasher_program_intensiv_70": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_intensiv_70%]",
"dishcare_dishwasher_program_normal_65": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_normal_65%]",
"dishcare_dishwasher_program_glas_40": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_glas_40%]",
"dishcare_dishwasher_program_glass_care": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_glass_care%]",
"dishcare_dishwasher_program_night_wash": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_night_wash%]",
"dishcare_dishwasher_program_quick_65": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_quick_65%]",
"dishcare_dishwasher_program_normal_45": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_normal_45%]",
"dishcare_dishwasher_program_intensiv_45": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_intensiv_45%]",
"dishcare_dishwasher_program_auto_half_load": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_auto_half_load%]",
"dishcare_dishwasher_program_intensiv_power": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_intensiv_power%]",
"dishcare_dishwasher_program_magic_daily": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_magic_daily%]",
"dishcare_dishwasher_program_super_60": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_super_60%]",
"dishcare_dishwasher_program_kurz_60": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_kurz_60%]",
"dishcare_dishwasher_program_express_sparkle_65": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_express_sparkle_65%]",
"dishcare_dishwasher_program_machine_care": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_machine_care%]",
"dishcare_dishwasher_program_steam_fresh": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_steam_fresh%]",
"dishcare_dishwasher_program_maximum_cleaning": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_maximum_cleaning%]",
"dishcare_dishwasher_program_mixed_load": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_mixed_load%]",
"laundry_care_dryer_program_cotton": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_cotton%]",
"laundry_care_dryer_program_synthetic": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_synthetic%]",
"laundry_care_dryer_program_mix": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_mix%]",
"laundry_care_dryer_program_blankets": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_blankets%]",
"laundry_care_dryer_program_business_shirts": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_business_shirts%]",
"laundry_care_dryer_program_down_feathers": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_down_feathers%]",
"laundry_care_dryer_program_hygiene": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_hygiene%]",
"laundry_care_dryer_program_jeans": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_jeans%]",
"laundry_care_dryer_program_outdoor": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_outdoor%]",
"laundry_care_dryer_program_synthetic_refresh": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_synthetic_refresh%]",
"laundry_care_dryer_program_towels": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_towels%]",
"laundry_care_dryer_program_delicates": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_delicates%]",
"laundry_care_dryer_program_super_40": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_super_40%]",
"laundry_care_dryer_program_shirts_15": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_shirts_15%]",
"laundry_care_dryer_program_pillow": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_pillow%]",
"laundry_care_dryer_program_anti_shrink": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_anti_shrink%]",
"laundry_care_dryer_program_my_time_my_drying_time": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_my_time_my_drying_time%]",
"laundry_care_dryer_program_time_cold": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_time_cold%]",
"laundry_care_dryer_program_time_warm": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_time_warm%]",
"laundry_care_dryer_program_in_basket": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_in_basket%]",
"laundry_care_dryer_program_time_cold_fix_time_cold_20": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_time_cold_fix_time_cold_20%]",
"laundry_care_dryer_program_time_cold_fix_time_cold_30": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_time_cold_fix_time_cold_30%]",
"laundry_care_dryer_program_time_cold_fix_time_cold_60": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_time_cold_fix_time_cold_60%]",
"laundry_care_dryer_program_time_warm_fix_time_warm_30": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_time_warm_fix_time_warm_30%]",
"laundry_care_dryer_program_time_warm_fix_time_warm_40": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_time_warm_fix_time_warm_40%]",
"laundry_care_dryer_program_time_warm_fix_time_warm_60": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_time_warm_fix_time_warm_60%]",
"laundry_care_dryer_program_dessous": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_dessous%]",
"cooking_common_program_hood_automatic": "[%key:component::home_connect::entity::select::selected_program::state::cooking_common_program_hood_automatic%]",
"cooking_common_program_hood_venting": "[%key:component::home_connect::entity::select::selected_program::state::cooking_common_program_hood_venting%]",
"cooking_common_program_hood_delayed_shut_off": "[%key:component::home_connect::entity::select::selected_program::state::cooking_common_program_hood_delayed_shut_off%]",
"cooking_oven_program_heating_mode_pre_heating": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_pre_heating%]",
"cooking_oven_program_heating_mode_hot_air": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_hot_air%]",
"cooking_oven_program_heating_mode_hot_air_eco": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_hot_air_eco%]",
"cooking_oven_program_heating_mode_hot_air_grilling": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_hot_air_grilling%]",
"cooking_oven_program_heating_mode_top_bottom_heating": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_top_bottom_heating%]",
"cooking_oven_program_heating_mode_top_bottom_heating_eco": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_top_bottom_heating_eco%]",
"cooking_oven_program_heating_mode_bottom_heating": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_bottom_heating%]",
"cooking_oven_program_heating_mode_pizza_setting": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_pizza_setting%]",
"cooking_oven_program_heating_mode_slow_cook": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_slow_cook%]",
"cooking_oven_program_heating_mode_intensive_heat": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_intensive_heat%]",
"cooking_oven_program_heating_mode_keep_warm": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_keep_warm%]",
"cooking_oven_program_heating_mode_preheat_ovenware": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_preheat_ovenware%]",
"cooking_oven_program_heating_mode_frozen_heatup_special": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_frozen_heatup_special%]",
"cooking_oven_program_heating_mode_desiccation": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_desiccation%]",
"cooking_oven_program_heating_mode_defrost": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_defrost%]",
"cooking_oven_program_heating_mode_proof": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_proof%]",
"cooking_oven_program_heating_mode_hot_air_30_steam": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_hot_air_30_steam%]",
"cooking_oven_program_heating_mode_hot_air_60_steam": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_hot_air_60_steam%]",
"cooking_oven_program_heating_mode_hot_air_80_steam": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_hot_air_80_steam%]",
"cooking_oven_program_heating_mode_hot_air_100_steam": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_hot_air_100_steam%]",
"cooking_oven_program_heating_mode_sabbath_programme": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_sabbath_programme%]",
"cooking_oven_program_microwave_90_watt": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_microwave_90_watt%]",
"cooking_oven_program_microwave_180_watt": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_microwave_180_watt%]",
"cooking_oven_program_microwave_360_watt": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_microwave_360_watt%]",
"cooking_oven_program_microwave_600_watt": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_microwave_600_watt%]",
"cooking_oven_program_microwave_900_watt": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_microwave_900_watt%]",
"cooking_oven_program_microwave_1000_watt": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_microwave_1000_watt%]",
"cooking_oven_program_microwave_max": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_microwave_max%]",
"cooking_oven_program_heating_mode_warming_drawer": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_warming_drawer%]",
"laundry_care_washer_program_cotton": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_cotton%]",
"laundry_care_washer_program_cotton_cotton_eco": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_cotton_cotton_eco%]",
"laundry_care_washer_program_cotton_eco_4060": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_cotton_eco_4060%]",
"laundry_care_washer_program_cotton_colour": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_cotton_colour%]",
"laundry_care_washer_program_easy_care": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_easy_care%]",
"laundry_care_washer_program_mix": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_mix%]",
"laundry_care_washer_program_mix_night_wash": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_mix_night_wash%]",
"laundry_care_washer_program_delicates_silk": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_delicates_silk%]",
"laundry_care_washer_program_wool": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_wool%]",
"laundry_care_washer_program_sensitive": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_sensitive%]",
"laundry_care_washer_program_auto_30": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_auto_30%]",
"laundry_care_washer_program_auto_40": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_auto_40%]",
"laundry_care_washer_program_auto_60": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_auto_60%]",
"laundry_care_washer_program_chiffon": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_chiffon%]",
"laundry_care_washer_program_curtains": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_curtains%]",
"laundry_care_washer_program_dark_wash": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_dark_wash%]",
"laundry_care_washer_program_dessous": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_dessous%]",
"laundry_care_washer_program_monsoon": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_monsoon%]",
"laundry_care_washer_program_outdoor": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_outdoor%]",
"laundry_care_washer_program_plush_toy": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_plush_toy%]",
"laundry_care_washer_program_shirts_blouses": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_shirts_blouses%]",
"laundry_care_washer_program_sport_fitness": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_sport_fitness%]",
"laundry_care_washer_program_towels": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_towels%]",
"laundry_care_washer_program_water_proof": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_water_proof%]",
"laundry_care_washer_program_power_speed_59": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_power_speed_59%]",
"laundry_care_washer_program_super_153045_super_15": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_super_153045_super_15%]",
"laundry_care_washer_program_super_153045_super_1530": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_super_153045_super_1530%]",
"laundry_care_washer_program_down_duvet_duvet": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_down_duvet_duvet%]",
"laundry_care_washer_program_rinse_rinse_spin_drain": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_rinse_rinse_spin_drain%]",
"laundry_care_washer_program_drum_clean": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_drum_clean%]",
"laundry_care_washer_dryer_program_cotton": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_dryer_program_cotton%]",
"laundry_care_washer_dryer_program_cotton_eco_4060": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_dryer_program_cotton_eco_4060%]",
"laundry_care_washer_dryer_program_mix": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_dryer_program_mix%]",
"laundry_care_washer_dryer_program_easy_care": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_dryer_program_easy_care%]",
"laundry_care_washer_dryer_program_wash_and_dry_60": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_dryer_program_wash_and_dry_60%]",
"laundry_care_washer_dryer_program_wash_and_dry_90": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_dryer_program_wash_and_dry_90%]"
}
}
},
"sensor": {
"program_progress": {
"name": "Program progress"

View File

@@ -13,6 +13,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import HomeConnectConfigEntry, get_dict_from_home_connect_error
from .const import (
APPLIANCES_WITH_PROGRAMS,
ATTR_ALLOWED_VALUES,
ATTR_CONSTRAINTS,
ATTR_VALUE,
@@ -36,18 +37,6 @@ from .entity import HomeConnectDevice, HomeConnectEntity
_LOGGER = logging.getLogger(__name__)
APPLIANCES_WITH_PROGRAMS = (
"CleaningRobot",
"CoffeeMaker",
"Dishwasher",
"Dryer",
"Hood",
"Oven",
"WarmingDrawer",
"Washer",
"WasherDryer",
)
SWITCHES = (
SwitchEntityDescription(

View File

@@ -114,7 +114,7 @@ _LOGGER = logging.getLogger(__name__)
NUMBERS_ONLY_RE = re.compile(r"[^\d.]+")
VERSION_RE = re.compile(r"([0-9]+)(\.[0-9]+)?(\.[0-9]+)?")
INVALID_END_CHARS = "-_"
INVALID_END_CHARS = "-_ "
MAX_VERSION_PART = 2**32 - 1
@@ -424,20 +424,12 @@ def cleanup_name_for_homekit(name: str | None) -> str:
def temperature_to_homekit(temperature: float, unit: str) -> float:
"""Convert temperature to Celsius for HomeKit."""
return round(
TemperatureConverter.convert(temperature, unit, UnitOfTemperature.CELSIUS), 1
)
return TemperatureConverter.convert(temperature, unit, UnitOfTemperature.CELSIUS)
def temperature_to_states(temperature: float, unit: str) -> float:
"""Convert temperature back from Celsius to Home Assistant unit."""
return (
round(
TemperatureConverter.convert(temperature, UnitOfTemperature.CELSIUS, unit)
* 2
)
/ 2
)
return TemperatureConverter.convert(temperature, UnitOfTemperature.CELSIUS, unit)
def density_to_air_quality(density: float) -> int:

View File

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

View File

@@ -9,13 +9,15 @@ from typing import Any, NamedTuple
from homewizard_energy import HomeWizardEnergyV1
from homewizard_energy.errors import DisabledError, RequestError, UnsupportedError
from homewizard_energy.v1.models import Device
from voluptuous import Required, Schema
import voluptuous as vol
from homeassistant.components import onboarding, zeroconf
from homeassistant.components.dhcp import DhcpServiceInfo
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_IP_ADDRESS, CONF_PATH
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.selector import TextSelector
from .const import (
CONF_API_ENABLED,
@@ -68,11 +70,11 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
user_input = user_input or {}
return self.async_show_form(
step_id="user",
data_schema=Schema(
data_schema=vol.Schema(
{
Required(
vol.Required(
CONF_IP_ADDRESS, default=user_input.get(CONF_IP_ADDRESS)
): str,
): TextSelector(),
}
),
errors=errors,
@@ -110,6 +112,32 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
return await self.async_step_discovery_confirm()
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle dhcp discovery to update existing entries.
This flow is triggered only by DHCP discovery of known devices.
"""
try:
device = await self._async_try_connect(discovery_info.ip)
except RecoverableError as ex:
_LOGGER.error(ex)
return self.async_abort(reason="unknown")
await self.async_set_unique_id(
f"{device.product_type}_{discovery_info.macaddress}"
)
self._abort_if_unique_id_configured(
updates={CONF_IP_ADDRESS: discovery_info.ip}
)
# This situation should never happen, as Home Assistant will only
# send updates for existing entries. In case it does, we'll just
# abort the flow with an unknown error.
return self.async_abort(reason="unknown")
async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -170,6 +198,43 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form(step_id="reauth_confirm", errors=errors)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of the integration."""
errors: dict[str, str] = {}
if user_input:
try:
device_info = await self._async_try_connect(user_input[CONF_IP_ADDRESS])
except RecoverableError as ex:
_LOGGER.error(ex)
errors = {"base": ex.error_code}
else:
await self.async_set_unique_id(
f"{device_info.product_type}_{device_info.serial}"
)
self._abort_if_unique_id_mismatch(reason="wrong_device")
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(),
data_updates=user_input,
)
reconfigure_entry = self._get_reconfigure_entry()
return self.async_show_form(
step_id="reconfigure",
data_schema=vol.Schema(
{
vol.Required(
CONF_IP_ADDRESS,
default=reconfigure_entry.data.get(CONF_IP_ADDRESS),
): TextSelector(),
}
),
description_placeholders={
"title": reconfigure_entry.title,
},
errors=errors,
)
@staticmethod
async def _async_try_connect(ip_address: str) -> Device:
"""Try to connect.

View File

@@ -3,9 +3,15 @@
"name": "HomeWizard Energy",
"codeowners": ["@DCSBL"],
"config_flow": true,
"dhcp": [
{
"registered_devices": true
}
],
"documentation": "https://www.home-assistant.io/integrations/homewizard",
"iot_class": "local_polling",
"loggers": ["homewizard_energy"],
"quality_scale": "platinum",
"requirements": ["python-homewizard-energy==v7.0.0"],
"zeroconf": ["_hwenergy._tcp.local."]
}

View File

@@ -46,11 +46,7 @@ rules:
# Gold
devices: done
diagnostics: done
discovery-update-info:
status: todo
comment: |
The integration doesn't update the device info based on DHCP discovery
of known existing devices.
discovery-update-info: done
discovery: done
docs-data-update: done
docs-examples: done
@@ -69,7 +65,7 @@ rules:
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow: todo
reconfiguration-flow: done
repair-issues:
status: exempt
comment: |

View File

@@ -17,6 +17,15 @@
},
"reauth_confirm": {
"description": "The local API is disabled. Go to the HomeWizard Energy app and enable the API in the device settings."
},
"reconfigure": {
"description": "Update configuration for {title}.",
"data": {
"ip_address": "[%key:common::config_flow::data::ip%]"
},
"data_description": {
"ip_address": "[%key:component::homewizard::config::step::user::data_description::ip_address%]"
}
}
},
"error": {
@@ -29,7 +38,9 @@
"device_not_supported": "This device is not supported",
"unknown_error": "[%key:common::config_flow::error::unknown%]",
"unsupported_api_version": "Detected unsupported API version",
"reauth_successful": "Enabling API was successful"
"reauth_successful": "Enabling API was successful",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"wrong_device": "The configured device is not the same found on this IP address."
}
},
"entity": {

View File

@@ -326,7 +326,8 @@ class HomeAssistantApplication(web.Application):
protocol,
writer,
task,
loop=self._loop,
# loop will never be None when called from aiohttp
loop=self._loop, # type: ignore[arg-type]
client_max_size=self._client_max_size,
)

View File

@@ -22,6 +22,8 @@ from .entity import (
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class AutomowerButtonEntityDescription(ButtonEntityDescription):

View File

@@ -22,6 +22,10 @@ from .const import DOMAIN
from .coordinator import AutomowerDataUpdateCoordinator
from .entity import AutomowerAvailableEntity, handle_sending_exception
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
DOCKED_ACTIVITIES = (MowerActivities.PARKED_IN_CS, MowerActivities.CHARGING)
MOWING_ACTIVITIES = (
MowerActivities.MOWING,
@@ -42,9 +46,6 @@ PARK = "park"
OVERRIDE_MODES = [MOW, PARK]
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: AutomowerConfigEntry,

View File

@@ -24,6 +24,8 @@ from .entity import (
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
@callback
def _async_get_cutting_height(data: MowerAttributes) -> int:

View File

@@ -16,6 +16,7 @@ from .entity import AutomowerControlEntity, handle_sending_exception
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
HEADLIGHT_MODES: list = [
HeadlightModes.ALWAYS_OFF.lower(),

View File

@@ -349,6 +349,7 @@ MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
key="number_of_collisions",
translation_key="number_of_collisions",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL,
exists_fn=lambda data: data.statistics.number_of_collisions is not None,
value_fn=attrgetter("statistics.number_of_collisions"),

View File

@@ -19,6 +19,8 @@ from .entity import (
handle_sending_exception,
)
PARALLEL_UPDATES = 1
_LOGGER = logging.getLogger(__name__)

View File

@@ -45,6 +45,7 @@ from homeassistant.util import dt as dt_util
from .const import DOMAIN, TIMER_DATA
from .timers import (
CancelAllTimersIntentHandler,
CancelTimerIntentHandler,
DecreaseTimerIntentHandler,
IncreaseTimerIntentHandler,
@@ -130,6 +131,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
intent.async_register(hass, SetPositionIntentHandler())
intent.async_register(hass, StartTimerIntentHandler())
intent.async_register(hass, CancelTimerIntentHandler())
intent.async_register(hass, CancelAllTimersIntentHandler())
intent.async_register(hass, IncreaseTimerIntentHandler())
intent.async_register(hass, DecreaseTimerIntentHandler())
intent.async_register(hass, PauseTimerIntentHandler())

View File

@@ -887,6 +887,36 @@ class CancelTimerIntentHandler(intent.IntentHandler):
return intent_obj.create_response()
class CancelAllTimersIntentHandler(intent.IntentHandler):
"""Intent handler for cancelling all timers."""
intent_type = intent.INTENT_CANCEL_ALL_TIMERS
description = "Cancels all timers"
slot_schema = {
vol.Optional("area"): cv.string,
}
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
"""Handle the intent."""
hass = intent_obj.hass
timer_manager: TimerManager = hass.data[TIMER_DATA]
slots = self.async_validate_slots(intent_obj.slots)
canceled = 0
for timer in _find_timers(hass, intent_obj.device_id, slots):
timer_manager.cancel_timer(timer.id)
canceled += 1
response = intent_obj.create_response()
speech_slots = {"canceled": canceled}
if "area" in slots:
speech_slots["area"] = slots["area"]["value"]
response.async_set_speech_slots(speech_slots)
return response
class IncreaseTimerIntentHandler(intent.IntentHandler):
"""Intent handler for increasing the time of a timer."""

View File

@@ -5,10 +5,13 @@
"description": "[%key:component::bluetooth::config::step::user::description%]",
"data": {
"address": "[%key:common::config_flow::data::device%]"
},
"data_description": {
"address": "Ensure your device is powered on and within Bluetooth range before continuing"
}
},
"bluetooth_confirm": {
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
"description": "Do you want to set up {name}?\n\n*Ensure your device is powered on and within Bluetooth range before continuing*"
}
},
"abort": {

View File

@@ -54,6 +54,7 @@ from .const import (
CONF_KNX_SECURE_USER_PASSWORD,
CONF_KNX_STATE_UPDATER,
CONF_KNX_TELEGRAM_LOG_SIZE,
CONF_KNX_TUNNEL_ENDPOINT_IA,
CONF_KNX_TUNNELING,
CONF_KNX_TUNNELING_TCP,
CONF_KNX_TUNNELING_TCP_SECURE,
@@ -352,6 +353,7 @@ class KNXModule:
if _conn_type == CONF_KNX_TUNNELING_TCP:
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING_TCP,
individual_address=self.entry.data.get(CONF_KNX_TUNNEL_ENDPOINT_IA),
gateway_ip=self.entry.data[CONF_HOST],
gateway_port=self.entry.data[CONF_PORT],
auto_reconnect=True,
@@ -364,6 +366,7 @@ class KNXModule:
if _conn_type == CONF_KNX_TUNNELING_TCP_SECURE:
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING_TCP_SECURE,
individual_address=self.entry.data.get(CONF_KNX_TUNNEL_ENDPOINT_IA),
gateway_ip=self.entry.data[CONF_HOST],
gateway_port=self.entry.data[CONF_PORT],
secure_config=SecureConfig(

View File

@@ -104,7 +104,7 @@ class KNXConfigEntryData(TypedDict, total=False):
route_back: bool # not required
host: str # only required for tunnelling
port: int # only required for tunnelling
tunnel_endpoint_ia: str | None
tunnel_endpoint_ia: str | None # tunnelling only - not required (use get())
# KNX secure
user_id: int | None # not required
user_password: str | None # not required

View File

@@ -291,6 +291,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_ADDRESS: discovery_info.macaddress,
}
)
self._async_abort_entries_match({CONF_ADDRESS: discovery_info.macaddress})
_LOGGER.debug(
"Discovered La Marzocco machine %s through DHCP at address %s",

View File

@@ -36,5 +36,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"loggers": ["pylamarzocco"],
"requirements": ["pylamarzocco==1.2.11"]
"requirements": ["pylamarzocco==1.2.12"]
}

View File

@@ -67,8 +67,10 @@
"step": {
"init": {
"data": {
"title": "Update Configuration",
"use_bluetooth": "Use Bluetooth"
},
"data_description": {
"use_bluetooth": "Should the integration try to use Bluetooth to control the machine?"
}
}
}

View File

@@ -24,4 +24,10 @@ TUBE_LINES = [
"Piccadilly",
"Victoria",
"Waterloo & City",
"Liberty",
"Lioness",
"Mildmay",
"Suffragette",
"Weaver",
"Windrush",
]

View File

@@ -38,6 +38,10 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN):
) -> tuple[dict[str, str], str | None]:
"""Check connection to the Mealie API."""
assert self.host is not None
if "/hassio/ingress/" in self.host:
return {"base": "ingress_url"}, None
client = MealieClient(
self.host,
token=api_token,

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/mealie",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["aiomealie==0.9.3"]
"requirements": ["aiomealie==0.9.4"]
}

View File

@@ -8,7 +8,7 @@
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"host": "The URL of your Mealie instance."
"host": "The URL of your Mealie instance, for example, http://192.168.1.123:1234"
}
},
"reauth_confirm": {
@@ -29,6 +29,7 @@
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"ingress_url": "Ingress URLs are only used for accessing the Mealie UI. Use your Home Assistant IP address and the network port within the configuration tab of the Mealie add-on.",
"unknown": "[%key:common::config_flow::error::unknown%]",
"mealie_version": "Minimum required version is v1.0.0. Please upgrade Mealie and then retry."
},

View File

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

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/met_eireann",
"iot_class": "cloud_polling",
"loggers": ["meteireann"],
"requirements": ["PyMetEireann==2021.8.0"]
"requirements": ["PyMetEireann==2024.11.0"]
}

View File

@@ -158,8 +158,6 @@ async def async_modbus_setup(
async def async_stop_modbus(event: Event) -> None:
"""Stop Modbus service."""
async_dispatcher_send(hass, SIGNAL_STOP_ENTITY)
for client in hub_collect.values():
await client.async_close()

View File

@@ -3,13 +3,14 @@
from dataclasses import dataclass
from datetime import timedelta
import logging
from pprint import pformat
from typing import Any
from monzopy import AuthorisationExpiredError
from monzopy import AuthorisationExpiredError, InvalidMonzoAPIResponseError
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .api import AuthenticatedMonzoAPI
from .const import DOMAIN
@@ -45,5 +46,16 @@ class MonzoCoordinator(DataUpdateCoordinator[MonzoData]):
pots = await self.api.user_account.pots()
except AuthorisationExpiredError as err:
raise ConfigEntryAuthFailed from err
except InvalidMonzoAPIResponseError as err:
message = "Invalid Monzo API response."
if err.missing_key:
_LOGGER.debug(
"%s\nMissing key: %s\nResponse:\n%s",
message,
err.missing_key,
pformat(err.response),
)
message += " Enabling debug logging for details."
raise UpdateFailed(message) from err
return MonzoData(accounts, pots)

View File

@@ -14,5 +14,5 @@
"integration_type": "device",
"iot_class": "assumed_state",
"loggers": ["motionblindsble"],
"requirements": ["motionblindsble==0.1.2"]
"requirements": ["motionblindsble==0.1.3"]
}

View File

@@ -1185,6 +1185,33 @@ def device_info_from_specifications(
return info
@callback
def ensure_via_device_exists(
hass: HomeAssistant, device_info: DeviceInfo | None, config_entry: ConfigEntry
) -> None:
"""Ensure the via device is in the device registry."""
if (
device_info is None
or CONF_VIA_DEVICE not in device_info
or (device_registry := dr.async_get(hass)).async_get_device(
identifiers={device_info["via_device"]}
)
):
return
# Ensure the via device exists in the device registry
_LOGGER.debug(
"Device identifier %s via_device reference from device_info %s "
"not found in the Device Registry, creating new entry",
device_info["via_device"],
device_info,
)
device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={device_info["via_device"]},
)
class MqttEntityDeviceInfo(Entity):
"""Mixin used for mqtt platforms that support the device registry."""
@@ -1203,6 +1230,7 @@ class MqttEntityDeviceInfo(Entity):
device_info = self.device_info
if device_info is not None:
ensure_via_device_exists(self.hass, device_info, self._config_entry)
device_registry.async_get_or_create(
config_entry_id=config_entry_id, **device_info
)
@@ -1256,6 +1284,7 @@ class MqttEntity(
self, hass, discovery_data, self.discovery_update
)
MqttEntityDeviceInfo.__init__(self, config.get(CONF_DEVICE), config_entry)
ensure_via_device_exists(self.hass, self.device_info, self._config_entry)
def _init_entity_id(self) -> None:
"""Set entity_id from object_id if defined in config."""
@@ -1490,6 +1519,8 @@ def update_device(
config_entry_id = config_entry.entry_id
device_info = device_info_from_specifications(config[CONF_DEVICE])
ensure_via_device_exists(hass, device_info, config_entry)
if config_entry_id is not None and device_info is not None:
update_device_info = cast(dict[str, Any], device_info)
update_device_info["config_entry_id"] = config_entry_id

View File

@@ -28,13 +28,13 @@ from .const import DOMAIN, LOGGER
if TYPE_CHECKING:
from music_assistant_models.event import MassEvent
type MusicAssistantConfigEntry = ConfigEntry[MusicAssistantEntryData]
PLATFORMS = [Platform.MEDIA_PLAYER]
CONNECT_TIMEOUT = 10
LISTEN_READY_TIMEOUT = 30
type MusicAssistantConfigEntry = ConfigEntry[MusicAssistantEntryData]
@dataclass
class MusicAssistantEntryData:
@@ -47,7 +47,7 @@ class MusicAssistantEntryData:
async def async_setup_entry(
hass: HomeAssistant, entry: MusicAssistantConfigEntry
) -> bool:
"""Set up from a config entry."""
"""Set up Music Assistant from a config entry."""
http_session = async_get_clientsession(hass, verify_ssl=False)
mass_url = entry.data[CONF_URL]
mass = MusicAssistantClient(mass_url, http_session)
@@ -97,6 +97,7 @@ async def async_setup_entry(
listen_task.cancel()
raise ConfigEntryNotReady("Music Assistant client not ready") from err
# store the listen task and mass client in the entry data
entry.runtime_data = MusicAssistantEntryData(mass, listen_task)
# If the listen task is already failed, we need to raise ConfigEntryNotReady

View File

@@ -0,0 +1,7 @@
{
"services": {
"play_media": { "service": "mdi:play" },
"play_announcement": { "service": "mdi:bullhorn" },
"transfer_queue": { "service": "mdi:transfer" }
}
}

View File

@@ -7,6 +7,6 @@
"documentation": "https://www.home-assistant.io/integrations/music_assistant",
"iot_class": "local_push",
"loggers": ["music_assistant"],
"requirements": ["music-assistant-client==1.0.5"],
"requirements": ["music-assistant-client==1.0.8"],
"zeroconf": ["_mass._tcp.local."]
}

View File

@@ -0,0 +1,351 @@
"""Media Source Implementation."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from music_assistant_models.media_items import MediaItemType
from homeassistant.components import media_source
from homeassistant.components.media_player import (
BrowseError,
BrowseMedia,
MediaClass,
MediaType,
)
from homeassistant.core import HomeAssistant
from .const import DEFAULT_NAME, DOMAIN
if TYPE_CHECKING:
from music_assistant_client import MusicAssistantClient
MEDIA_TYPE_RADIO = "radio"
PLAYABLE_MEDIA_TYPES = [
MediaType.PLAYLIST,
MediaType.ALBUM,
MediaType.ARTIST,
MEDIA_TYPE_RADIO,
MediaType.TRACK,
]
LIBRARY_ARTISTS = "artists"
LIBRARY_ALBUMS = "albums"
LIBRARY_TRACKS = "tracks"
LIBRARY_PLAYLISTS = "playlists"
LIBRARY_RADIO = "radio"
LIBRARY_TITLE_MAP = {
LIBRARY_ARTISTS: "Artists",
LIBRARY_ALBUMS: "Albums",
LIBRARY_TRACKS: "Tracks",
LIBRARY_PLAYLISTS: "Playlists",
LIBRARY_RADIO: "Radio stations",
}
LIBRARY_MEDIA_CLASS_MAP = {
LIBRARY_ARTISTS: MediaClass.ARTIST,
LIBRARY_ALBUMS: MediaClass.ALBUM,
LIBRARY_TRACKS: MediaClass.TRACK,
LIBRARY_PLAYLISTS: MediaClass.PLAYLIST,
LIBRARY_RADIO: MediaClass.MUSIC, # radio is not accepted by HA
}
MEDIA_CONTENT_TYPE_FLAC = "audio/flac"
THUMB_SIZE = 200
def media_source_filter(item: BrowseMedia) -> bool:
"""Filter media sources."""
return item.media_content_type.startswith("audio/")
async def async_browse_media(
hass: HomeAssistant,
mass: MusicAssistantClient,
media_content_id: str | None,
media_content_type: str | None,
) -> BrowseMedia:
"""Browse media."""
if media_content_id is None:
return await build_main_listing(hass)
assert media_content_type is not None
if media_source.is_media_source_id(media_content_id):
return await media_source.async_browse_media(
hass, media_content_id, content_filter=media_source_filter
)
if media_content_id == LIBRARY_ARTISTS:
return await build_artists_listing(mass)
if media_content_id == LIBRARY_ALBUMS:
return await build_albums_listing(mass)
if media_content_id == LIBRARY_TRACKS:
return await build_tracks_listing(mass)
if media_content_id == LIBRARY_PLAYLISTS:
return await build_playlists_listing(mass)
if media_content_id == LIBRARY_RADIO:
return await build_radio_listing(mass)
if "artist" in media_content_id:
return await build_artist_items_listing(mass, media_content_id)
if "album" in media_content_id:
return await build_album_items_listing(mass, media_content_id)
if "playlist" in media_content_id:
return await build_playlist_items_listing(mass, media_content_id)
raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}")
async def build_main_listing(hass: HomeAssistant) -> BrowseMedia:
"""Build main browse listing."""
children: list[BrowseMedia] = []
for library, media_class in LIBRARY_MEDIA_CLASS_MAP.items():
child_source = BrowseMedia(
media_class=MediaClass.DIRECTORY,
media_content_id=library,
media_content_type=DOMAIN,
title=LIBRARY_TITLE_MAP[library],
children_media_class=media_class,
can_play=False,
can_expand=True,
)
children.append(child_source)
try:
item = await media_source.async_browse_media(
hass, None, content_filter=media_source_filter
)
# If domain is None, it's overview of available sources
if item.domain is None and item.children is not None:
children.extend(item.children)
else:
children.append(item)
except media_source.BrowseError:
pass
return BrowseMedia(
media_class=MediaClass.DIRECTORY,
media_content_id="",
media_content_type=DOMAIN,
title=DEFAULT_NAME,
can_play=False,
can_expand=True,
children=children,
)
async def build_playlists_listing(mass: MusicAssistantClient) -> BrowseMedia:
"""Build Playlists browse listing."""
media_class = LIBRARY_MEDIA_CLASS_MAP[LIBRARY_PLAYLISTS]
return BrowseMedia(
media_class=MediaClass.DIRECTORY,
media_content_id=LIBRARY_PLAYLISTS,
media_content_type=MediaType.PLAYLIST,
title=LIBRARY_TITLE_MAP[LIBRARY_PLAYLISTS],
can_play=False,
can_expand=True,
children_media_class=media_class,
children=sorted(
[
build_item(mass, item, can_expand=True)
# we only grab the first page here because the
# HA media browser does not support paging
for item in await mass.music.get_library_playlists(limit=500)
if item.available
],
key=lambda x: x.title,
),
)
async def build_playlist_items_listing(
mass: MusicAssistantClient, identifier: str
) -> BrowseMedia:
"""Build Playlist items browse listing."""
playlist = await mass.music.get_item_by_uri(identifier)
return BrowseMedia(
media_class=MediaClass.PLAYLIST,
media_content_id=playlist.uri,
media_content_type=MediaType.PLAYLIST,
title=playlist.name,
can_play=True,
can_expand=True,
children_media_class=MediaClass.TRACK,
children=[
build_item(mass, item, can_expand=False)
# we only grab the first page here because the
# HA media browser does not support paging
for item in await mass.music.get_playlist_tracks(
playlist.item_id, playlist.provider
)
if item.available
],
)
async def build_artists_listing(mass: MusicAssistantClient) -> BrowseMedia:
"""Build Albums browse listing."""
media_class = LIBRARY_MEDIA_CLASS_MAP[LIBRARY_ARTISTS]
return BrowseMedia(
media_class=MediaClass.DIRECTORY,
media_content_id=LIBRARY_ARTISTS,
media_content_type=MediaType.ARTIST,
title=LIBRARY_TITLE_MAP[LIBRARY_ARTISTS],
can_play=False,
can_expand=True,
children_media_class=media_class,
children=sorted(
[
build_item(mass, artist, can_expand=True)
# we only grab the first page here because the
# HA media browser does not support paging
for artist in await mass.music.get_library_artists(limit=500)
if artist.available
],
key=lambda x: x.title,
),
)
async def build_artist_items_listing(
mass: MusicAssistantClient, identifier: str
) -> BrowseMedia:
"""Build Artist items browse listing."""
artist = await mass.music.get_item_by_uri(identifier)
albums = await mass.music.get_artist_albums(artist.item_id, artist.provider)
return BrowseMedia(
media_class=MediaType.ARTIST,
media_content_id=artist.uri,
media_content_type=MediaType.ARTIST,
title=artist.name,
can_play=True,
can_expand=True,
children_media_class=MediaClass.ALBUM,
children=[
build_item(mass, album, can_expand=True)
for album in albums
if album.available
],
)
async def build_albums_listing(mass: MusicAssistantClient) -> BrowseMedia:
"""Build Albums browse listing."""
media_class = LIBRARY_MEDIA_CLASS_MAP[LIBRARY_ALBUMS]
return BrowseMedia(
media_class=MediaClass.DIRECTORY,
media_content_id=LIBRARY_ALBUMS,
media_content_type=MediaType.ALBUM,
title=LIBRARY_TITLE_MAP[LIBRARY_ALBUMS],
can_play=False,
can_expand=True,
children_media_class=media_class,
children=sorted(
[
build_item(mass, album, can_expand=True)
# we only grab the first page here because the
# HA media browser does not support paging
for album in await mass.music.get_library_albums(limit=500)
if album.available
],
key=lambda x: x.title,
),
)
async def build_album_items_listing(
mass: MusicAssistantClient, identifier: str
) -> BrowseMedia:
"""Build Album items browse listing."""
album = await mass.music.get_item_by_uri(identifier)
tracks = await mass.music.get_album_tracks(album.item_id, album.provider)
return BrowseMedia(
media_class=MediaType.ALBUM,
media_content_id=album.uri,
media_content_type=MediaType.ALBUM,
title=album.name,
can_play=True,
can_expand=True,
children_media_class=MediaClass.TRACK,
children=[
build_item(mass, track, False) for track in tracks if track.available
],
)
async def build_tracks_listing(mass: MusicAssistantClient) -> BrowseMedia:
"""Build Tracks browse listing."""
media_class = LIBRARY_MEDIA_CLASS_MAP[LIBRARY_TRACKS]
return BrowseMedia(
media_class=MediaClass.DIRECTORY,
media_content_id=LIBRARY_TRACKS,
media_content_type=MediaType.TRACK,
title=LIBRARY_TITLE_MAP[LIBRARY_TRACKS],
can_play=False,
can_expand=True,
children_media_class=media_class,
children=sorted(
[
build_item(mass, track, can_expand=False)
# we only grab the first page here because the
# HA media browser does not support paging
for track in await mass.music.get_library_tracks(limit=500)
if track.available
],
key=lambda x: x.title,
),
)
async def build_radio_listing(mass: MusicAssistantClient) -> BrowseMedia:
"""Build Radio browse listing."""
media_class = LIBRARY_MEDIA_CLASS_MAP[LIBRARY_RADIO]
return BrowseMedia(
media_class=MediaClass.DIRECTORY,
media_content_id=LIBRARY_RADIO,
media_content_type=DOMAIN,
title=LIBRARY_TITLE_MAP[LIBRARY_RADIO],
can_play=False,
can_expand=True,
children_media_class=media_class,
children=[
build_item(mass, track, can_expand=False, media_class=media_class)
# we only grab the first page here because the
# HA media browser does not support paging
for track in await mass.music.get_library_radios(limit=500)
if track.available
],
)
def build_item(
mass: MusicAssistantClient,
item: MediaItemType,
can_expand: bool = True,
media_class: Any = None,
) -> BrowseMedia:
"""Return BrowseMedia for MediaItem."""
if artists := getattr(item, "artists", None):
title = f"{artists[0].name} - {item.name}"
else:
title = item.name
img_url = mass.get_media_item_image_url(item)
return BrowseMedia(
media_class=media_class or item.media_type.value,
media_content_id=item.uri,
media_content_type=MediaType.MUSIC,
title=title,
can_play=True,
can_expand=can_expand,
thumbnail=img_url,
)

View File

@@ -13,15 +13,18 @@ from music_assistant_models.enums import (
EventType,
MediaType,
PlayerFeature,
PlayerState as MassPlayerState,
QueueOption,
RepeatMode as MassRepeatMode,
)
from music_assistant_models.errors import MediaNotFoundError, MusicAssistantError
from music_assistant_models.event import MassEvent
from music_assistant_models.media_items import ItemMapping, MediaItemType, Track
import voluptuous as vol
from homeassistant.components import media_source
from homeassistant.components.media_player import (
ATTR_MEDIA_ENQUEUE,
ATTR_MEDIA_EXTRA,
BrowseMedia,
MediaPlayerDeviceClass,
@@ -37,12 +40,17 @@ from homeassistant.const import STATE_OFF
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
async_get_current_platform,
)
from homeassistant.util.dt import utc_from_timestamp
from . import MusicAssistantConfigEntry
from .const import ATTR_ACTIVE_QUEUE, ATTR_MASS_PLAYER_TYPE, DOMAIN
from .entity import MusicAssistantEntity
from .media_browser import async_browse_media
if TYPE_CHECKING:
from music_assistant_client import MusicAssistantClient
@@ -78,6 +86,9 @@ QUEUE_OPTION_MAP = {
MediaPlayerEnqueue.REPLACE: QueueOption.REPLACE,
}
SERVICE_PLAY_MEDIA_ADVANCED = "play_media"
SERVICE_PLAY_ANNOUNCEMENT = "play_announcement"
SERVICE_TRANSFER_QUEUE = "transfer_queue"
ATTR_RADIO_MODE = "radio_mode"
ATTR_MEDIA_ID = "media_id"
ATTR_MEDIA_TYPE = "media_type"
@@ -137,6 +148,38 @@ async def async_setup_entry(
async_add_entities(mass_players)
# add platform service for play_media with advanced options
platform = async_get_current_platform()
platform.async_register_entity_service(
SERVICE_PLAY_MEDIA_ADVANCED,
{
vol.Required(ATTR_MEDIA_ID): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(ATTR_MEDIA_TYPE): vol.Coerce(MediaType),
vol.Optional(ATTR_MEDIA_ENQUEUE): vol.Coerce(QueueOption),
vol.Optional(ATTR_ARTIST): cv.string,
vol.Optional(ATTR_ALBUM): cv.string,
vol.Optional(ATTR_RADIO_MODE): vol.Coerce(bool),
},
"_async_handle_play_media",
)
platform.async_register_entity_service(
SERVICE_PLAY_ANNOUNCEMENT,
{
vol.Required(ATTR_URL): cv.string,
vol.Optional(ATTR_USE_PRE_ANNOUNCE): vol.Coerce(bool),
vol.Optional(ATTR_ANNOUNCE_VOLUME): vol.Coerce(int),
},
"_async_handle_play_announcement",
)
platform.async_register_entity_service(
SERVICE_TRANSFER_QUEUE,
{
vol.Optional(ATTR_SOURCE_PLAYER): cv.entity_id,
vol.Optional(ATTR_AUTO_PLAY): vol.Coerce(bool),
},
"_async_handle_transfer_queue",
)
class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
"""Representation of MediaPlayerEntity from Music Assistant Player."""
@@ -150,7 +193,7 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
super().__init__(mass, player_id)
self._attr_icon = self.player.icon.replace("mdi-", "mdi:")
self._attr_supported_features = SUPPORTED_FEATURES
if PlayerFeature.SYNC in self.player.supported_features:
if PlayerFeature.SET_MEMBERS in self.player.supported_features:
self._attr_supported_features |= MediaPlayerEntityFeature.GROUPING
if PlayerFeature.VOLUME_MUTE in self.player.supported_features:
self._attr_supported_features |= MediaPlayerEntityFeature.VOLUME_MUTE
@@ -357,24 +400,26 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
async def async_join_players(self, group_members: list[str]) -> None:
"""Join `group_members` as a player group with the current player."""
player_ids: list[str] = []
entity_registry = er.async_get(self.hass)
for child_entity_id in group_members:
# resolve HA entity_id to MA player_id
if (hass_state := self.hass.states.get(child_entity_id)) is None:
continue
if (mass_player_id := hass_state.attributes.get("mass_player_id")) is None:
continue
player_ids.append(mass_player_id)
await self.mass.players.player_command_sync_many(self.player_id, player_ids)
if not (entity_reg_entry := entity_registry.async_get(child_entity_id)):
raise HomeAssistantError(f"Entity {child_entity_id} not found")
# unique id is the MA player_id
player_ids.append(entity_reg_entry.unique_id)
await self.mass.players.player_command_group_many(self.player_id, player_ids)
@catch_musicassistant_error
async def async_unjoin_player(self) -> None:
"""Remove this player from any group."""
await self.mass.players.player_command_unsync(self.player_id)
await self.mass.players.player_command_ungroup(self.player_id)
@catch_musicassistant_error
async def _async_handle_play_media(
self,
media_id: list[str],
artist: str | None = None,
album: str | None = None,
enqueue: MediaPlayerEnqueue | QueueOption | None = None,
radio_mode: bool | None = None,
media_type: str | None = None,
@@ -401,6 +446,14 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
elif await asyncio.to_thread(os.path.isfile, media_id_str):
media_uris.append(media_id_str)
continue
# last resort: search for media item by name/search
if item := await self.mass.music.get_item_by_name(
name=media_id_str,
artist=artist,
album=album,
media_type=MediaType(media_type) if media_type else None,
):
media_uris.append(item.uri)
if not media_uris:
raise HomeAssistantError(
@@ -434,16 +487,43 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
self.player_id, url, use_pre_announce, announce_volume
)
@catch_musicassistant_error
async def _async_handle_transfer_queue(
self, source_player: str | None = None, auto_play: bool | None = None
) -> None:
"""Transfer the current queue to another player."""
if not source_player:
# no source player given; try to find a playing player(queue)
for queue in self.mass.player_queues:
if queue.state == MassPlayerState.PLAYING:
source_queue_id = queue.queue_id
break
else:
raise HomeAssistantError(
"Source player not specified and no playing player found."
)
else:
# resolve HA entity_id to MA player_id
entity_registry = er.async_get(self.hass)
if (entity := entity_registry.async_get(source_player)) is None:
raise HomeAssistantError("Source player not available.")
source_queue_id = entity.unique_id # unique_id is the MA player_id
target_queue_id = self.player_id
await self.mass.player_queues.transfer_queue(
source_queue_id, target_queue_id, auto_play
)
async def async_browse_media(
self,
media_content_type: MediaType | str | None = None,
media_content_id: str | None = None,
) -> BrowseMedia:
"""Implement the websocket media browsing helper."""
return await media_source.async_browse_media(
return await async_browse_media(
self.hass,
self.mass,
media_content_id,
content_filter=lambda item: item.media_content_type.startswith("audio/"),
media_content_type,
)
def _update_media_image_url(

View File

@@ -0,0 +1,90 @@
# Descriptions for Music Assistant custom services
play_media:
target:
entity:
domain: media_player
integration: music_assistant
supported_features:
- media_player.MediaPlayerEntityFeature.PLAY_MEDIA
fields:
media_id:
required: true
example: "spotify://playlist/aabbccddeeff"
selector:
object:
media_type:
example: "playlist"
selector:
select:
translation_key: media_type
options:
- artist
- album
- playlist
- track
- radio
artist:
example: "Queen"
selector:
text:
album:
example: "News of the world"
selector:
text:
enqueue:
selector:
select:
options:
- "play"
- "replace"
- "next"
- "replace_next"
- "add"
translation_key: enqueue
radio_mode:
advanced: true
selector:
boolean:
play_announcement:
target:
entity:
domain: media_player
integration: music_assistant
supported_features:
- media_player.MediaPlayerEntityFeature.PLAY_MEDIA
- media_player.MediaPlayerEntityFeature.MEDIA_ANNOUNCE
fields:
url:
required: true
example: "http://someremotesite.com/doorbell.mp3"
selector:
text:
use_pre_announce:
example: "true"
selector:
boolean:
announce_volume:
example: 75
selector:
number:
min: 1
max: 100
step: 1
transfer_queue:
target:
entity:
domain: media_player
integration: music_assistant
fields:
source_player:
selector:
entity:
domain: media_player
integration: music_assistant
auto_play:
example: "true"
selector:
boolean:

View File

@@ -37,6 +37,70 @@
"description": "Check if there are updates available for the Music Assistant Server and/or integration."
}
},
"services": {
"play_media": {
"name": "Play media",
"description": "Play media on a Music Assistant player with more fine-grained control options.",
"fields": {
"media_id": {
"name": "Media ID(s)",
"description": "URI or name of the item you want to play. Specify a list if you want to play/enqueue multiple items."
},
"media_type": {
"name": "Media type",
"description": "The type of the content to play. Such as artist, album, track or playlist. Will be auto-determined if omitted."
},
"enqueue": {
"name": "Enqueue",
"description": "If the content should be played now or added to the queue."
},
"artist": {
"name": "Artist name",
"description": "When specifying a track or album by name in the Media ID field, you can optionally restrict results by this artist name."
},
"album": {
"name": "Album name",
"description": "When specifying a track by name in the Media ID field, you can optionally restrict results by this album name."
},
"radio_mode": {
"name": "Enable radio mode",
"description": "Enable radio mode to auto-generate a playlist based on the selection."
}
}
},
"play_announcement": {
"name": "Play announcement",
"description": "Play announcement on a Music Assistant player with more fine-grained control options.",
"fields": {
"url": {
"name": "URL",
"description": "URL to the notification sound."
},
"use_pre_announce": {
"name": "Use pre-announce",
"description": "Use pre-announcement sound for the announcement. Omit to use the player default."
},
"announce_volume": {
"name": "Announce volume",
"description": "Use a forced volume level for the announcement. Omit to use player default."
}
}
},
"transfer_queue": {
"name": "Transfer queue",
"description": "Transfer the player's queue to another player.",
"fields": {
"source_player": {
"name": "Source media player",
"description": "The source media player which has the queue you want to transfer. When omitted, the first playing player will be used."
},
"auto_play": {
"name": "Auto play",
"description": "Start playing the queue on the target player. Omit to use the default behavior."
}
}
}
},
"selector": {
"enqueue": {
"options": {
@@ -46,6 +110,15 @@
"replace": "Play now and clear queue",
"replace_next": "Play next and clear queue"
}
},
"media_type": {
"options": {
"artist": "Artist",
"album": "Album",
"track": "Track",
"playlist": "Playlist",
"radio": "Radio"
}
}
}
}

View File

@@ -13,6 +13,7 @@ from homeassistant.components.water_heater import (
WaterHeaterEntityFeature,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.util import dt as dt_util
from .. import OverkizDataUpdateCoordinator
from ..entity import OverkizEntity
@@ -153,11 +154,11 @@ class AtlanticDomesticHotWaterProductionMBLComponent(OverkizEntity, WaterHeaterE
async def async_turn_away_mode_on(self) -> None:
"""Turn away mode on.
This requires the start date and the end date to be also set.
This requires the start date and the end date to be also set, and those dates have to match the device datetime.
The API accepts setting dates in the format of the core:DateTimeState state for the DHW
{'day': 11, 'hour': 21, 'minute': 12, 'month': 7, 'second': 53, 'weekday': 3, 'year': 2024})
The dict is then passed as an away mode start date, and then as an end date, but with the year incremented by 1,
so the away mode is getting turned on for the next year.
{'day': 11, 'hour': 21, 'minute': 12, 'month': 7, 'second': 53, 'weekday': 3, 'year': 2024}
The dict is then passed as an actual device date, the away mode start date, and then as an end date,
but with the year incremented by 1, so the away mode is getting turned on for the next year.
The weekday number seems to have no effect so the calculation of the future date's weekday number is redundant,
but possible via homeassistant dt_util to form both start and end dates dictionaries from scratch
based on datetime.now() and datetime.timedelta into the future.
@@ -167,13 +168,19 @@ class AtlanticDomesticHotWaterProductionMBLComponent(OverkizEntity, WaterHeaterE
With `refresh_afterwards=False` on the first commands, and `refresh_afterwards=True` only the last command,
the API is not choking and the transition is smooth without the unavailability state.
"""
now_date = cast(
dict,
self.executor.select_state(OverkizState.CORE_DATETIME),
)
now = dt_util.now()
now_date = {
"month": now.month,
"hour": now.hour,
"year": now.year,
"weekday": now.weekday(),
"day": now.day,
"minute": now.minute,
"second": now.second,
}
await self.executor.async_execute_command(
OverkizCommand.SET_ABSENCE_MODE,
OverkizCommandParam.PROG,
OverkizCommand.SET_DATE_TIME,
now_date,
refresh_afterwards=False,
)
await self.executor.async_execute_command(
@@ -183,7 +190,11 @@ class AtlanticDomesticHotWaterProductionMBLComponent(OverkizEntity, WaterHeaterE
await self.executor.async_execute_command(
OverkizCommand.SET_ABSENCE_END_DATE, now_date, refresh_afterwards=False
)
await self.executor.async_execute_command(
OverkizCommand.SET_ABSENCE_MODE,
OverkizCommandParam.PROG,
refresh_afterwards=False,
)
await self.coordinator.async_refresh()
async def async_turn_away_mode_off(self) -> None:

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