Compare commits

..

611 Commits

Author SHA1 Message Date
Franck Nijhof ab5d1d27f1 2025.9.2 (#152198) 2025-09-12 23:10:27 +02:00
wollew 1c10b85fed Use position percentage for closed status in Velux (#151679)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-12 20:39:06 +00:00
Franck Nijhof 91a7db08ff Bump version to 2025.9.2 2025-09-12 20:20:56 +00:00
Bram Kragten a764d54123 Update frontend to 20250903.5 (#152170) 2025-09-12 20:20:44 +00:00
Joakim Sørensen dc09e33556 Bump hass-nabucasa from 1.1.0 to 1.1.1 (#152147) 2025-09-12 20:20:43 +00:00
Simone Chemelli 14173bd9ec Fix reauth for Alexa Devices (#152128) 2025-09-12 20:20:42 +00:00
Jan Bouwhuis d2e7537629 Fix supported _color_modes attribute not set for on/off MQTT JSON light (#152126) 2025-09-12 20:20:41 +00:00
J. Nick Koston 9a165a64fe Fix DoorBird being updated with wrong IP addresses during discovery (#152088)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-12 20:20:39 +00:00
Abílio Costa 9c749a6abc Fix duplicated IP port usage in Govee Light Local (#152087) 2025-09-12 20:20:38 +00:00
J. Nick Koston 2e33222c71 Fix HomeKit Controller stale values at startup (#152086)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2025-09-12 20:20:37 +00:00
Maciej Bieniek ab1c2c4f70 Bump accuweather to version 4.2.1 (#152029) 2025-09-12 20:20:36 +00:00
Joost Lekkerkerker 529219ae69 Bump yt-dlp to 2025.09.05 (#152006) 2025-09-12 20:20:35 +00:00
J. Nick Koston d6ce71fa61 Bump habluetooth to 5.6.2 (#151985) 2025-09-12 20:20:33 +00:00
peteS-UK e5b67d513a Fix playlist media_class_filter in search_media for squeezebox (#151973) 2025-09-12 20:20:32 +00:00
peteS-UK a547179f66 Fix for squeezebox track content_type (#151963) 2025-09-12 20:20:31 +00:00
epenet 8c61788a7d Fix invalid logger in Tuya (#151957) 2025-09-12 20:19:26 +00:00
J. Nick Koston 6b934d94db Bump habluetooth to 5.6.0 (#151942) 2025-09-12 20:18:38 +00:00
J. Nick Koston d30ad82774 Bump bleak-esphome to 3.3.0 (#151922)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-09-12 20:18:37 +00:00
J. Nick Koston 4618b33e93 Bump habluetooth to 5.5.1 (#151921) 2025-09-12 20:18:36 +00:00
epenet d6299094db Fix _is_valid_suggested_unit in sensor platform (#151912) 2025-09-12 20:18:35 +00:00
Simone Chemelli 087d9d30c0 Avoid cleanup/recreate of device_trackers not linked to a device for Vodafone Station (#151904) 2025-09-12 20:18:33 +00:00
Simone Chemelli f07890cf5c Bump aiovodafone to 1.2.1 (#151901) 2025-09-12 20:18:31 +00:00
Avi Miller e5b78cc481 Bump aiolifx-themes to 1.0.2 to support newer LIFX devices (#151898)
Signed-off-by: Avi Miller <me@dje.li>
2025-09-12 20:18:30 +00:00
Manu 12b409d8e1 Bump aiontfy to v0.5.5 (#151869) 2025-09-12 20:18:29 +00:00
Maciej Bieniek def5408db8 Use native_visibility property instead of visibility for OpenWeatherMap weather entity (#151867) 2025-09-12 20:18:28 +00:00
Robert Resch f105b45ee2 Bump aioecowitt to 2025.9.1 (#151859) 2025-09-12 20:18:26 +00:00
wollew 9d904c30a7 fix rain sensor for Velux GPU windows (#151857) 2025-09-12 20:18:25 +00:00
Martins Sipenko 99b047939f Update pysmarty2 to 0.10.3 (#151855) 2025-09-12 20:18:24 +00:00
J. Nick Koston 3a615908ee Bump aioharmony to 0.5.3 (#151853) 2025-09-12 20:18:23 +00:00
David Knowles baff541f46 Bump pydrawise to 2025.9.0 (#151842) 2025-09-12 20:18:22 +00:00
jan iversen 6d8c35cfe9 removed assert fron entity in modbus. (#151834) 2025-09-12 20:18:20 +00:00
jan iversen b8d9883e74 max_temp / min_temp in modbus light could only be int, otherwise an assert was provoked. (#151833) 2025-09-12 20:18:19 +00:00
jan iversen c3c65af450 Allow delay > 1 in modbus. (#151832) 2025-09-12 20:18:18 +00:00
Maciej Bieniek 3af8616764 Mark Tractive switches as unavailable when tacker is in the enegy saving zone (#151817) 2025-09-12 20:18:17 +00:00
Matthias Alphart 64ec4609c5 Fix KNX Light - individual color initialisation from UI config (#151815) 2025-09-12 20:18:15 +00:00
Matthias Alphart c78bc26b83 Fix KNX BinarySensor config_store data (#151808) 2025-09-12 20:18:14 +00:00
J. Nick Koston 0c093646c9 Bump habluetooth to 5.3.1 (#151803) 2025-09-12 20:18:13 +00:00
Simone Chemelli 1b27acdde0 Improve config entry migration for edge cases in Alexa Devices (#151788) 2025-09-12 20:18:12 +00:00
Ludovic BOUÉ 9dafc0e02f Remove device class for Matter NitrogenDioxideSensor (#151782)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-09-12 20:18:10 +00:00
Tsvi Mostovicz 0091dafcb0 Revert "Jewish Calendar add coordinator " (#151780) 2025-09-12 20:18:09 +00:00
Jan Bouwhuis b387acffb7 Fix update of the entity ID does not clean up an old restored state (#151696)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-09-12 20:18:08 +00:00
blotus 36b3133fa2 Fix support for Ecowitt soil moisture sensors (#151685) 2025-09-12 20:18:06 +00:00
hbludworth fe01e96012 Fix Aladdin Connect state not updating (#151652)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-09-12 20:18:05 +00:00
Thomas55555 0b56ec16ed Add event entity on websocket ready in Husqvarna Automower (#151428) 2025-09-12 20:18:04 +00:00
Mark Adkins ca79f4c963 Update SharkIQ authentication method (#151046) 2025-09-12 20:18:02 +00:00
Bob Igo 9a43f2776d Fix XMPP not working with non-TLS servers (#150957) 2025-09-12 20:18:01 +00:00
Franck Nijhof 0cda883b56 2025.9.1 (#151766) 2025-09-05 13:13:34 +02:00
Franck Nijhof ae58e633f0 Bump version to 2025.9.1 2025-09-05 10:33:36 +00:00
jan iversen 06480bfd9d Fix enable/disable entity in modbus (#151626) 2025-09-05 10:33:04 +00:00
Artur Pragacz 625f586945 Fix recognition of entity names in default agent with interpunction (#151759) 2025-09-05 10:30:27 +00:00
Richard Kroegel 7dbeaa475d Bump bimmer_connected to 0.17.3 (#151756) 2025-09-05 10:30:24 +00:00
David Knowles dff3d5f8af Bump pyschlage to 2025.9.0 (#151731) 2025-09-05 10:30:21 +00:00
Michael Hansen 89c335919a Handle match failures in intent HTTP API (#151726) 2025-09-05 10:30:16 +00:00
Daniel Hjelseth Høyer 2bb4573357 Update Mill library 0.13.1 (#151712) 2025-09-05 10:30:01 +00:00
Dan Raper 7037ce989c Bump ohmepy version to 1.5.2 (#151707) 2025-09-05 10:29:58 +00:00
Dan Raper bfdd2053ba Require OhmeAdvancedSettingsCoordinator to run regardless of entities (#151701) 2025-09-05 10:29:55 +00:00
Bram Kragten fcc3f92f8c Update frontend to 20250903.3 (#151694) 2025-09-05 10:29:51 +00:00
Marcel van der Veldt 8710267d53 Bump aiohue to 4.7.5 (#151684) 2025-09-05 10:29:48 +00:00
Imeon-Energy 85b6adcc9a Fix, entities stay unavailable after timeout error, Imeon inverter integration (#151671)
Co-authored-by: TheBushBoy <theodavid@icloud.com>
2025-09-05 10:29:46 +00:00
Felipe Santos beec6e86e0 Fix WebSocket proxy for add-ons not forwarding ping/pong frame data (#151654) 2025-09-05 10:29:45 +00:00
Pete Sage 3dacffaaf9 Fix Sonos Dialog Select type conversion (#151649) 2025-09-05 10:29:41 +00:00
Manu d90f2a1de1 Correct capitalization of "FRITZ!Box" in FRITZ!Box Tools integration (#151637) 2025-09-05 10:29:39 +00:00
karwosts b6c9217429 Add missing device trigger duration localizations (#151578)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-09-05 10:29:37 +00:00
Marcel van der Veldt 7fc8da6769 Add support for migrated Hue bridge (#151411)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-09-05 10:29:34 +00:00
Franck Nijhof 220c233c0b 2025.9.0 (#151263) 2025-09-03 19:49:19 +02:00
Franck Nijhof cdf7d8df16 Bump version to 2025.9.0 2025-09-03 17:12:06 +00:00
Franck Nijhof 5db4057781 Bump version to 2025.9.0b6 2025-09-03 16:17:57 +00:00
Bram Kragten 5d9277e4ab Update frontend to 20250903.2 (#151629) 2025-09-03 16:16:36 +00:00
Michael Hansen 67d3a9623d Bump intents (#151627) 2025-09-03 16:14:25 +00:00
Norbert Rittel 0ad44e423b Fix naming of "State of charge" sensor in growatt_server (#151619) 2025-09-03 16:12:59 +00:00
jan iversen cb7097cdf1 Simplify Modbus update methods (#151494) 2025-09-03 16:12:57 +00:00
mattreim 22b8ad9d0b Fix for deCONZ issue - Detected that integration 'deconz' calls device_registry.async_get_or_create referencing a non existing via_device - #134539 (#150355) 2025-09-03 16:12:56 +00:00
Franck Nijhof 26c9d283a4 Bump version to 2025.9.0b5 2025-09-03 11:38:27 +00:00
Bram Kragten 4dbccbc056 Update frontend to 20250903.1 (#151617) 2025-09-03 11:38:03 +00:00
Erik Montnemery 17466ce866 Bump device registry version to 1.12 (#151616) 2025-09-03 11:38:02 +00:00
Erik Montnemery 422862a699 Handle colliding aliases for floors (#151614) 2025-09-03 11:38:00 +00:00
Erik Montnemery 5be2e4e14b Handle colliding aliases for areas (#151613) 2025-09-03 11:37:59 +00:00
Bram Kragten 75ebbe60db Update frontend to 20250903.0 (#151612) 2025-09-03 11:37:58 +00:00
Robert Resch f0e18cc63d Bump aioecowitt to 2025.9.0 (#151608) 2025-09-03 11:37:57 +00:00
Krisjanis Lejejs c2b4e9b075 Bump hass-nabucasa from 1.0.0 to 1.1.0 (#151606) 2025-09-03 11:37:55 +00:00
jan iversen baa1c51bcf Fix racing bug in slave entities in Modbus (#151522) 2025-09-03 11:37:54 +00:00
Franck Nijhof a1d484fa73 Bump version to 2025.9.0b4 2025-09-03 09:24:19 +00:00
Paul Bottein 6013f50aa6 Update frontend to 20250902.1 (#151593) 2025-09-03 09:23:18 +00:00
Stefan Agner b4ab63d9db Update Home Assistant base image to 2025.09.0 (#151582) 2025-09-03 09:23:16 +00:00
Thomas D 7229781aeb Bump volvocarsapi to v0.4.2 (#151579) 2025-09-03 09:23:15 +00:00
Erik Montnemery 465512b0ea Improve migration to device registry version 1.10 (#151571) 2025-09-03 09:23:14 +00:00
Erik Montnemery 75d6c0bb53 Improve migration to entity registry version 1.18 (#151570) 2025-09-03 09:23:12 +00:00
Erik Montnemery 7500406e36 Revert "Improve migration to device registry version 1.11" (#151563) 2025-09-03 09:23:11 +00:00
Erik Montnemery 2afbca9751 Revert "Improve migration to entity registry version 1.18" (#151561) 2025-09-03 09:23:09 +00:00
Simone Chemelli 6023a8e6b0 Remove config entry from device instead of deleting in Uptime robot (#151557) 2025-09-03 09:23:08 +00:00
Erik Montnemery 869801b643 Exclude non mowers from husqvarna_automower_ble discovery (#151507) 2025-09-03 09:23:07 +00:00
Franck Nijhof ed9e46bbca Bump version to 2025.9.0b3 2025-09-02 08:42:14 +00:00
Abílio Costa ac4eef0571 Add back missing controller cleanup to Govee Light Local (#151541) 2025-09-02 08:42:08 +00:00
Abílio Costa 1039936f39 Filter out IPv6 addresses in Govee Light Local (#151540) 2025-09-02 08:42:06 +00:00
Lukas 9910df2b21 Remove mac address from Pooldose device (#151536)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2025-09-02 08:42:05 +00:00
Bram Kragten e57019a80b Update frontend to 20250901.0 (#151529) 2025-09-02 08:42:03 +00:00
Imeon-Energy 1f6853db28 Fix typo in const.py for Imeon inverter integration (#151515)
Co-authored-by: TheBushBoy <theodavid@icloud.com>
2025-09-02 08:42:02 +00:00
Andrew Jackson dc4d6ddbef Bump aiomealie to 0.10.2 (#151514) 2025-09-02 08:42:00 +00:00
Antoni Czaplicki 399286deae Remove the vulcan integration (#151504) 2025-09-02 08:41:59 +00:00
Joost Lekkerkerker 1a2898cc89 Update Pooldose quality scale (#151499) 2025-09-02 08:41:57 +00:00
Iskra kranj d9629affca Bump pyiskra to 0.1.26 (#151489) 2025-09-02 08:41:56 +00:00
Willem-Jan van Rootselaar 9581c705b9 Fix add checks for None values and check if DHW is available (#151376) 2025-09-02 08:41:55 +00:00
Andrea Turri e2a4a9393e Miele refrigerators cause index out of range errors when offline (#151299) 2025-09-02 08:41:53 +00:00
Norbert Rittel f32d12c519 Fix wrong description for numeric_state observation in bayesian (#151291) 2025-09-02 08:41:52 +00:00
Artur Pragacz 18ce6da4e6 Allow ignored Onkyo devices to be set up from the user flow (#150921) 2025-09-02 08:41:51 +00:00
Jozef Kruszynski 031ae3a921 Fix sort order in media browser for music assistant integration (#150910) 2025-09-02 08:41:49 +00:00
Artur Pragacz bbe66f5cea Improve unpair schema in homekit (#150235) 2025-09-02 08:41:48 +00:00
Phil Male 9f4369dc8b Use average color for Hue light group state (#149499) 2025-09-02 08:41:46 +00:00
Franck Nijhof 249dbf976f Bump version to 2025.9.0b2 2025-09-01 10:36:21 +00:00
Imeon-Energy 8d1a45bb8b Missing state for inverter state sensor in Imeon inverter (#151493)
Co-authored-by: TheBushBoy <theodavid@icloud.com>
2025-09-01 10:36:02 +00:00
Joost Lekkerkerker e3d08d5f26 Set Aladdin Connect integration type to hub (#151491) 2025-09-01 10:36:00 +00:00
Marc Mueller a67919fd7c Fix backup manager delete backup error filter (#151490) 2025-09-01 10:35:59 +00:00
Denis Shulyaka 5edb786aad Allow structure field of ai_task.generate_data for non-advanced users (#151481) 2025-09-01 10:35:57 +00:00
J. Nick Koston bfa3b53409 Bump bluetooth-adapters to 2.1.0 and habluetooth to 5.3.0 (#151465) 2025-09-01 10:35:56 +00:00
tronikos 0050626d8c Bump opower to 0.15.4 (#151443) 2025-09-01 10:35:54 +00:00
Brett Adams 74c91e46f2 Fix history startup failures (#151439) 2025-09-01 10:35:52 +00:00
J. Nick Koston d00bf4b014 Fix Yale Access Bluetooth key discovery timing issues (#151433)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-01 10:34:52 +00:00
Thomas55555 fbab53bd0c Bump aioautomower to 2.2.1 (#151427) 2025-09-01 10:29:59 +00:00
Maciej Bieniek 66442f1714 Allow integration to initialize when BraviaTV is offline (#151415) 2025-09-01 10:29:56 +00:00
Aaron Bach 281bf2f308 Bump aiopurpleair to 2025.08.1 (#151398) 2025-09-01 10:29:54 +00:00
Michael Hansen d9af4f1b3c Bump intents to 2025.8.29 (#151397) 2025-09-01 10:28:53 +00:00
Russell VanderMey b86c37f556 Avoid blocking IO in TRIGGERcmd (#151396) 2025-09-01 10:27:25 +00:00
karwosts 94081e011b Fix play media example data (#151394) 2025-09-01 10:27:23 +00:00
J. Nick Koston 5428c6fc23 Bump habluetooth to 5.2.1 (#151391) 2025-09-01 10:27:22 +00:00
Paul Bottein 737ee51b53 Update frontend to 20250829.0 (#151390) 2025-09-01 10:26:33 +00:00
J. Nick Koston 9d0e222671 Reduce log spam from unauthenticated websocket connections (#151388) 2025-09-01 10:25:29 +00:00
Arjan a972c1e0b0 Fix typo in Meteo France mappings (#151344) 2025-09-01 10:25:27 +00:00
Simone Chemelli aea39133d0 Change sounds list source for Alexa Devices (#151317) 2025-09-01 10:25:26 +00:00
jan iversen 12c9f6bea9 modbus: Do not modify registers (return wrong data). (#151131) 2025-09-01 10:25:25 +00:00
Yevhenii Vaskivskyi afdb004aa0 Fix bug with the wrong temperature scale on new router firmware (asuswrt) (#151011) 2025-09-01 10:25:23 +00:00
Franck Nijhof 3ea0e9ee88 Bump version to 2025.9.0b1 2025-08-29 15:17:32 +00:00
Erik Montnemery bec8cf3ea8 Fix restoring disabled_by flag of deleted devices (#151313) 2025-08-29 15:16:24 +00:00
J. Nick Koston e21c2fa08b Bump aioesphomeapi to 39.0.1 (#151385) 2025-08-29 15:12:34 +00:00
Marc Mueller 900b59d148 Pin pytest-rerunfailures to 15.1 (#151383) 2025-08-29 15:12:33 +00:00
J. Nick Koston 0a9203e241 Bump bleak-esphome to 3.2.0 (#151380) 2025-08-29 15:12:31 +00:00
J. Nick Koston c69c3e7d85 Bump nexia to 2.11.1 (#151379) 2025-08-29 15:12:30 +00:00
starkillerOG a938a33e98 Bump reolink-aio to 0.15.0 (#151367)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-08-29 15:12:29 +00:00
Tom 2d62c5f8d6 Bump airOS to 0.4.4 (#151345) 2025-08-29 15:12:27 +00:00
J. Nick Koston da4ec7b3dd Bump bleak-retry-connector to 4.4.3 (#151341) 2025-08-29 15:12:26 +00:00
J. Nick Koston 1ffc0560c5 Bump habluetooth to 5.2.0 (#151333) 2025-08-29 15:12:25 +00:00
Robert Resch 68ec41c43a Bump deebot-client to 13.7.0 (#151327) 2025-08-29 15:12:23 +00:00
Paul Bottein 24017a9555 Update frontend to 20250828.0 (#151321) 2025-08-29 15:12:22 +00:00
J. Nick Koston 6f17c1653c Bump nexia to 2.11.0 (#151319) 2025-08-29 15:12:21 +00:00
Erik Montnemery f49ce2a77a Improve migration to device registry version 1.11 (#151315)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-08-29 15:12:19 +00:00
Yevhenii Vaskivskyi 282ec58c4e Bump asusrouter to 1.20.1 (#151311) 2025-08-29 15:07:34 +00:00
Erik Montnemery 32cbd2a239 Improve migration to entity registry version 1.18 (#151308)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-08-29 15:07:33 +00:00
Martin Hjelmare 12b161e154 Fix Z-Wave duplicate notification binary sensors (#151304) 2025-08-29 15:07:32 +00:00
starkillerOG d0866704ba Fix Reolink duplicates due to wrong merge (#151298) 2025-08-29 15:07:30 +00:00
Simone Chemelli b23bf164f1 Add missing state class to Alexa Devices sensors (#151296) 2025-08-29 15:07:29 +00:00
Simone Chemelli 894fb6ee66 Fix exception countries migration for Alexa Devices (#151292) 2025-08-29 15:07:28 +00:00
Arjan d5e9d2b9dc Adding missing: Averses de grèle (#151288) 2025-08-29 15:07:27 +00:00
Felipe Santos 2de572ea11 Fix ONVIF not displaying sensor and binary_sensor entity names (#151285) 2025-08-29 15:07:25 +00:00
Jamie Magee def27ab705 Remove uv.lock (#151282) 2025-08-29 15:07:24 +00:00
Andrew Jackson ab7c5bf8d9 Fix endpoint deprecation warning in Mastodon (#151275) 2025-08-29 15:07:22 +00:00
G Johansson a57d77899a Fix spelling in bayesian strings (#151265) 2025-08-29 15:07:21 +00:00
Florent Thoumie e2ca439a3a Iaqualink: create parent device manually and link entities (#151215) 2025-08-29 15:07:20 +00:00
Manu 5d64dae3a0 Fix direct message notifiers in PlayStation Network (#150548) 2025-08-29 15:07:18 +00:00
Manu 821577dc21 Ignore errors when PlayStation Network group fetch is blocked by parental controls (#150364) 2025-08-29 15:07:17 +00:00
Ian Tewksbury 0eaf8c6946 Add multiple NICs in govee_light_local (#128123) 2025-08-29 15:07:15 +00:00
Franck Nijhof f955dec1ba Bump version to 2025.9.0b0 2025-08-27 18:06:45 +00:00
Jamie Magee 8fc334b338 Re-add aladdin_connect integration (#149029)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-08-27 19:06:53 +02:00
Martin Hjelmare abb59f2233 Use Z-Wave notification event enums in binary sensor (#151236) 2025-08-27 19:06:38 +02:00
dependabot[bot] f583dfe532 Bump actions/dependency-review-action from 4.7.2 to 4.7.3 (#151251)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-27 19:00:26 +02:00
Michael Hansen cd40b7eed6 Bump intents to 2025.8.27 (#151250) 2025-08-27 18:45:43 +02:00
Abílio Costa dad96598a3 Remove uneeded update listener from Idasen (#151243) 2025-08-27 18:22:34 +02:00
Norbert Rittel 669527b1e9 Capitalize "TV (show)" in media_player (#151249) 2025-08-27 17:11:26 +01:00
Petro31 090c74f18e Update object_id to default_entity_id and consolidate common schemas (#151235) 2025-08-27 18:06:03 +02:00
Simone Chemelli 4b9594b876 Bump aioamazondevices to 5.0.1 (#151246) 2025-08-27 18:05:01 +02:00
jvmahon cd5bfd6baf Add Matter lock event changed_by (#149861)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-08-27 17:48:55 +02:00
Matthias Alphart 2ef335f403 KNX: Support external scene activation recording (#151218) 2025-08-27 17:13:49 +02:00
Ludovic BOUÉ 22e70723f4 Matter SensitivityLevel for Aqara Door and Window Sensor P2 (#151117) 2025-08-27 17:05:30 +02:00
Thomas D aac572c457 Record scene activation for Qbus integration (#151232) 2025-08-27 17:02:12 +02:00
Simone Chemelli 8f9167abbe Followup async_migrate_entry fix for Alexa Devices (#151231) 2025-08-27 15:46:57 +02:00
Bram Kragten d0deb16c10 Update frontend to 20250827.0 (#151237) 2025-08-27 15:46:17 +02:00
dependabot[bot] ad37e00d1d Bump actions/ai-inference from 2.0.0 to 2.0.1 (#151147)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-27 14:04:01 +02:00
karwosts 4821c9ec29 Use media_selector for media_player.play_media (#150721) 2025-08-27 12:39:33 +02:00
Martin Hjelmare 81a5b4a684 Refactor zwave_js discovery schema foundation (#151146) 2025-08-27 12:01:34 +02:00
Denis Shulyaka 20e4d37cc6 Add ai_task.generate_image action (#151101) 2025-08-27 11:41:14 +02:00
Mike Degatano adfdeff84c Use unhealthy/unsupported reason enums from aiohasupervisor (#150919) 2025-08-27 11:27:38 +02:00
Jan Bouwhuis 0d29b2d5a7 Add MQTT alarm control panel subentry support (#150395)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-08-27 11:00:31 +02:00
Sören Beye e894a03c43 Person: Use the home zone lat/lon coordinates when detected home by a stationary tracker (#134075)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-08-27 10:57:25 +02:00
Petro31 43a1a679f9 Add object_id to modern template syntax (#150489)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-08-27 10:49:38 +02:00
Franck Nijhof 1759bfbfaf Merge branch 'master' into dev 2025-08-27 08:47:22 +00:00
epenet fc1c0d22b9 Add online status to Tuya debug log (#151222) 2025-08-27 10:45:00 +02:00
Erik Montnemery 3a48c9569c Fix stale comment in entity registry (#151226) 2025-08-27 10:43:59 +02:00
Erik Montnemery 85f3f180ab Fix stale comment in device registry (#151227) 2025-08-27 10:43:41 +02:00
Erik Montnemery bfd4f85225 Fix husqvarna_automower_ble activity mapping (#151228) 2025-08-27 10:37:32 +02:00
Erik Montnemery 2abb914867 Update husqvarna_automower_ble bluetooth discovery checks (#151225) 2025-08-27 10:36:42 +02:00
MosheL 8b10128c50 Fix CCM15 temperature set always changes the ac_mode to cool (#134719)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Joostlek <joostlek@outlook.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-08-27 10:16:29 +02:00
wollew 10bf1cb999 Add DeviceInfo to Velux entities (#149575)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-08-27 10:15:52 +02:00
Alistair Francis 6e45713d3a Ask for PIN in Husqvarna Automower BLE integration (#135440)
Signed-off-by: Alistair Francis <alistair@alistair23.me>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Christopher Fenner <9592452+CFenner@users.noreply.github.com>
2025-08-27 09:49:05 +02:00
Yuxin Wang 50a2eba66e Add platform patching in init_integration fixture in copilot-instructions.md (#151173) 2025-08-27 08:14:42 +02:00
Thomas55555 0bb16befbd Make event entity dependend on websocket in Husqvarna Automower (#151203) 2025-08-27 07:47:14 +02:00
Thomas55555 d72cc45ca8 Bump aioautomower to 2.2.0 (#151207) 2025-08-27 07:46:21 +02:00
J. Nick Koston d4bc066cc4 Bump bleak-retry-connector to 4.4.1 (#151217) 2025-08-27 07:46:02 +02:00
Jonathan Jogenfors c2c561bc21 Don't use custom bypass in SIA (#132628) 2025-08-27 07:43:53 +02:00
J. Nick Koston 5a6e26fedf Bump yalexs to 9.0.1 (#151216) 2025-08-26 22:13:06 -05:00
Lukas 51c7bafb41 Add Seko PoolDose integration (#146972)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-08-27 01:48:50 +02:00
Joakim Plate c8964494a2 Move togrill entites to sub devices (#151100) 2025-08-27 01:17:33 +02:00
Matthias Alphart 5db5f36554 Update xknx to 3.9.0 (#151214) 2025-08-27 01:05:40 +02:00
Thomas D 11f1e376c4 Add binary sensors to Volvo integration (#150127) 2025-08-26 23:52:13 +02:00
jan iversen 0139407f52 modbus: add async_will_remove_from_hass() to do cleanup. (#150906) 2025-08-26 23:50:11 +02:00
Stefan Agner d278d21561 Bump aiohasupervisor from version 0.3.2b0 to version 0.3.2 (#151202) 2025-08-26 23:39:55 +02:00
Jan-Philipp Benecke efce6c8468 Bump aioelectricitymaps to v1.1.1 (#150928) 2025-08-26 22:16:17 +01:00
TheJulianJES 8c7e9bcf7c Bump ZHA to 0.0.70 (#151212) 2025-08-26 22:13:26 +01:00
Marc Mueller 8bcc9485c3 Update orjson to 3.11.3 (#151211) 2025-08-26 22:43:41 +02:00
felosity bc8e00d9e0 Add support for HTTP Digest Authentication in REST commands (#150865)
Co-authored-by: Jan-Philipp Benecke <jan-philipp@bnck.me>
2025-08-26 20:51:44 +02:00
Artur Pragacz d5c208672e Add TARGET_FIELDS to config validation (#150238) 2025-08-26 19:40:39 +01:00
Marc Mueller 376f6ce4a7 Update h2 to 4.3.0 (#151194) 2025-08-26 19:35:47 +02:00
Álvaro Fernández Rojas 370bb14b46 Update aioairzone-cloud to v0.7.2 (#151200) 2025-08-26 19:34:25 +02:00
starkillerOG 3d1773fca5 Add Reolink chime silent time number entity (#151190) 2025-08-26 19:32:19 +02:00
starkillerOG 977d4c8f01 Add Reolink speak and doorbell volume entities (#151198)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-08-26 19:30:14 +02:00
Thomas55555 bf0bcc4c95 Remove unused constants in Husqvarna Automower (#151205) 2025-08-26 19:28:53 +02:00
HarvsG ecb51ce185 Baysesian Config Flow (#122552)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-26 19:15:57 +02:00
Tomeroeni 87f0703be1 Add support for port control in UniFi switch integration (#150152) 2025-08-26 17:20:45 +02:00
Erik Montnemery a90ac612f0 Allow dynamically creating menu options in SchemaFlowHandler (#151191) 2025-08-26 14:38:14 +02:00
Duco Sebel e2faa7020b Bumb python-homewizard-energy to 9.3.0 (#151187) 2025-08-26 13:59:08 +02:00
Thomas D ce523fc91d Expose method to set last activated on scene (#146884) 2025-08-26 12:44:07 +01:00
Erik Montnemery 5bb96f7f06 Adjust device disabled_by flag when changing config entry (#151155) 2025-08-26 12:37:56 +02:00
Michel van de Wetering dfbe42fb21 Use device id instead of archetype to check for Hue bridge (#151097) 2025-08-26 12:30:28 +02:00
markhannon c9876e2a2b Fix support for blinds in zimi integration (#150729)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-08-26 12:24:54 +02:00
Paulus Schoutsen 4ee9eada41 Mark AI Task as integration type entity (#151188) 2025-08-26 12:23:47 +02:00
Thomas D e82d91d394 Fix API field rename for Volvo integration (#151183) 2025-08-26 12:23:33 +02:00
tronikos 60e91555f8 Remove Arizona Public Service (APS) virtual integration (#150944) 2025-08-26 12:21:20 +02:00
Christian e5f163fa56 Add clear cache button to Fully Kiosk integration (#150943)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-26 12:20:43 +02:00
Florent Thoumie 060f748287 Update iaqualink to 0.6.0 (#151176) 2025-08-26 12:20:14 +02:00
Simone Chemelli 0031bce832 Fix async_migrate_entry for Alexa Devices (#151038) 2025-08-26 12:15:46 +02:00
starkillerOG 4c166c2320 Bump reolink-aio to 0.14.7 (#151045)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-26 12:11:33 +02:00
epenet 340a5f92e5 Add battery and tamper to Tuya siren (#151132) 2025-08-26 12:06:17 +02:00
Jon Seager 32645bead0 Bump pytouchlinesl to 0.5.0 (#151140) 2025-08-26 12:01:31 +02:00
tronikos 04f00d7010 Bump opower to 0.15.3 (#151179) 2025-08-26 11:52:18 +02:00
J. Diego Rodríguez Royo df9b0432b9 Bump aiohomeconnect to 0.19.0 (#151180) 2025-08-26 11:48:09 +02:00
hahn-th 47dbf923ed Bump to homematicip 2.3.0 (#151182) 2025-08-26 11:47:50 +02:00
epenet d0c5f291fc Add Tuya test fixtures (#151185) 2025-08-26 11:46:53 +02:00
J. Nick Koston 4b5ab472ad Bump qingping-ble to 1.0.1 (#151170) 2025-08-26 10:52:52 +02:00
Paulus Schoutsen ee9abd519d Default virtual environment location to .venv (#151181) 2025-08-26 09:44:04 +02:00
Erik Montnemery 1d2599184b Adjust entity disabled_by flag when moving entity to another config entry (#151151)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-08-25 23:35:28 +02:00
Marc Mueller b67d34d428 Update HAP-python to 5.0.0 (#151156) 2025-08-25 22:57:01 +02:00
hanwg b203a04f1b Add websocket command to rename config subentry (#150843)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
2025-08-25 22:55:01 +02:00
Erik Montnemery 50108e23ed Adjust device disabled_by flag when restoring a deleted device (#151154) 2025-08-25 22:49:47 +02:00
Erik Montnemery 58339d79d3 Revert "Fix entities/devices stuck in disabled state after config entry re-add" (#151158) 2025-08-25 22:48:30 +02:00
Erik Montnemery 05c8e8b4fd Adjust entity disabled_by flag when restoring a deleted entity (#151150) 2025-08-25 22:47:55 +02:00
Christopher Fenner ae676d6857 Bump PyViCare to 2.51.0 (#151153) 2025-08-25 22:43:50 +02:00
Michael Hansen db4d51e617 Bump hassil to 3.2.0 (#151168) 2025-08-25 21:39:11 +01:00
Glenn Vandeuren (aka Iondependent) bfe84ccd12 Add reconfigure flow to niko_home_control (#133993)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2025-08-25 22:03:05 +02:00
J. Nick Koston ede948c277 Fix HomeKit Controller entity state restore issues for IP/COAP devices (#151087) 2025-08-25 19:50:13 +02:00
G Johansson 28e8405622 Fix correct breaking version in stiebel_eltron (#151163) 2025-08-25 19:43:19 +02:00
Marc Mueller 202d285286 Update typing-extensions to 4.15.0 (#151157) 2025-08-25 18:33:59 +01:00
Marc Mueller c0b1536cd8 Fix hassfest requirements check (#151159) 2025-08-25 18:31:17 +02:00
Norbert Rittel f1d2b102cf Fix broken reference for "event_types" in template (#151152) 2025-08-25 15:57:04 +01:00
Ludovic BOUÉ 8b29e3011e Matter Valve new attributes (#150788) 2025-08-25 15:43:00 +02:00
Jan Bouwhuis dea5e7454a Add MQTT lock subentry support (#150860)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-08-25 15:37:36 +02:00
Petro31 58d4fd0b75 Add update platform to template integration (#150277) 2025-08-25 14:43:23 +02:00
epenet ca37bc1506 Add Tuya test fixtures (#151022) 2025-08-25 11:28:55 +02:00
karwosts 1f25641e3b Fix schedule entity can't delete icon (#150995) 2025-08-25 10:56:33 +02:00
Maikel Punie 962b276a77 Bump velbusaio to 2025.8.0 (#151133) 2025-08-25 10:41:22 +02:00
Yuxin Wang 0786e333ce Remove tests for setting up multiple integrations for APCUPSD (#151125) 2025-08-25 10:33:00 +02:00
Matthias Alphart d35271a214 Update knx-frontend to 2025.8.24.205840 (#151118) 2025-08-25 08:52:06 +02:00
Petro31 8fbf6dbd0b Add event platform to templates (#145408)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-08-25 07:50:22 +02:00
Retha Runolfsson 60d8353b10 Bump pyswitchbot to 0.69.0 (#151123) 2025-08-25 07:03:09 +02:00
Yuxin Wang 2d69d2ef1c Remove unnecessary tests for APCUPSD (#151126) 2025-08-25 07:00:13 +02:00
LG-ThinQ-Integration e1e11db3d2 Add switch for AC's air_clean, ventilator and washers to LG (#140842)
Co-authored-by: yunseon.park <yunseon.park@lge.com>
2025-08-25 06:42:14 +02:00
jan iversen 91ef5fb429 modbus: Sensor caused hanging call_later method (#151033) 2025-08-24 22:37:26 +02:00
Christian Fetzer 0dc2a5a02c Add m³/min as volume flow rate unit (#151051) 2025-08-24 20:30:25 +01:00
Joakim Plate b018465a4d Update togrill to 0.8.0 (#150945) 2025-08-24 20:07:55 +02:00
Joris Pelgröm e797d651b5 Bump letpot to 0.6.2 (#151094) 2025-08-24 18:08:30 +02:00
J. Nick Koston bc6f261105 Fix entities/devices stuck in disabled state after config entry re-add (#151075) 2025-08-24 15:55:58 +02:00
Marc Mueller 6c2ba15a73 Update lxml to 6.0.1 (#151093) 2025-08-24 15:16:41 +02:00
Simone Chemelli ef0712a785 Handle TypeError in Alexa Devices (#151088) 2025-08-24 15:08:51 +02:00
Tom 2bd45e4625 Bump airos to 0.4.3 (#151042) 2025-08-24 13:04:41 +02:00
J. Nick Koston 03ca164fb3 Switch to August OAuth with official API (#151080) 2025-08-24 05:29:53 +02:00
J. Nick Koston 3c11f8e50e Bump yalexs to 8.12.0 (#151079) 2025-08-24 05:04:40 +02:00
Andrea Turri f0e5325510 Provide elapsed time sensor consistent in Miele (#145093)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-08-23 20:50:19 +02:00
Yevhenii Vaskivskyi 4ff2da7553 Bump asusrouter to 1.20.0 (#151067) 2025-08-23 20:45:20 +02:00
LG-ThinQ-Integration af951ff0d4 Modified to use built-in fan modes for "mid". (#150927)
Co-authored-by: yunseon.park <yunseon.park@lge.com>
2025-08-23 13:24:10 +02:00
Matthias Alphart 207e2f61ea Fix KNX strings for UI entity creation (#151053) 2025-08-22 23:43:05 +02:00
Manu 9e204cd347 Bump habiticalib to v0.4.3 (#151050) 2025-08-22 21:08:13 +02:00
dependabot[bot] 97a7e46579 Bump github/codeql-action from 3.29.10 to 3.29.11 (#151020)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-22 20:58:10 +02:00
Andrew Jackson 8175f4ba65 Bump mastodon to 2.1.2 (#150948) 2025-08-22 19:35:11 +02:00
Andre Lengwenus 22e307a48a Bump lcn-frontend to 0.2.7 (#151041) 2025-08-22 19:34:09 +02:00
TimL 6c69b36b7b Bump pysmlight to v0.2.8 (#151036) 2025-08-22 15:06:23 +02:00
J. Diego Rodríguez Royo dd270f54fc Delete Home Connect deprecated actions (#150929) 2025-08-22 14:16:01 +02:00
Simone Chemelli 158fb35c5b Add account reconfigure to Alexa Devices config flow (#149637)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-08-22 13:52:01 +02:00
Matthias Alphart 85e5081fc3 Use serialized schema from backend in UI entity configuration (#149496) 2025-08-22 12:24:05 +02:00
Billy Rowell f6bb32c44b Add Tuya test fixtures for bzyd category (#150923)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-08-22 08:57:16 +02:00
Christopher Fenner e16eff6f28 Improve device details for Husqvarna BLE mower device (#150993) 2025-08-22 08:15:35 +02:00
Aidan Timson 2b80a621af Update aiolyric to 2.0.2 (#150998) 2025-08-22 08:12:49 +02:00
Aidan Timson 80d2e08572 Update aioazuredevops to 2.2.2 (#151000) 2025-08-22 06:55:13 +02:00
puddly e15166cbf3 Bump ZHA to 0.0.69 (#151010) 2025-08-22 06:49:38 +02:00
Paulus Schoutsen 3b1b095b47 2025.8.3 (#151008) 2025-08-21 20:22:43 +02:00
Matthias Alphart 72e78b6719 Allow nested translation schema for config panels (#149346) 2025-08-21 20:13:36 +02:00
J. Nick Koston a50b035479 Bump habluetooth to 5.1.0 and bleak-retry-connector to 4.3.0 (#150962) 2025-08-21 10:19:39 -05:00
Paulus Schoutsen bb4f8adffe Bump version to 2025.8.3 2025-08-21 15:13:51 +00:00
Bram Kragten 61a50e77cf Update frontend to 20250811.1 (#151005) 2025-08-21 15:13:37 +00:00
Simone Chemelli 82f94de0b8 Enable country site autodetection in Alexa Devices (#150989) 2025-08-21 15:13:36 +00:00
Erik Montnemery 71b2d46afd Except ujson from license check (#150980) 2025-08-21 15:13:35 +00:00
J. Nick Koston edc1989ff6 Bump onvif-zeep-async to 4.0.4 (#150969) 2025-08-21 15:12:19 +00:00
Martin Hjelmare 2dad6fa298 Ask user for Z-Wave RF region if country is missing (#150959)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2025-08-21 15:12:18 +00:00
elsi06 2f4e29ba71 Bump python-mystrom to 2.5.0 (#150947) 2025-08-21 15:12:17 +00:00
Imeon-Energy add75e06e3 Fix update retry for Imeon inverter integration (#150936)
Co-authored-by: TheBushBoy <theodavid@icloud.com>
2025-08-21 15:12:16 +00:00
Maciej Bieniek 9194ddd4fe Bump imgw-pib to version 1.5.4 (#150930) 2025-08-21 15:12:15 +00:00
J. Nick Koston 2e7821d64a Bump ESPHome minimum stable BLE version to 2025.8.0 (#150924) 2025-08-21 15:12:14 +00:00
Calvin Walton bb9660269c Matter valve Open command doesn't support TargetLevel=0 (#150922) 2025-08-21 15:12:13 +00:00
Paulus Schoutsen 1bd5aa0ab0 Fix structured output object selector conversion for OpenAI (#150916) 2025-08-21 15:12:12 +00:00
Tobias Sauerwein e4329ab8a5 update pyatmo to v9.2.3 (#150900) 2025-08-21 15:12:11 +00:00
J. Nick Koston 9414356a4d Bump bleak-retry-connector to 4.0.2 (#150899) 2025-08-21 15:12:10 +00:00
Paulus Schoutsen 0cd28e7fc1 Fix PWA theme color to match darker blue color scheme in 2025.8 (#150896)
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-21 15:12:09 +00:00
Joost Lekkerkerker 6383f9365c Bump pysmartthings to 3.2.9 (#150892) 2025-08-21 15:12:08 +00:00
epenet cb8669c84f Fix icloud service calls (#150881) 2025-08-21 15:12:07 +00:00
Stefan Agner d1698222f4 Add missing unsupported reasons to list (#150866) 2025-08-21 15:12:05 +00:00
Noah Husby a3f5c3f422 Bump aiorussound to 4.8.1 (#150858) 2025-08-21 15:12:04 +00:00
G Johansson 945771098e Bump holidays to 0.79 (#150857) 2025-08-21 15:12:03 +00:00
Imeon-Energy 59d73138e7 Use correct unit and class for the Imeon inverter sensors (#150847)
Co-authored-by: TheBushBoy <theodavid@icloud.com>
2025-08-21 15:12:02 +00:00
Martin Hjelmare fe71b54c3e Handle Z-Wave RssiErrorReceived (#150846) 2025-08-21 15:12:01 +00:00
LG-ThinQ-Integration 7639e12ff2 Initialize the coordinator's data to include data.options. (#150839)
Co-authored-by: yunseon.park <yunseon.park@lge.com>
2025-08-21 15:12:00 +00:00
Joost Lekkerkerker 4b2a149072 Bump yt-dlp to 2025.08.11 (#150821) 2025-08-21 15:11:59 +00:00
Joost Lekkerkerker 92b988a292 Abort Nanoleaf discovery flows with user flow (#150818) 2025-08-21 15:11:58 +00:00
Joost Lekkerkerker 1ca6c4b5b8 Include device data in Withings diagnostics (#150816) 2025-08-21 15:11:57 +00:00
tronikos 81377be92f Bump opower to 0.15.2 (#150809) 2025-08-21 15:11:56 +00:00
Joost Lekkerkerker 38aba81f62 Pin gql to 3.5.3 (#150800) 2025-08-21 15:10:38 +00:00
Simone Chemelli e175e3ed0b Enable country site autodetection in Alexa Devices (#150989) 2025-08-21 17:10:27 +02:00
Thomas D 27b32c5e93 Show charging power as 0 when not charging for the Volvo integration (#150797) 2025-08-21 15:08:25 +00:00
peteS-UK 332996cc38 Fix volume step error in Squeezebox media player (#150760) 2025-08-21 15:08:23 +00:00
Thomas Schamm 199b7e8ba7 Fix for bosch_shc: 'device_registry.async_get_or_create' referencing a non existing 'via_device' (#150756) 2025-08-21 15:08:23 +00:00
Thomas Schamm 122af46a92 Bump boschshcpy to 0.2.107 (#150754) 2025-08-21 15:08:21 +00:00
Marc Mueller 3dd091de44 Update hassfest package exceptions (#150744) 2025-08-21 15:08:20 +00:00
epenet 932c5ccf0f Bump renault-api to 0.4.0 (#150624) 2025-08-21 15:08:19 +00:00
JP-Ellis 4e52826664 fix(amberelectric): add request timeouts (#150613)
Signed-off-by: JP-Ellis <josh@jpellis.me>
2025-08-21 15:08:18 +00:00
markhannon c30d778a54 Bump to zcc-helper==3.6 (#150608) 2025-08-21 15:08:16 +00:00
Bram Kragten 7cd767e920 Update frontend to 20250811.1 (#151005) 2025-08-21 17:02:02 +02:00
Calvin Walton 4c39936b81 Matter valve Open command doesn't support TargetLevel=0 (#150922) 2025-08-21 16:19:14 +02:00
elsi06 d90590b228 Bump python-mystrom to 2.5.0 (#150947) 2025-08-21 08:48:03 -05:00
J. Nick Koston 7914e6b135 Bump onvif-zeep-async to 4.0.4 (#150969) 2025-08-21 08:47:14 -05:00
Martin Hjelmare 1d646d06a6 Ask user for Z-Wave RF region if country is missing (#150959)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2025-08-21 14:44:19 +02:00
LG-ThinQ-Integration 135c80d194 Initialize the coordinator's data to include data.options. (#150839)
Co-authored-by: yunseon.park <yunseon.park@lge.com>
2025-08-21 13:40:22 +02:00
Imeon-Energy aed853267a Fix update retry for Imeon inverter integration (#150936)
Co-authored-by: TheBushBoy <theodavid@icloud.com>
2025-08-21 13:31:06 +02:00
Erik Montnemery 62be31f899 Add test of automower_ble activity mapping (#150983)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-21 10:59:19 +02:00
Erik Montnemery ca75581d43 Bump automower-ble to 0.2.7 (#150979) 2025-08-21 10:59:10 +02:00
Erik Montnemery f8a8fb516b Except ujson from license check (#150980) 2025-08-21 10:33:37 +02:00
Andrea Turri f9575a3b2f Add "profile" extra attribute to Miele program sensor on coffee machines (#145073) 2025-08-21 10:19:13 +02:00
Erik Montnemery bbde98bc9f Bump pychromecast to 14.0.9 (#150939) 2025-08-21 10:18:29 +02:00
Paulus Schoutsen 90e8d74fcd Add HA version to device analytics (#150877) 2025-08-21 09:44:17 +02:00
dependabot[bot] 9489c19598 Bump github/codeql-action from 3.29.9 to 3.29.10 (#150913)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-21 09:42:36 +02:00
Erik Montnemery 210ec28f0a Fix bluetooth tests (#150978) 2025-08-21 09:38:42 +02:00
dependabot[bot] 88d853cfbd Bump codecov/codecov-action from 5.4.3 to 5.5.0 (#150977)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-21 09:26:35 +02:00
Yevhenii Vaskivskyi 069b21a5a5 Create a special cookie jar for aiohttp client session in asuswrt (#150973) 2025-08-21 06:35:12 +02:00
Ludovic BOUÉ da864ca034 Matter Refrigerator DoorOpen alarm (#150759)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-08-20 23:01:03 +02:00
Joakim Plate 8d6b7bf950 Fix event affecting multiple probes (#150954) 2025-08-20 20:40:16 +02:00
Maciej Bieniek cf8e7cfd28 Bump imgw-pib to version 1.5.4 (#150930) 2025-08-20 14:08:47 +03:00
Marc Mueller 8e5f9264b6 Fix togrill test warning (#150933) 2025-08-20 11:20:55 +02:00
J. Nick Koston 3d0ecf0585 Bump ESPHome minimum stable BLE version to 2025.8.0 (#150924) 2025-08-20 10:25:17 +03:00
JP-Ellis 19b2c6da23 fix(amberelectric): add request timeouts (#150613)
Signed-off-by: JP-Ellis <josh@jpellis.me>
2025-08-20 07:59:33 +02:00
Tobias Sauerwein 0b231ff042 update pyatmo to v9.2.3 (#150900) 2025-08-20 07:46:51 +02:00
Joost Lekkerkerker d8ae89be6a Bump pysmartthings to 3.2.9 (#150892) 2025-08-20 07:45:35 +02:00
Keilin Bickar e96ff77cbf Bump asyncsleepiq dependency to 1.6.0 (#150915) 2025-08-20 07:45:06 +02:00
Denis Shulyaka 9797d391af OpenAI external tools (#150599) 2025-08-19 23:36:23 +02:00
Paulus Schoutsen e68df66028 Fix structured output object selector conversion for OpenAI (#150916) 2025-08-19 22:17:30 +02:00
dependabot[bot] 8d30d69af5 Bump actions/dependency-review-action from 4.7.1 to 4.7.2 (#150904)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-19 19:41:37 +02:00
J. Nick Koston 93e9fab6c7 Bump bleak-retry-connector to 4.0.2 (#150899) 2025-08-19 19:21:37 +02:00
Jan Bouwhuis 48300f4563 Use greek small letter mu "\u03bc" instead of micro sign "\u00B5" for micro unit prefix (alt 1) (#144853)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-08-19 18:48:50 +02:00
Erik Montnemery 48091e5995 Improve test of WS command get_services (#150901)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-19 17:47:12 +02:00
Erik Montnemery 26582cecbd Improve test of REST endpoint /api/services (#150897) 2025-08-19 17:32:52 +02:00
jan iversen 7ecf32390c Modbus: Avoid duplicate updates. (#150895) 2025-08-19 17:15:51 +02:00
Luke Heckman cded163930 Update contributing guide links (#150159)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-08-19 17:15:31 +02:00
rossfoss 10fe479311 Add new attributes to Met Éireann (#150653)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-08-19 17:04:35 +02:00
Stefan Agner 65696f9b53 Bump aiohasupervisor from version 0.3.1 to version 0.3.2b0 (#150893) 2025-08-19 16:55:36 +02:00
Stefan Agner 08fc2ab03b Add missing unsupported reasons to list (#150866) 2025-08-19 16:55:16 +02:00
jan iversen 2290940638 Modbus: Remove unused variable. (#150894) 2025-08-19 16:54:05 +02:00
Matthias Alphart 63640af4d4 Update voluptuous-serialize to 2.7.0 (#150822) 2025-08-19 16:42:49 +02:00
Paulus Schoutsen f4400516b8 Fix PWA theme color to match darker blue color scheme in 2025.8 (#150896)
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-19 17:24:32 +03:00
Thomas D b52a806b36 Show charging power as 0 when not charging for the Volvo integration (#150797) 2025-08-19 16:23:30 +02:00
Paul Warren 76f3397aa0 Bump pyDaikin to 2.16.0 (#150867) 2025-08-19 16:12:53 +02:00
Retha Runolfsson 89abe65e1d Add air purifier for switchbot cloud integration (#147001) 2025-08-19 16:02:18 +02:00
jan iversen 785c9ebc3b Modbus: Retry primary connect. (#150853) 2025-08-19 15:03:05 +02:00
Joakim Plate a08be4fcb6 Add event entity to Togrill (#150812) 2025-08-19 15:01:57 +02:00
markhannon e8409e7c42 Bump to zcc-helper==3.6 (#150608) 2025-08-19 14:40:12 +02:00
epenet c46618cbd1 Bump renault-api to 0.4.0 (#150624) 2025-08-19 14:37:56 +02:00
Alexandre CUER 319e37384f Migrate Emoncms_history to external async library (#149824) 2025-08-19 14:32:13 +02:00
Imeon-Energy 4c1788e757 Add sensors to Imeon inverter integration (#146437)
Co-authored-by: TheBushBoy <theodavid@icloud.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-08-19 14:21:33 +02:00
epenet e6a158b1ac Add temperature sensor to Tuya solar inverters (#150878) 2025-08-19 14:20:29 +02:00
epenet 69dbcb0627 Add sound switch to Tuya fan light (#150879) 2025-08-19 14:20:05 +02:00
Andrew Jackson c62c52e8cf Bump mastodon.py to 2.1.1 (#150876) 2025-08-19 14:19:44 +02:00
epenet c18dc9b63b Fix icloud service calls (#150881) 2025-08-19 14:19:10 +02:00
Matrix 69757bed52 Support for YoLink YS4102 YS4103 (#150464) 2025-08-19 11:35:36 +02:00
epenet 899f0e03c1 Add Tuya test fixtures (#150835) 2025-08-19 11:34:25 +02:00
Imeon-Energy fc5e720764 Use correct unit and class for the Imeon inverter sensors (#150847)
Co-authored-by: TheBushBoy <theodavid@icloud.com>
2025-08-19 10:19:03 +02:00
Noah Husby 0c0e54b541 Bump aiorussound to 4.8.1 (#150858) 2025-08-19 09:45:42 +02:00
Retha Runolfsson 86e3eca57f Bump pyswitchbot to 0.68.4 (#150871) 2025-08-19 09:43:35 +02:00
Martin Hjelmare 15505cdd56 Handle Z-Wave RssiErrorReceived (#150846) 2025-08-18 22:14:52 +02:00
G Johansson c7001dcfc4 Bump holidays to 0.79 (#150857) 2025-08-18 21:40:43 +02:00
epenet 40feefc0fa Cleanup sw_version in Renault (#150844) 2025-08-18 22:28:58 +03:00
Marc Mueller f6d23b9b34 Check for forbidden files in dependencies with hassfest (#150772) 2025-08-18 17:44:31 +02:00
Andrew Jackson ab4aeb65f2 Bump mastodon.py to 2.1.0 and change quality scale (#150836) 2025-08-18 17:35:37 +02:00
Foscam-wangzhengyu 019c4ab874 Bump libpyfoscamcgi to 0.0.7 (#150829) 2025-08-18 17:34:00 +02:00
jan iversen 53ca369395 Do not start modbus update process until connection+delay. (#150796) 2025-08-18 17:20:41 +02:00
Manu 9910480980 Bump aiontfy to v0.5.4 (#150825) 2025-08-18 12:06:09 +02:00
Maciej Bieniek 7ecbe53b15 Bump brother to version 5.0.1 (#150840) 2025-08-18 12:05:10 +02:00
Maciej Bieniek 6baa162963 Bump brother to version 5.0.1 (#150840) 2025-08-18 12:04:52 +02:00
Ludovic BOUÉ 2f5561aeba Matter Custom Eve Weather trend (#147620) 2025-08-18 11:32:08 +02:00
Erik Montnemery 330bb46cf9 Revert "Bump automower-ble to 0.2.7" (#150833) 2025-08-18 10:07:57 +02:00
Joost Lekkerkerker 2f8ddae24d Include device data in Withings diagnostics (#150816) 2025-08-18 10:04:54 +02:00
Joost Lekkerkerker 419315d9cf Clean up freebox entity (#150695) 2025-08-18 09:47:02 +02:00
Joost Lekkerkerker 9138930cb9 Abort Nanoleaf discovery flows with user flow (#150818) 2025-08-18 09:41:37 +02:00
Joost Lekkerkerker a325596898 Bump yt-dlp to 2025.08.11 (#150821) 2025-08-18 09:39:08 +02:00
Claudio Ruggeri - CR-Tech 5fdb95e83c Fix Modbus issue 150453: correct transition update for climate without HVAC mode enabled (#150522)
Co-authored-by: jan iversen <jancasacondor@gmail.com>
2025-08-18 08:49:09 +02:00
Joost Lekkerkerker fcbfca52f3 Bump spotifyaio to 1.0.0 (#150820) 2025-08-18 08:28:58 +02:00
Yuxin Wang f44578f45f Add more exception types for cannot_connect test in APCUPSD (#150830) 2025-08-18 07:19:47 +02:00
Yuxin Wang 2b7bd923d6 Add a base entity to APCUPSD integration (#150828) 2025-08-18 07:18:08 +02:00
Joakim Plate 3ab4fd3035 Add number entity to togrill (#150609) 2025-08-17 23:48:53 +02:00
tronikos 794deaa5fd Bump opower to 0.15.2 (#150809) 2025-08-17 23:48:14 +02:00
Pete Sage 79bbae2fde Change the default name of the speech enhancement select for Sonos (#150815) 2025-08-17 23:41:51 +02:00
Franck Nijhof d7320f00ea 2025.8.2 (#150718) 2025-08-15 17:55:32 +02:00
Franck Nijhof 0bcc0f3fb9 Bump version to 2025.8.2 2025-08-15 15:22:30 +00:00
G Johansson c551a133c1 Improve handling decode errors in rest (#150699) 2025-08-15 14:43:43 +00:00
G Johansson 22e19e768e Fix missing labels for subdiv in workday (#150684) 2025-08-15 14:43:42 +00:00
Luke Lashley 0647222402 Bump python-snoo to 0.8.3 (#150670) 2025-08-15 14:43:41 +00:00
J. Nick Koston 83226ed015 Bump onvif-zeep-async to 4.0.3 (#150663) 2025-08-15 14:43:39 +00:00
J. Nick Koston 837472c12d Bump uiprotect to 7.21.1 (#150657) 2025-08-15 14:43:38 +00:00
Tom 0b337c7e2a Bump airOS to 0.2.11 (#150627) 2025-08-15 14:43:36 +00:00
Åke Strandberg 1a0b61c98e Bump pymiele to 0.5.4 (#150605) 2025-08-15 14:43:35 +00:00
Åke Strandberg 87a2d3e6d9 Bump pymiele to 0.5.3 (#150216) 2025-08-15 14:43:34 +00:00
Joakim Plate 2c1407f159 Make sure we update the api version in philips_js discovery (#150604) 2025-08-15 14:43:05 +00:00
Luke Lashley 1643d5df67 Change Snoo to use MQTT instead of PubNub (#150570) 2025-08-15 14:43:05 +00:00
Luke Lashley 5a49007b86 Bump python-snoo to 0.8.2 (#150569) 2025-08-15 14:43:04 +00:00
Robert Resch 312d8aaff5 Bump uv to 0.8.9 (#150542) 2025-08-15 14:43:04 +00:00
Luke Lashley 776726a053 Bump python-snoo to 0.8.1 (#150530) 2025-08-15 14:43:03 +00:00
J. Nick Koston 4213427b9c Bump aiodhcpwatcher to 1.2.1 (#150519) 2025-08-15 14:43:03 +00:00
J. Nick Koston 82907e5b88 Bump bleak-retry-connector to 4.0.1 (#150515) 2025-08-15 14:43:02 +00:00
Shay Levy 56b4c554de Bump aiowebostv to 0.7.5 (#150514) 2025-08-15 14:43:02 +00:00
Tom 82390f6f7b Bump airOS to 0.2.8 (#150504) 2025-08-15 14:43:01 +00:00
Åke Strandberg d9ebda4910 Add missing boost2 code for Miele hobs (#150481) 2025-08-15 14:43:01 +00:00
Martin Hjelmare 8f94657b0c Improve Z-Wave manual config flow step description (#150479) 2025-08-15 14:43:00 +00:00
Thomas D b0ab3cddb8 Fix re-auth flow for Volvo integration (#150478) 2025-08-15 14:43:00 +00:00
peteS-UK 3d4d57fa32 Additional Fix error on startup when no Apps or Radio plugins are installed for Squeezebox (#150475) 2025-08-15 14:43:00 +00:00
Matrix fed6f19edf Fix YoLink valve state when device running in class A mode (#150456) 2025-08-15 14:42:59 +00:00
Cyrill Raccaud 2725abf032 Bump cookidoo-api to 0.14.0 (#150450) 2025-08-15 14:42:59 +00:00
wedsa5 bd1b81493c Fix brightness command not sent when in white color mode (#150439)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-08-15 14:42:58 +00:00
Kevin David c58a188179 Bump python-snoo to 0.7.0 (#150434) 2025-08-15 14:42:58 +00:00
David ffbb7a2ab4 Fix error of the Powerfox integration in combination with the new Powerfox FLOW adapter (#150429)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-15 14:42:57 +00:00
Petro31 2ad470d172 Fix optimistic set to false for template entities (#150421) 2025-08-15 14:42:57 +00:00
Manu 8b2fce9c33 Bump habiticalib to version 0.4.2 (#150417) 2025-08-15 14:42:56 +00:00
HarvsG e22e7f1bcf Pi_hole - Account for auth succeeding when it shouldn't (#150413) 2025-08-15 14:42:56 +00:00
Michael Hansen b5bd61b20a Handle non-streaming TTS case correctly (#150218) 2025-08-15 14:42:55 +00:00
Arie Catsman 391c9a679e Fix enphase_envoy non existing via device warning at first config. (#149010)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-08-15 14:42:55 +00:00
epenet 8d49cb1195 Add pymodbus to package constraints (#150420) 2025-08-15 13:57:11 +02:00
Franck Nijhof dc8aaac6fb 2025.8.1 (#150412) 2025-08-11 21:06:26 +02:00
Franck Nijhof 1e87f0cab1 Revert "Update pystiebeleltron to 0.2.3 (#150339)"
This reverts commit 3158aa8891.
2025-08-11 18:29:46 +00:00
epenet 1aeced0fe6 Fix issue with Tuya suggested unit (#150414) 2025-08-11 20:25:24 +02:00
Franck Nijhof 7ed14f0afd Bump version to 2025.8.1 2025-08-11 12:15:36 +00:00
Martin Hjelmare dc5d159ffb Lower Z-Wave firmware check delay (#150411) 2025-08-11 12:14:57 +00:00
epenet 5fdd04b860 Handle empty electricity RAW sensors in Tuya (#150406) 2025-08-11 12:14:55 +00:00
Bram Kragten 6f5d72fd81 Update frontend to 20250811.0 (#150404) 2025-08-11 12:14:54 +00:00
Manuel Stahl 3158aa8891 Update pystiebeleltron to 0.2.3 (#150339)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-08-11 09:59:45 +00:00
Philipp Waller 0c74e22069 Update knx-frontend to 2025.8.9.63154 (#150323) 2025-08-11 09:59:44 +00:00
Tom a88549315c Bump airOS to 0.2.7 supporting firmware 8.7.11 (#150298) 2025-08-11 09:59:43 +00:00
steinmn fde548b825 Set suggested display precision on Volvo energy/fuel consumption sensors (#150296) 2025-08-11 09:59:42 +00:00
Norbert Rittel 91b10fb6d7 Remove misleading "the" from Launch Library configuration (#150288) 2025-08-11 09:59:41 +00:00
Thomas D 39f41fe17d Volvo: Skip unsupported API fields (#150285) 2025-08-11 09:59:39 +00:00
Denis Shulyaka 3d39fb08e5 Add GPT-5 support (#150281) 2025-08-11 09:59:38 +00:00
Thomas D a1731cd210 Volvo: fix distance to empty battery (#150278) 2025-08-11 09:59:37 +00:00
Robert Resch 0c31ec9bb6 Constraint num2words to 0.5.14 (#150276) 2025-08-11 09:58:56 +00:00
Thomas D 762c179b80 Volvo: fix missing charging power options (#150272) 2025-08-11 09:30:40 +00:00
peteS-UK 66019953db Fix error on startup when no Apps or Radio plugins are installed for Squeezebox (#150267) 2025-08-11 09:30:39 +00:00
peteS-UK a2931efeeb Fix handing for zero volume error in Squeezebox (#150265) 2025-08-11 09:30:38 +00:00
Martin Hjelmare 90c03f4115 Fix Tibber coordinator ContextVar warning (#150229) 2025-08-11 09:30:36 +00:00
Raphael Hehl 8afe3fed74 Handle Unifi Protect BadRequest exception during API key creation (#150223) 2025-08-11 09:30:35 +00:00
Martin Hjelmare 3ef332e168 Ignore MQTT vacuum battery warning (#150211) 2025-08-11 09:30:34 +00:00
puddly 8d821d9f98 Fix JSON serialization for ZHA diagnostics download (#150210) 2025-08-11 09:30:33 +00:00
Joakim Sørensen 23619fb2d3 Bump hass-nabucasa from 0.111.1 to 0.111.2 (#150209) 2025-08-11 09:30:31 +00:00
puddly bc70aeea85 Bump ZHA to 0.0.68 (#150208) 2025-08-11 09:30:30 +00:00
Martin Hjelmare beca01e857 Silence vacuum battery deprecation for built in integrations (#150204) 2025-08-11 09:30:29 +00:00
Manu 4765d9da92 Migrate unique_id only if monitor_id is present in Uptime Kuma (#150197) 2025-08-11 09:30:27 +00:00
Norbert Rittel 7951e822be Fix description of button.press action (#150181) 2025-08-11 09:30:26 +00:00
Maciej Bieniek c653bfff9f Bump imgw_pib to version 1.5.3 (#150178) 2025-08-11 09:30:25 +00:00
Marco Gasparini 2223bdb48e Fix Progettihwsw config flow (#150149) 2025-08-11 09:30:23 +00:00
Thomas55555 42a3bef34a Handle HusqvarnaWSClientError (#150145) 2025-08-11 09:30:22 +00:00
Tom 6f4d405b26 Bump airOS to 0.2.6 improving device class matching more devices (#150134) 2025-08-11 09:30:21 +00:00
puddly 8edc5f0359 Bump ZHA to 0.0.67 (#150132) 2025-08-11 09:30:19 +00:00
Pete Sage efcffd1016 Fix dialog enhancement switch for Sonos Arc Ultra (#150116) 2025-08-11 09:30:18 +00:00
Stefan H. ee32992010 Fix Enigma2 startup hang (#149756) 2025-08-11 09:30:17 +00:00
epenet 319128043e Make Tuya complex type handling explicit (#149677) 2025-08-11 09:30:15 +00:00
Franck Nijhof f074d81c8b 2025.8.0 (#150115) 2025-08-06 19:53:07 +02:00
Franck Nijhof d791d66104 Bump version to 2025.8.0 2025-08-06 17:19:42 +00:00
Luca Angemi dd9bd50a7b Deprecate Roborock battery feature (#150126) 2025-08-06 15:35:24 +00:00
Åke Strandberg 6243517271 Improve miele climate test coverage (#149859) 2025-08-06 15:20:37 +00:00
Franck Nijhof ad8ff7570d Bump version to 2025.8.0b5 2025-08-06 13:34:19 +00:00
Bram Kragten c4c14bee36 Update frontend to 20250806.0 (#150106)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-08-06 13:34:10 +00:00
Joost Lekkerkerker 2cf5badc17 Enable disabled Ollama config entries after entry migration (#150105) 2025-08-06 13:34:09 +00:00
G Johansson 0478f43b4b Bump holidays to 0.78 (#150103) 2025-08-06 13:34:08 +00:00
Martin Hjelmare d18f6273a8 Fix update coordinator ContextVar log for custom integrations (#150100) 2025-08-06 13:34:07 +00:00
David Poll 94bade0202 Fix zero-argument functions with as_function (#150062) 2025-08-06 13:34:06 +00:00
Franck Nijhof 855e8b08e9 Bump version to 2025.8.0b4 2025-08-06 11:26:23 +00:00
Joost Lekkerkerker 9820956b46 Enable disabled OpenAI config entries after entry migration (#150099) 2025-08-06 11:26:10 +00:00
Joost Lekkerkerker 1693299652 Enable disabled Anthropic config entries after entry migration (#150098) 2025-08-06 11:26:09 +00:00
starkillerOG 75200a9426 Reduce Reolink fimware polling from 12h to 24h (#150095) 2025-08-06 11:24:58 +00:00
Robert Resch fa587cec38 Fix hassio tests by only mocking supervisor id (#150093) 2025-08-06 11:24:57 +00:00
epenet 47946d0103 Add Tuya debug logging for new devices (#150091)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-06 11:24:56 +00:00
epenet d2586ca4ff Remove tuya vacuum battery level attribute (#150086) 2025-08-06 11:23:14 +00:00
Philipp Waller 4e21ef5fbc Update knx-frontend to 2025.8.6.52906 (#150085) 2025-08-06 10:02:44 +00:00
Joakim Sørensen a9998b41a5 Bump hass-nabucasa from 0.111.0 to 0.111.1 (#150082) 2025-08-06 10:01:07 +00:00
puddly 0a72f31504 Bump ZHA to 0.0.66 (#150081) 2025-08-06 10:00:41 +00:00
Retha Runolfsson f3a50c176d Bump pyswitchbot to 0.68.3 (#150080) 2025-08-06 10:00:39 +00:00
J. Nick Koston b6b422775a Bump habluetooth to 4.0.2 (#150078)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-08-06 09:58:55 +00:00
J. Nick Koston 00baecd01e Bump yalexs to 8.11.1 (#150073) 2025-08-06 09:57:46 +00:00
Pete Sage b370b7a7f6 Bump soco to 0.30.11 (#150072) 2025-08-06 09:57:45 +00:00
Robert Svensson baa2d751e4 Bump axis to v65 (#150065) 2025-08-06 09:57:43 +00:00
Martin Hjelmare c8d54fcffc Remove matter vacuum battery level attribute (#150061) 2025-08-06 09:57:42 +00:00
karwosts 80e3655bac Fix template sensor uom string (#150057) 2025-08-06 09:57:41 +00:00
starkillerOG e5b0a366fe Bump reolink-aio to 0.14.6 (#150055) 2025-08-06 09:57:40 +00:00
Joost Lekkerkerker 20e78a15b4 Change AI task strings (#150051) 2025-08-06 09:57:39 +00:00
Bram Kragten 9d806aef88 Update frontend to 20250805.0 (#150049) 2025-08-06 09:57:38 +00:00
Andrew Jackson 7e16973166 Default to zero quantity on new todo items in Mealie (#150047) 2025-08-06 09:57:36 +00:00
Martin Hjelmare e5f776fdc3 Improve downloader service (#150046)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-08-06 09:57:35 +00:00
Joost Lekkerkerker 83ccdb35f1 Ignore vacuum entities that properly deprecate battery (#150043) 2025-08-06 09:57:34 +00:00
Stefan Agner 52984f2fd1 Add missing translations for unhealthy Supervisor issues (#150036) 2025-08-06 09:57:33 +00:00
Jan Bouwhuis a548e13da5 Deprecate MQTT vacuum battery feature and remove it as default feature (#149877)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-08-06 09:57:32 +00:00
tronikos 55301a50b2 Fix PG&E and Duquesne Light Company in Opower (#149658)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-08-06 09:57:30 +00:00
Franck Nijhof 808273962d Bump version to 2025.8.0b3 2025-08-05 12:01:54 +00:00
Martin Hjelmare 094fe43557 Fix Z-Wave duplicate provisioned device (#150008) 2025-08-05 11:56:00 +00:00
Michael Hansen 8f5bd51eef Bump wyoming to 1.7.2 (#150007) 2025-08-05 11:55:59 +00:00
Thomas55555 faf0ded854 Bump aioautomower to 2.1.2 (#150003) 2025-08-05 11:55:58 +00:00
Matthias Alphart d20302f97b Update knx-frontend to 2025.8.4.154919 (#149991) 2025-08-05 11:55:57 +00:00
Grzegorz M 74c25496bc Bump icalendar from 6.1.0 to 6.3.1 for CalDav (#149990) 2025-08-05 11:55:56 +00:00
Petro31 67ecea0778 Create battery_level deprecation repair for template vacuum platform (#149987)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-08-05 11:55:55 +00:00
Robert Resch 164e5871cb Bump deebot-client to 13.6.0 (#149983) 2025-08-05 11:55:53 +00:00
Joakim Sørensen 7a9966120e Bump hass-nabucasa from 0.110.1 to 0.111.0 (#149977) 2025-08-05 11:55:52 +00:00
Martin Hjelmare d810b4ca38 Bump zwave-js-server-python to 0.67.1 (#149972) 2025-08-05 11:55:51 +00:00
epenet 896062d669 Fix Tuya fan speeds with numeric values (#149971) 2025-08-05 11:54:02 +00:00
epenet 03bd133577 Rename Tuya fixture files (#149927) 2025-08-05 11:47:15 +00:00
Martin Hjelmare 4596c1644b Direct migrations with Z-Wave JS UI to docs (#149966) 2025-08-05 11:43:12 +00:00
Petro31 778fe96eb6 Fix optimistic covers (#149962) 2025-08-05 11:43:11 +00:00
Joost Lekkerkerker a06557ed54 Pass config entry to Remote Calendar coordinator (#149958) 2025-08-05 11:43:10 +00:00
Joakim Sørensen 641621d184 Bump hass-nabucasa from 0.110.0 to 0.110.1 (#149956) 2025-08-05 11:43:09 +00:00
Joost Lekkerkerker b163f2b855 Pass config entry to SMS coordinator (#149955) 2025-08-05 11:43:08 +00:00
Joost Lekkerkerker 0c0604e5bd Pass config entry to Fronius coordinator (#149954) 2025-08-05 11:43:07 +00:00
Joost Lekkerkerker e0e4fc8afb Pass config entry to AsusWRT coordinator (#149953) 2025-08-05 11:43:05 +00:00
Joost Lekkerkerker f832a2844f Pass config entry to Unifi coordinator (#149952) 2025-08-05 11:43:04 +00:00
Erik Montnemery 4b0b268227 Fix DeviceEntry.suggested_area deprecation warning (#149951) 2025-08-05 11:43:03 +00:00
Joost Lekkerkerker dfc16d9f15 Pass config entry to Broadlink coordinator (#149949) 2025-08-05 11:43:02 +00:00
Joost Lekkerkerker 4e3309bd22 Pass config entry to Snoo coordinator (#149947) 2025-08-05 11:43:01 +00:00
Joost Lekkerkerker a5a45ce59f Pass config entry to Smarttub coordinator (#149946) 2025-08-05 11:43:00 +00:00
Joost Lekkerkerker 6cb48da2f3 Pass config entry to Meteo France coordinator (#149945) 2025-08-05 11:42:59 +00:00
Joost Lekkerkerker ab5aac47b2 Pass config entry to Kraken coordinator (#149944) 2025-08-05 11:42:58 +00:00
Joost Lekkerkerker d50b9405f0 Pass config entry to Simplisafe coordinator (#149943) 2025-08-05 11:42:57 +00:00
Joost Lekkerkerker a2722f08c4 Pass config entry to Mill coordinator (#149942) 2025-08-05 11:42:56 +00:00
Joost Lekkerkerker aa700c3982 Pass config entry to hue coordinator (#149941) 2025-08-05 11:42:55 +00:00
Ståle Storø Hauknes 3b1bb41129 Airthings ContextVar warning (#149930) 2025-08-05 11:42:54 +00:00
Brett Adams 79ef51fb07 Fix credit sensor when there are no vehicles in Teslemetry (#149925) 2025-08-05 11:34:16 +00:00
andreimoraru 53769da55e Bump yt-dlp to 2025.07.21 (#149916)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-08-05 11:34:15 +00:00
Thomas55555 82d153a240 Fix options for error sensor in Husqvarna Automower (#149901) 2025-08-05 11:34:13 +00:00
Andrew Jackson 0dac635478 Bump aiomealie to 0.10.1 (#149890) 2025-08-05 11:34:12 +00:00
Tom 90fc7d314b Bump python-airos to 0.2.4 (#149885) 2025-08-05 11:34:10 +00:00
Mike Degatano 636c1b7e4f Add translation strings for unsupported OS version (#149837) 2025-08-05 11:34:09 +00:00
Christopher Fenner 49c23de2d2 Update sensor icons in Volvo integration (#149811) 2025-08-05 11:34:08 +00:00
Ludovic BOUÉ e48820b2c1 Matter pump setpoint CurrentLevel limit (#149689) 2025-08-05 11:34:06 +00:00
Bram Kragten 2b7a434677 Bump version to 2025.8.0b2 2025-08-04 10:37:10 +02:00
puddly 9ef7c6c99a Bump ZHA to 0.0.65 (#149922) 2025-08-04 10:37:07 +02:00
J. Nick Koston b789c11217 Bump dbus-fast to 2.44.3 (#149921) 2025-08-04 10:37:06 +02:00
J. Nick Koston 5e8cd19cc3 Bump aiodiscover to 2.7.1 (#149920) 2025-08-04 10:37:05 +02:00
J. Nick Koston 027052440d Bump yalexs-ble to 3.1.2 (#149917) 2025-08-04 10:37:04 +02:00
Maciej Bieniek 47a7ed4084 Bump imgw_pib to version 1.5.2 (#149892) 2025-08-04 10:37:03 +02:00
Martin Hjelmare 89f6cfeb81 Fix Z-Wave handling of driver ready event (#149879) 2025-08-04 10:37:02 +02:00
Joost Lekkerkerker c268e57ba7 Bump python-open-router to 0.3.1 (#149873) 2025-08-04 10:37:01 +02:00
Andrea Turri 138c19126b Fix Miele hob translation keys (#149865) 2025-08-04 10:37:01 +02:00
Oliver c459ceba73 Update denonavr to 1.1.2 (#149842) 2025-08-04 10:37:00 +02:00
Martin Hjelmare 8d0ceff652 Fix Z-Wave config entry state conditions in listen task (#149841) 2025-08-04 10:36:59 +02:00
peteS-UK 1d383e80a4 Fix initialisation of Apps and Radios list for Squeezebox (#149834) 2025-08-04 10:36:58 +02:00
Norbert Rittel 6a17a12be5 Update reference for volatile_organic_compounds_parts in template (#149831) 2025-08-04 10:36:57 +02:00
Norbert Rittel 3a8d962d34 Add translation for absolute_humidity device class to mqtt (#149818) 2025-08-04 10:36:56 +02:00
Norbert Rittel 7e5cf17cf4 Add translation for absolute_humidity device class to random (#149815) 2025-08-04 10:36:55 +02:00
Norbert Rittel 214940d04f Add translation for absolute_humidity device class to template (#149814) 2025-08-04 10:36:54 +02:00
Thomas D 6877fdaf5b Add scopes in config flow auth request for Volvo integration (#149813) 2025-08-04 10:36:53 +02:00
Norbert Rittel 35d0c254a2 Fix descriptions for template number fields (#149804) 2025-08-04 10:36:53 +02:00
epenet 9649fbc189 Fix tuya light supported color modes (#149793)
Co-authored-by: Erik <erik@montnemery.com>
2025-08-04 10:36:52 +02:00
Jamin b60b1fc0c6 Bump VoIP utils to 0.3.4 (#149786) 2025-08-04 10:36:51 +02:00
Manu 6b93f6d75c Hide configuration URL when Uptime Kuma is installed locally (#149781) 2025-08-04 10:36:50 +02:00
starkillerOG c8069a383e Bump motionblinds to 0.6.30 (#149764) 2025-08-04 10:36:49 +02:00
Nathan Spencer 6857e87b30 Bump pylitterbot to 2024.2.3 (#149763) 2025-08-04 10:36:48 +02:00
J. Nick Koston a095631f4f Bump aioesphomeapi to 37.2.2 (#149755) 2025-08-04 10:36:48 +02:00
Copilot c59fbdeec1 Fix ZHA ContextVar deprecation by passing config_entry (#149748)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joostlek <7083755+joostlek@users.noreply.github.com>
Co-authored-by: puddly <32534428+puddly@users.noreply.github.com>
Co-authored-by: TheJulianJES <6409465+TheJulianJES@users.noreply.github.com>
2025-08-04 10:36:47 +02:00
Erik Montnemery b521b1e64c Make device suggested_area only influence new devices (#149758)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-08-04 10:34:42 +02:00
Erik Montnemery 073589ae19 Deprecate DeviceEntry.suggested_area (#149730) 2025-08-04 10:34:38 +02:00
Erik Montnemery 9435b0ad3a Fix flaky velbus test (#149743) 2025-08-04 10:34:15 +02:00
karwosts 1662d36125 Fix add_suggested_values_to_schema when the schema has sections (#149718)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-08-04 10:14:30 +02:00
Erik Montnemery 70e54fdadd Improve test of FlowHandler.add_suggested_values_to_schema (#149759) 2025-08-04 10:13:44 +02:00
Tom 38d0ebb8ba Add diagnostics to UISP AirOS (#149631) 2025-08-04 10:08:19 +02:00
Bram Kragten 15cb48badb Bump version to 2025.8.0b1 2025-07-31 19:01:26 +02:00
Erik Montnemery 22214e8d31 Fix kitchen_sink option flow (#149760) 2025-07-31 19:01:02 +02:00
Bram Kragten fc04e0b2cc Update frontend to 20250731.0 (#149757) 2025-07-31 19:01:01 +02:00
Petro31 3fc6ebdb43 Fix unique_id in config validation for legacy weather platform (#149742) 2025-07-31 19:01:00 +02:00
Petro31 3ccb7deb3c Nitpick default translations for template integration (#149740) 2025-07-31 19:00:59 +02:00
Erik Montnemery f5f63b914a Make _EventDeviceRegistryUpdatedData_Remove JSON serializable (#149734) 2025-07-31 19:00:58 +02:00
J. Nick Koston bd0a3f5a5d Bump aioesphomeapi to 37.2.0 (#149732) 2025-07-31 19:00:58 +02:00
J. Nick Koston ab9eebd092 Bump aioesphomeapi to 37.1.6 (#149715) 2025-07-31 19:00:57 +02:00
J. Nick Koston 68c43099d9 Fix ESPHome unnecessary probing on DHCP discovery (#149713) 2025-07-31 19:00:56 +02:00
Åke Strandberg 041c417164 Fix bug when interpreting miele action response (#149710) 2025-07-31 19:00:55 +02:00
Andrea Turri 537d09c697 Fix Miele induction hob empty state (#149706) 2025-07-31 19:00:54 +02:00
Roman Sivriver 21e3b8da92 Fix typo in backup log message (#149705) 2025-07-31 19:00:53 +02:00
Jan Bouwhuis d390681360 Fix inconsistent use of the term 'target' and a typo in MQTT translation strings (#149703) 2025-07-31 19:00:53 +02:00
Åke Strandberg 918ec78348 Add missing translations for miele dishwasher (#149702) 2025-07-31 19:00:52 +02:00
starkillerOG 1deae3ee1a Bump reolink-aio to 0.14.5 (#149700) 2025-07-31 19:00:51 +02:00
Petro31 59eace67df Add translations for all fields in template integration (#149692)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-07-31 19:00:50 +02:00
Åke Strandberg 7eb7c66e3f Explicitly pass config_entry to miele coordinator (#149691) 2025-07-31 19:00:49 +02:00
Copilot aa2941592d Fix ContextVar deprecation warning in homeassistant_hardware integration (#149687)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joostlek <7083755+joostlek@users.noreply.github.com>
Co-authored-by: mib1185 <35783820+mib1185@users.noreply.github.com>
2025-07-31 19:00:48 +02:00
Manu 29daf136d2 Fix KeyError in friends coordinator (#149684) 2025-07-31 19:00:47 +02:00
puddly 3da3cf7f52 Bump ZHA to 0.0.64 (#149683)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
Co-authored-by: abmantis <amfcalt@gmail.com>
2025-07-31 19:00:47 +02:00
Michael Hansen d8c93d54d5 Bump intents to 2025.7.30 (#149678) 2025-07-31 19:00:46 +02:00
Jan Bouwhuis 0799ee9fba Fix translation string reference for MQTT climate subentry option (#149673) 2025-07-31 19:00:45 +02:00
Bram Kragten 9d31403984 2025.8.0b0 (#149675) 2025-07-30 17:13:15 +02:00
Bram Kragten 02f87cba9b Merge branch 'dev' into dev-rc 2025-07-30 17:07:48 +02:00
Bram Kragten 5b54784378 Bump version to 2025.8.0b0 2025-07-30 16:56:55 +02:00
811 changed files with 48536 additions and 9121 deletions
+2 -1
View File
@@ -14,7 +14,8 @@ tests
# Other virtualization methods
venv
.venv
.vagrant
# Temporary files
**/__pycache__
**/__pycache__
+16 -3
View File
@@ -1073,7 +1073,11 @@ async def test_flow_connection_error(hass, mock_api_error):
### Entity Testing Patterns
```python
@pytest.mark.parametrize("init_integration", [Platform.SENSOR], indirect=True)
@pytest.fixture
def platforms() -> list[Platform]:
"""Overridden fixture to specify platforms to test."""
return [Platform.SENSOR] # Or another specific platform as needed.
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
async def test_entities(
hass: HomeAssistant,
@@ -1120,16 +1124,25 @@ def mock_device_api() -> Generator[MagicMock]:
)
yield api
@pytest.fixture
def platforms() -> list[Platform]:
"""Fixture to specify platforms to test."""
return PLATFORMS
@pytest.fixture
async def init_integration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_device_api: MagicMock,
platforms: list[Platform],
) -> MockConfigEntry:
"""Set up the integration for testing."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
with patch("homeassistant.components.my_integration.PLATFORMS", platforms):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
return mock_config_entry
```
+4 -4
View File
@@ -37,7 +37,7 @@ on:
type: boolean
env:
CACHE_VERSION: 5
CACHE_VERSION: 7
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.9"
@@ -653,7 +653,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v5.0.0
- name: Dependency review
uses: actions/dependency-review-action@v4.7.1
uses: actions/dependency-review-action@v4.7.3
with:
license-check: false # We use our own license audit checks
@@ -1341,7 +1341,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@v5.4.3
uses: codecov/codecov-action@v5.5.0
with:
fail_ci_if_error: true
flags: full-suite
@@ -1491,7 +1491,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@v5.4.3
uses: codecov/codecov-action@v5.5.0
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
+2 -2
View File
@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@v5.0.0
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.29.9
uses: github/codeql-action/init@v3.29.11
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.29.9
uses: github/codeql-action/analyze@v3.29.11
with:
category: "/language:python"
@@ -231,7 +231,7 @@ jobs:
- name: Detect duplicates using AI
id: ai_detection
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
uses: actions/ai-inference@v2.0.0
uses: actions/ai-inference@v2.0.1
with:
model: openai/gpt-4o
system-prompt: |
@@ -57,7 +57,7 @@ jobs:
- name: Detect language using AI
id: ai_language_detection
if: steps.detect_language.outputs.should_continue == 'true'
uses: actions/ai-inference@v2.0.0
uses: actions/ai-inference@v2.0.1
with:
model: openai/gpt-4o-mini
system-prompt: |
Generated
+8 -4
View File
@@ -87,6 +87,8 @@ build.json @home-assistant/supervisor
/tests/components/airzone/ @Noltari
/homeassistant/components/airzone_cloud/ @Noltari
/tests/components/airzone_cloud/ @Noltari
/homeassistant/components/aladdin_connect/ @swcloudgenie
/tests/components/aladdin_connect/ @swcloudgenie
/homeassistant/components/alarm_control_panel/ @home-assistant/core
/tests/components/alarm_control_panel/ @home-assistant/core
/homeassistant/components/alert/ @home-assistant/core @frenck
@@ -422,6 +424,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/emby/ @mezz64
/homeassistant/components/emoncms/ @borpin @alexandrecuer
/tests/components/emoncms/ @borpin @alexandrecuer
/homeassistant/components/emoncms_history/ @alexandrecuer
/tests/components/emoncms_history/ @alexandrecuer
/homeassistant/components/emonitor/ @bdraco
/tests/components/emonitor/ @bdraco
/homeassistant/components/emulated_hue/ @bdraco @Tho85
@@ -1181,6 +1185,8 @@ build.json @home-assistant/supervisor
/tests/components/plum_lightpad/ @ColinHarrington @prystupa
/homeassistant/components/point/ @fredrike
/tests/components/point/ @fredrike
/homeassistant/components/pooldose/ @lmaertin
/tests/components/pooldose/ @lmaertin
/homeassistant/components/poolsense/ @haemishkyd
/tests/components/poolsense/ @haemishkyd
/homeassistant/components/powerfox/ @klaasnicolaas
@@ -1684,8 +1690,8 @@ build.json @home-assistant/supervisor
/tests/components/vegehub/ @ghowevege
/homeassistant/components/velbus/ @Cereal2nd @brefra
/tests/components/velbus/ @Cereal2nd @brefra
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio
/tests/components/velux/ @Julius2342 @DeerMaximum @pawlizio
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
/tests/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
/homeassistant/components/venstar/ @garbled1 @jhollowe
/tests/components/venstar/ @garbled1 @jhollowe
/homeassistant/components/versasense/ @imstevenxyz
@@ -1712,8 +1718,6 @@ build.json @home-assistant/supervisor
/tests/components/volvo/ @thomasddn
/homeassistant/components/volvooncall/ @molobrakos
/tests/components/volvooncall/ @molobrakos
/homeassistant/components/vulcan/ @Antoni-Czaplicki
/tests/components/vulcan/ @Antoni-Czaplicki
/homeassistant/components/wake_on_lan/ @ntilley905
/tests/components/wake_on_lan/ @ntilley905
/homeassistant/components/wake_word/ @home-assistant/core @synesthesiam
+5 -2
View File
@@ -14,5 +14,8 @@ Still interested? Then you should take a peek at the [developer documentation](h
## Feature suggestions
If you want to suggest a new feature for Home Assistant (e.g., new integrations), please open a thread in our [Community Forum: Feature Requests](https://community.home-assistant.io/c/feature-requests).
We use [GitHub for tracking issues](https://github.com/home-assistant/core/issues), not for tracking feature requests.
If you want to suggest a new feature for Home Assistant (e.g. new integrations), please [start a discussion](https://github.com/orgs/home-assistant/discussions) on GitHub.
## Issue Tracker
If you want to report an issue, please [create an issue](https://github.com/home-assistant/core/issues) on GitHub.
+5 -5
View File
@@ -1,10 +1,10 @@
image: ghcr.io/home-assistant/{arch}-homeassistant
build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.05.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.05.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.05.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.05.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.05.0
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.09.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.09.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.09.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.09.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.09.0
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io
@@ -7,6 +7,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["accuweather"],
"requirements": ["accuweather==4.2.0"],
"requirements": ["accuweather==4.2.1"],
"single_config_entry": true
}
+68 -2
View File
@@ -3,8 +3,10 @@
import logging
from typing import Any
from aiohttp import web
import voluptuous as vol
from homeassistant.components.http import KEY_HASS, HomeAssistantView
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, CONF_DESCRIPTION, CONF_SELECTOR
from homeassistant.core import (
@@ -26,14 +28,24 @@ from .const import (
ATTR_STRUCTURE,
ATTR_TASK_NAME,
DATA_COMPONENT,
DATA_IMAGES,
DATA_PREFERENCES,
DOMAIN,
SERVICE_GENERATE_DATA,
SERVICE_GENERATE_IMAGE,
AITaskEntityFeature,
)
from .entity import AITaskEntity
from .http import async_setup as async_setup_http
from .task import GenDataTask, GenDataTaskResult, async_generate_data
from .task import (
GenDataTask,
GenDataTaskResult,
GenImageTask,
GenImageTaskResult,
ImageData,
async_generate_data,
async_generate_image,
)
__all__ = [
"DOMAIN",
@@ -41,7 +53,11 @@ __all__ = [
"AITaskEntityFeature",
"GenDataTask",
"GenDataTaskResult",
"GenImageTask",
"GenImageTaskResult",
"ImageData",
"async_generate_data",
"async_generate_image",
"async_setup",
"async_setup_entry",
"async_unload_entry",
@@ -78,8 +94,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
entity_component = EntityComponent[AITaskEntity](_LOGGER, DOMAIN, hass)
hass.data[DATA_COMPONENT] = entity_component
hass.data[DATA_PREFERENCES] = AITaskPreferences(hass)
hass.data[DATA_IMAGES] = {}
await hass.data[DATA_PREFERENCES].async_load()
async_setup_http(hass)
hass.http.register_view(ImageView)
hass.services.async_register(
DOMAIN,
SERVICE_GENERATE_DATA,
@@ -101,6 +119,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
supports_response=SupportsResponse.ONLY,
job_type=HassJobType.Coroutinefunction,
)
hass.services.async_register(
DOMAIN,
SERVICE_GENERATE_IMAGE,
async_service_generate_image,
schema=vol.Schema(
{
vol.Required(ATTR_TASK_NAME): cv.string,
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(ATTR_INSTRUCTIONS): cv.string,
vol.Optional(ATTR_ATTACHMENTS): vol.All(
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
),
}
),
supports_response=SupportsResponse.ONLY,
job_type=HassJobType.Coroutinefunction,
)
return True
@@ -115,11 +150,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_service_generate_data(call: ServiceCall) -> ServiceResponse:
"""Run the run task service."""
"""Run the data task service."""
result = await async_generate_data(hass=call.hass, **call.data)
return result.as_dict()
async def async_service_generate_image(call: ServiceCall) -> ServiceResponse:
"""Run the image task service."""
return await async_generate_image(hass=call.hass, **call.data)
class AITaskPreferences:
"""AI Task preferences."""
@@ -164,3 +204,29 @@ class AITaskPreferences:
def as_dict(self) -> dict[str, str | None]:
"""Get the current preferences."""
return {key: getattr(self, key) for key in self.KEYS}
class ImageView(HomeAssistantView):
"""View to generated images."""
url = f"/api/{DOMAIN}/images/{{filename}}"
name = f"api:{DOMAIN}/images"
requires_auth = False
async def get(
self,
request: web.Request,
filename: str,
) -> web.Response:
"""Serve image."""
hass = request.app[KEY_HASS]
image_storage = hass.data[DATA_IMAGES]
image_data = image_storage.get(filename)
if image_data is None:
raise web.HTTPNotFound
return web.Response(
body=image_data.data,
content_type=image_data.mime_type,
)
@@ -12,12 +12,18 @@ if TYPE_CHECKING:
from . import AITaskPreferences
from .entity import AITaskEntity
from .task import ImageData
DOMAIN = "ai_task"
DATA_COMPONENT: HassKey[EntityComponent[AITaskEntity]] = HassKey(DOMAIN)
DATA_PREFERENCES: HassKey[AITaskPreferences] = HassKey(f"{DOMAIN}_preferences")
DATA_IMAGES: HassKey[dict[str, ImageData]] = HassKey(f"{DOMAIN}_images")
IMAGE_EXPIRY_TIME = 60 * 60 # 1 hour
MAX_IMAGES = 20
SERVICE_GENERATE_DATA = "generate_data"
SERVICE_GENERATE_IMAGE = "generate_image"
ATTR_INSTRUCTIONS: Final = "instructions"
ATTR_TASK_NAME: Final = "task_name"
@@ -38,3 +44,6 @@ class AITaskEntityFeature(IntFlag):
SUPPORT_ATTACHMENTS = 2
"""Support attachments with generate data."""
GENERATE_IMAGE = 4
"""Generate images based on instructions."""
+22 -2
View File
@@ -18,7 +18,7 @@ from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.util import dt as dt_util
from .const import DEFAULT_SYSTEM_PROMPT, DOMAIN, AITaskEntityFeature
from .task import GenDataTask, GenDataTaskResult
from .task import GenDataTask, GenDataTaskResult, GenImageTask, GenImageTaskResult
class AITaskEntity(RestoreEntity):
@@ -57,7 +57,7 @@ class AITaskEntity(RestoreEntity):
async def _async_get_ai_task_chat_log(
self,
session: ChatSession,
task: GenDataTask,
task: GenDataTask | GenImageTask,
) -> AsyncGenerator[ChatLog]:
"""Context manager used to manage the ChatLog used during an AI Task."""
# pylint: disable-next=contextmanager-generator-missing-cleanup
@@ -104,3 +104,23 @@ class AITaskEntity(RestoreEntity):
) -> GenDataTaskResult:
"""Handle a gen data task."""
raise NotImplementedError
@final
async def internal_async_generate_image(
self,
session: ChatSession,
task: GenImageTask,
) -> GenImageTaskResult:
"""Run a gen image task."""
self.__last_activity = dt_util.utcnow().isoformat()
self.async_write_ha_state()
async with self._async_get_ai_task_chat_log(session, task) as chat_log:
return await self._async_generate_image(task, chat_log)
async def _async_generate_image(
self,
task: GenImageTask,
chat_log: ChatLog,
) -> GenImageTaskResult:
"""Handle a gen image task."""
raise NotImplementedError
@@ -1,7 +1,15 @@
{
"entity_component": {
"_": {
"default": "mdi:star-four-points"
}
},
"services": {
"generate_data": {
"service": "mdi:file-star-four-points-outline"
},
"generate_image": {
"service": "mdi:star-four-points-box-outline"
}
}
}
@@ -1,10 +1,10 @@
{
"domain": "ai_task",
"name": "AI Task",
"after_dependencies": ["camera"],
"after_dependencies": ["camera", "http"],
"codeowners": ["@home-assistant/core"],
"dependencies": ["conversation", "media_source"],
"documentation": "https://www.home-assistant.io/integrations/ai_task",
"integration_type": "system",
"integration_type": "entity",
"quality_scale": "internal"
}
@@ -0,0 +1,81 @@
"""Expose images as media sources."""
from __future__ import annotations
import logging
from homeassistant.components.media_player import BrowseError, MediaClass
from homeassistant.components.media_source import (
BrowseMediaSource,
MediaSource,
MediaSourceItem,
PlayMedia,
Unresolvable,
)
from homeassistant.core import HomeAssistant
from .const import DATA_IMAGES, DOMAIN
_LOGGER = logging.getLogger(__name__)
async def async_get_media_source(hass: HomeAssistant) -> ImageMediaSource:
"""Set up image media source."""
_LOGGER.debug("Setting up image media source")
return ImageMediaSource(hass)
class ImageMediaSource(MediaSource):
"""Provide images as media sources."""
name: str = "AI Generated Images"
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize ImageMediaSource."""
super().__init__(DOMAIN)
self.hass = hass
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
"""Resolve media to a url."""
image_storage = self.hass.data[DATA_IMAGES]
image = image_storage.get(item.identifier)
if image is None:
raise Unresolvable(f"Could not resolve media item: {item.identifier}")
return PlayMedia(f"/api/{DOMAIN}/images/{item.identifier}", image.mime_type)
async def async_browse_media(
self,
item: MediaSourceItem,
) -> BrowseMediaSource:
"""Return media."""
if item.identifier:
raise BrowseError("Unknown item")
image_storage = self.hass.data[DATA_IMAGES]
children = [
BrowseMediaSource(
domain=DOMAIN,
identifier=filename,
media_class=MediaClass.IMAGE,
media_content_type=image.mime_type,
title=image.title or filename,
can_play=True,
can_expand=False,
)
for filename, image in image_storage.items()
]
return BrowseMediaSource(
domain=DOMAIN,
identifier=None,
media_class=MediaClass.APP,
media_content_type="",
title="AI Generated Images",
can_play=False,
can_expand=True,
children_media_class=MediaClass.IMAGE,
children=children,
)
+27 -1
View File
@@ -20,7 +20,6 @@ generate_data:
supported_features:
- ai_task.AITaskEntityFeature.GENERATE_DATA
structure:
advanced: true
required: false
example: '{ "name": { "selector": { "text": }, "description": "Name of the user", "required": "True" } } }, "age": { "selector": { "number": }, "description": "Age of the user" } }'
selector:
@@ -31,3 +30,30 @@ generate_data:
media:
accept:
- "*"
generate_image:
fields:
task_name:
example: "picture of a dog"
required: true
selector:
text:
instructions:
example: "Generate a high quality square image of a dog on transparent background"
required: true
selector:
text:
multiline: true
entity_id:
required: true
selector:
entity:
filter:
domain: ai_task
supported_features:
- ai_task.AITaskEntityFeature.GENERATE_IMAGE
attachments:
required: false
selector:
media:
accept:
- "*"
@@ -25,6 +25,28 @@
"description": "List of files to attach for multi-modal AI analysis."
}
}
},
"generate_image": {
"name": "Generate image",
"description": "Uses AI to generate image.",
"fields": {
"task_name": {
"name": "Task name",
"description": "Name of the task."
},
"instructions": {
"name": "Instructions",
"description": "Instructions that explains the image to be generated."
},
"entity_id": {
"name": "Entity ID",
"description": "Entity ID to run the task on."
},
"attachments": {
"name": "Attachments",
"description": "List of files to attach for using as references."
}
}
}
}
}
+240 -48
View File
@@ -3,6 +3,8 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from functools import partial
import mimetypes
from pathlib import Path
import tempfile
@@ -11,11 +13,22 @@ from typing import Any
import voluptuous as vol
from homeassistant.components import camera, conversation, media_source
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant, ServiceResponse, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.chat_session import async_get_chat_session
from homeassistant.helpers.chat_session import ChatSession, async_get_chat_session
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.network import get_url
from homeassistant.util import RE_SANITIZE_FILENAME, slugify
from .const import DATA_COMPONENT, DATA_PREFERENCES, AITaskEntityFeature
from .const import (
DATA_COMPONENT,
DATA_IMAGES,
DATA_PREFERENCES,
DOMAIN,
IMAGE_EXPIRY_TIME,
MAX_IMAGES,
AITaskEntityFeature,
)
def _save_camera_snapshot(image: camera.Image) -> Path:
@@ -29,43 +42,15 @@ def _save_camera_snapshot(image: camera.Image) -> Path:
return Path(temp_file.name)
async def async_generate_data(
async def _resolve_attachments(
hass: HomeAssistant,
*,
task_name: str,
entity_id: str | None = None,
instructions: str,
structure: vol.Schema | None = None,
session: ChatSession,
attachments: list[dict] | None = None,
) -> GenDataTaskResult:
"""Run a task in the AI Task integration."""
if entity_id is None:
entity_id = hass.data[DATA_PREFERENCES].gen_data_entity_id
if entity_id is None:
raise HomeAssistantError("No entity_id provided and no preferred entity set")
entity = hass.data[DATA_COMPONENT].get_entity(entity_id)
if entity is None:
raise HomeAssistantError(f"AI Task entity {entity_id} not found")
if AITaskEntityFeature.GENERATE_DATA not in entity.supported_features:
raise HomeAssistantError(
f"AI Task entity {entity_id} does not support generating data"
)
# Resolve attachments
) -> list[conversation.Attachment]:
"""Resolve attachments for a task."""
resolved_attachments: list[conversation.Attachment] = []
created_files: list[Path] = []
if (
attachments
and AITaskEntityFeature.SUPPORT_ATTACHMENTS not in entity.supported_features
):
raise HomeAssistantError(
f"AI Task entity {entity_id} does not support attachments"
)
for attachment in attachments or []:
media_content_id = attachment["media_content_id"]
@@ -104,20 +89,59 @@ async def async_generate_data(
)
)
if not created_files:
return resolved_attachments
def cleanup_files() -> None:
"""Cleanup temporary files."""
for file in created_files:
file.unlink(missing_ok=True)
@callback
def cleanup_files_callback() -> None:
"""Cleanup temporary files."""
hass.async_add_executor_job(cleanup_files)
session.async_on_cleanup(cleanup_files_callback)
return resolved_attachments
async def async_generate_data(
hass: HomeAssistant,
*,
task_name: str,
entity_id: str | None = None,
instructions: str,
structure: vol.Schema | None = None,
attachments: list[dict] | None = None,
) -> GenDataTaskResult:
"""Run a data generation task in the AI Task integration."""
if entity_id is None:
entity_id = hass.data[DATA_PREFERENCES].gen_data_entity_id
if entity_id is None:
raise HomeAssistantError("No entity_id provided and no preferred entity set")
entity = hass.data[DATA_COMPONENT].get_entity(entity_id)
if entity is None:
raise HomeAssistantError(f"AI Task entity {entity_id} not found")
if AITaskEntityFeature.GENERATE_DATA not in entity.supported_features:
raise HomeAssistantError(
f"AI Task entity {entity_id} does not support generating data"
)
if (
attachments
and AITaskEntityFeature.SUPPORT_ATTACHMENTS not in entity.supported_features
):
raise HomeAssistantError(
f"AI Task entity {entity_id} does not support attachments"
)
with async_get_chat_session(hass) as session:
if created_files:
def cleanup_files() -> None:
"""Cleanup temporary files."""
for file in created_files:
file.unlink(missing_ok=True)
@callback
def cleanup_files_callback() -> None:
"""Cleanup temporary files."""
hass.async_add_executor_job(cleanup_files)
session.async_on_cleanup(cleanup_files_callback)
resolved_attachments = await _resolve_attachments(hass, session, attachments)
return await entity.internal_async_generate_data(
session,
@@ -130,6 +154,97 @@ async def async_generate_data(
)
def _cleanup_images(image_storage: dict[str, ImageData], num_to_remove: int) -> None:
"""Remove old images to keep the storage size under the limit."""
if num_to_remove <= 0:
return
if num_to_remove >= len(image_storage):
image_storage.clear()
return
sorted_images = sorted(
image_storage.items(),
key=lambda item: item[1].timestamp,
)
for filename, _ in sorted_images[:num_to_remove]:
image_storage.pop(filename, None)
async def async_generate_image(
hass: HomeAssistant,
*,
task_name: str,
entity_id: str,
instructions: str,
attachments: list[dict] | None = None,
) -> ServiceResponse:
"""Run an image generation task in the AI Task integration."""
entity = hass.data[DATA_COMPONENT].get_entity(entity_id)
if entity is None:
raise HomeAssistantError(f"AI Task entity {entity_id} not found")
if AITaskEntityFeature.GENERATE_IMAGE not in entity.supported_features:
raise HomeAssistantError(
f"AI Task entity {entity_id} does not support generating images"
)
if (
attachments
and AITaskEntityFeature.SUPPORT_ATTACHMENTS not in entity.supported_features
):
raise HomeAssistantError(
f"AI Task entity {entity_id} does not support attachments"
)
with async_get_chat_session(hass) as session:
resolved_attachments = await _resolve_attachments(hass, session, attachments)
task_result = await entity.internal_async_generate_image(
session,
GenImageTask(
name=task_name,
instructions=instructions,
attachments=resolved_attachments or None,
),
)
service_result = task_result.as_dict()
image_data = service_result.pop("image_data")
if service_result.get("revised_prompt") is None:
service_result["revised_prompt"] = instructions
image_storage = hass.data[DATA_IMAGES]
if len(image_storage) + 1 > MAX_IMAGES:
_cleanup_images(image_storage, len(image_storage) + 1 - MAX_IMAGES)
current_time = datetime.now()
ext = mimetypes.guess_extension(task_result.mime_type, False) or ".png"
sanitized_task_name = RE_SANITIZE_FILENAME.sub("", slugify(task_name))
filename = f"{current_time.strftime('%Y-%m-%d_%H%M%S')}_{sanitized_task_name}{ext}"
image_storage[filename] = ImageData(
data=image_data,
timestamp=int(current_time.timestamp()),
mime_type=task_result.mime_type,
title=service_result["revised_prompt"],
)
def _purge_image(filename: str, now: datetime) -> None:
"""Remove image from storage."""
image_storage.pop(filename, None)
if IMAGE_EXPIRY_TIME > 0:
async_call_later(hass, IMAGE_EXPIRY_TIME, partial(_purge_image, filename))
service_result["url"] = get_url(hass) + f"/api/{DOMAIN}/images/{filename}"
service_result["media_source_id"] = f"media-source://{DOMAIN}/images/{filename}"
return service_result
@dataclass(slots=True)
class GenDataTask:
"""Gen data task to be processed."""
@@ -167,3 +282,80 @@ class GenDataTaskResult:
"conversation_id": self.conversation_id,
"data": self.data,
}
@dataclass(slots=True)
class GenImageTask:
"""Gen image task to be processed."""
name: str
"""Name of the task."""
instructions: str
"""Instructions on what needs to be done."""
attachments: list[conversation.Attachment] | None = None
"""List of attachments to go along the instructions."""
def __str__(self) -> str:
"""Return task as a string."""
return f"<GenImageTask {self.name}: {id(self)}>"
@dataclass(slots=True)
class GenImageTaskResult:
"""Result of gen image task."""
image_data: bytes
"""Raw image data generated by the model."""
conversation_id: str
"""Unique identifier for the conversation."""
mime_type: str
"""MIME type of the generated image."""
width: int | None = None
"""Width of the generated image, if available."""
height: int | None = None
"""Height of the generated image, if available."""
model: str | None = None
"""Model used to generate the image, if available."""
revised_prompt: str | None = None
"""Revised prompt used to generate the image, if applicable."""
def as_dict(self) -> dict[str, Any]:
"""Return result as a dict."""
return {
"image_data": self.image_data,
"conversation_id": self.conversation_id,
"mime_type": self.mime_type,
"width": self.width,
"height": self.height,
"model": self.model,
"revised_prompt": self.revised_prompt,
}
@dataclass(slots=True)
class ImageData:
"""Image data for stored generated images."""
data: bytes
"""Raw image data."""
timestamp: int
"""Timestamp when the image was generated, as a Unix timestamp."""
mime_type: str
"""MIME type of the image."""
title: str
"""Title of the image, usually the prompt used to generate it."""
def __str__(self) -> str:
"""Return image data as a string."""
return f"<ImageData {self.title}: {id(self)}>"
@@ -61,7 +61,7 @@
"display_pm_standard": {
"name": "Display PM standard",
"state": {
"ugm3": "µg/m³",
"ugm3": "μg/m³",
"us_aqi": "US AQI"
}
},
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airos",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["airos==0.3.0"]
"requirements": ["airos==0.4.4"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_push",
"loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.7.1"]
"requirements": ["aioairzone-cloud==0.7.2"]
}
@@ -2,39 +2,96 @@
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from genie_partner_sdk.client import AladdinConnectClient
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers import (
aiohttp_client,
config_entry_oauth2_flow,
device_registry as dr,
)
DOMAIN = "aladdin_connect"
from . import api
from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION, DOMAIN
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
"""Set up Aladdin Connect from a config entry."""
ir.async_create_issue(
hass,
DOMAIN,
DOMAIN,
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="integration_removed",
translation_placeholders={
"entries": "/config/integrations/integration/aladdin_connect",
},
async def async_setup_entry(
hass: HomeAssistant, entry: AladdinConnectConfigEntry
) -> bool:
"""Set up Aladdin Connect Genie from a config entry."""
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
client = AladdinConnectClient(
api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session)
)
doors = await client.get_doors()
entry.runtime_data = {
door.unique_id: AladdinConnectCoordinator(hass, entry, client, door)
for door in doors
}
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
remove_stale_devices(hass, entry)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: AladdinConnectConfigEntry
) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_migrate_entry(
hass: HomeAssistant, config_entry: AladdinConnectConfigEntry
) -> bool:
"""Migrate old config."""
if config_entry.version < CONFIG_FLOW_VERSION:
config_entry.async_start_reauth(hass)
new_data = {**config_entry.data}
hass.config_entries.async_update_entry(
config_entry,
data=new_data,
version=CONFIG_FLOW_VERSION,
minor_version=CONFIG_FLOW_MINOR_VERSION,
)
return True
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Remove a config entry."""
if not hass.config_entries.async_loaded_entries(DOMAIN):
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
# Remove any remaining disabled or ignored entries
for _entry in hass.config_entries.async_entries(DOMAIN):
hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id))
def remove_stale_devices(
hass: HomeAssistant,
config_entry: AladdinConnectConfigEntry,
) -> None:
"""Remove stale devices from device registry."""
device_registry = dr.async_get(hass)
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry.entry_id
)
all_device_ids = set(config_entry.runtime_data)
for device_entry in device_entries:
device_id: str | None = None
for identifier in device_entry.identifiers:
if identifier[0] == DOMAIN:
device_id = identifier[1]
break
if device_id and device_id not in all_device_ids:
device_registry.async_update_device(
device_entry.id, remove_config_entry_id=config_entry.entry_id
)
@@ -0,0 +1,33 @@
"""API for Aladdin Connect Genie bound to Home Assistant OAuth."""
from typing import cast
from aiohttp import ClientSession
from genie_partner_sdk.auth import Auth
from homeassistant.helpers import config_entry_oauth2_flow
API_URL = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1"
API_KEY = "k6QaiQmcTm2zfaNns5L1Z8duBtJmhDOW8JawlCC3"
class AsyncConfigEntryAuth(Auth):
"""Provide Aladdin Connect Genie authentication tied to an OAuth2 based config entry."""
def __init__(
self,
websession: ClientSession,
oauth_session: config_entry_oauth2_flow.OAuth2Session,
) -> None:
"""Initialize Aladdin Connect Genie auth."""
super().__init__(
websession, API_URL, oauth_session.token["access_token"], API_KEY
)
self._oauth_session = oauth_session
async def async_get_access_token(self) -> str:
"""Return a valid access token."""
if not self._oauth_session.valid_token:
await self._oauth_session.async_ensure_token_valid()
return cast(str, self._oauth_session.token["access_token"])
@@ -0,0 +1,14 @@
"""application_credentials platform the Aladdin Connect Genie integration."""
from homeassistant.components.application_credentials import AuthorizationServer
from homeassistant.core import HomeAssistant
from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
"""Return authorization server."""
return AuthorizationServer(
authorize_url=OAUTH2_AUTHORIZE,
token_url=OAUTH2_TOKEN,
)
@@ -1,11 +1,63 @@
"""Config flow for Aladdin Connect integration."""
"""Config flow for Aladdin Connect Genie."""
from homeassistant.config_entries import ConfigFlow
from collections.abc import Mapping
import logging
from typing import Any
from . import DOMAIN
import jwt
import voluptuous as vol
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.helpers import config_entry_oauth2_flow
from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION, DOMAIN
class AladdinConnectConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Aladdin Connect."""
class OAuth2FlowHandler(
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
):
"""Config flow to handle Aladdin Connect Genie OAuth2 authentication."""
VERSION = 1
DOMAIN = DOMAIN
VERSION = CONFIG_FLOW_VERSION
MINOR_VERSION = CONFIG_FLOW_MINOR_VERSION
async def async_step_reauth(
self, user_input: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon API auth error or upgrade from v1 to v2."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: Mapping[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
if user_input is None:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema({}),
)
return await self.async_step_user()
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
"""Create an oauth config entry or update existing entry for reauth."""
# Extract the user ID from the JWT token's 'sub' field
token = jwt.decode(
data["token"]["access_token"], options={"verify_signature": False}
)
user_id = token["sub"]
await self.async_set_unique_id(user_id)
if self.source == SOURCE_REAUTH:
self._abort_if_unique_id_mismatch(reason="wrong_account")
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data=data
)
self._abort_if_unique_id_configured()
return self.async_create_entry(title="Aladdin Connect", data=data)
@property
def logger(self) -> logging.Logger:
"""Return logger."""
return logging.getLogger(__name__)
@@ -0,0 +1,14 @@
"""Constants for the Aladdin Connect Genie integration."""
from typing import Final
from homeassistant.components.cover import CoverEntityFeature
DOMAIN = "aladdin_connect"
CONFIG_FLOW_VERSION = 2
CONFIG_FLOW_MINOR_VERSION = 1
OAUTH2_AUTHORIZE = "https://app.aladdinconnect.com/login.html"
OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token"
SUPPORTED_FEATURES: Final = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
@@ -0,0 +1,50 @@
"""Coordinator for Aladdin Connect integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from genie_partner_sdk.client import AladdinConnectClient
from genie_partner_sdk.model import GarageDoor
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
type AladdinConnectConfigEntry = ConfigEntry[dict[str, AladdinConnectCoordinator]]
SCAN_INTERVAL = timedelta(seconds=15)
class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
"""Coordinator for Aladdin Connect integration."""
def __init__(
self,
hass: HomeAssistant,
entry: AladdinConnectConfigEntry,
client: AladdinConnectClient,
garage_door: GarageDoor,
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
logger=_LOGGER,
config_entry=entry,
name="Aladdin Connect Coordinator",
update_interval=SCAN_INTERVAL,
)
self.client = client
self.data = garage_door
async def _async_update_data(self) -> GarageDoor:
"""Fetch data from the Aladdin Connect API."""
await self.client.update_door(self.data.device_id, self.data.door_number)
self.data.status = self.client.get_door_status(
self.data.device_id, self.data.door_number
)
self.data.battery_level = self.client.get_battery_status(
self.data.device_id, self.data.door_number
)
return self.data
@@ -0,0 +1,64 @@
"""Cover Entity for Genie Garage Door."""
from __future__ import annotations
from typing import Any
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import SUPPORTED_FEATURES
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
from .entity import AladdinConnectEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: AladdinConnectConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the cover platform."""
coordinators = entry.runtime_data
async_add_entities(
AladdinCoverEntity(coordinator) for coordinator in coordinators.values()
)
class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
"""Representation of Aladdin Connect cover."""
_attr_device_class = CoverDeviceClass.GARAGE
_attr_supported_features = SUPPORTED_FEATURES
_attr_name = None
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
"""Initialize the Aladdin Connect cover."""
super().__init__(coordinator)
self._attr_unique_id = coordinator.data.unique_id
async def async_open_cover(self, **kwargs: Any) -> None:
"""Issue open command to cover."""
await self.client.open_door(self._device_id, self._number)
async def async_close_cover(self, **kwargs: Any) -> None:
"""Issue close command to cover."""
await self.client.close_door(self._device_id, self._number)
@property
def is_closed(self) -> bool | None:
"""Update is closed attribute."""
if (status := self.coordinator.data.status) is None:
return None
return status == "closed"
@property
def is_closing(self) -> bool | None:
"""Update is closing attribute."""
return self.coordinator.data.status == "closing"
@property
def is_opening(self) -> bool | None:
"""Update is opening attribute."""
return self.coordinator.data.status == "opening"
@@ -0,0 +1,32 @@
"""Base class for Aladdin Connect entities."""
from genie_partner_sdk.client import AladdinConnectClient
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import AladdinConnectCoordinator
class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]):
"""Defines a base Aladdin Connect entity."""
_attr_has_entity_name = True
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
"""Initialize Aladdin Connect entity."""
super().__init__(coordinator)
device = coordinator.data
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.unique_id)},
manufacturer="Aladdin Connect",
name=device.name,
)
self._device_id = device.device_id
self._number = device.door_number
@property
def client(self) -> AladdinConnectClient:
"""Return the client for this entity."""
return self.coordinator.client
@@ -1,9 +1,11 @@
{
"domain": "aladdin_connect",
"name": "Aladdin Connect",
"codeowners": [],
"codeowners": ["@swcloudgenie"],
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
"integration_type": "system",
"integration_type": "hub",
"iot_class": "cloud_polling",
"requirements": []
"requirements": ["genie-partner-sdk==1.0.11"]
}
@@ -0,0 +1,94 @@
rules:
# Bronze
action-setup:
status: exempt
comment: Integration does not register any service actions.
appropriate-polling: done
brands: done
common-modules: done
config-flow: done
config-flow-test-coverage: todo
dependency-transparency: done
docs-actions:
status: exempt
comment: Integration does not register any service actions.
docs-high-level-description: done
docs-installation-instructions:
status: todo
comment: Documentation needs to be created.
docs-removal-instructions:
status: todo
comment: Documentation needs to be created.
entity-event-setup:
status: exempt
comment: Integration does not subscribe to external events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure:
status: todo
comment: Config flow does not currently test connection during setup.
test-before-setup: todo
unique-config-entry: done
# Silver
action-exceptions: todo
config-entry-unloading: done
docs-configuration-parameters:
status: todo
comment: Documentation needs to be created.
docs-installation-parameters:
status: todo
comment: Documentation needs to be created.
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: done
test-coverage:
status: todo
comment: Platform tests for cover and sensor need to be implemented to reach 95% coverage.
# Gold
devices: done
diagnostics: todo
discovery: todo
discovery-update-info: todo
docs-data-update:
status: todo
comment: Documentation needs to be created.
docs-examples:
status: todo
comment: Documentation needs to be created.
docs-known-limitations:
status: todo
comment: Documentation needs to be created.
docs-supported-devices:
status: todo
comment: Documentation needs to be created.
docs-supported-functions:
status: todo
comment: Documentation needs to be created.
docs-troubleshooting:
status: todo
comment: Documentation needs to be created.
docs-use-cases:
status: todo
comment: Documentation needs to be created.
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices:
status: todo
comment: Stale devices can be done dynamically
# Platinum
async-dependency: todo
inject-websession: done
strict-typing: done
@@ -0,0 +1,77 @@
"""Support for Aladdin Connect Genie sensors."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from genie_partner_sdk.model import GarageDoor
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
from .entity import AladdinConnectEntity
@dataclass(frozen=True, kw_only=True)
class AladdinConnectSensorEntityDescription(SensorEntityDescription):
"""Sensor entity description for Aladdin Connect."""
value_fn: Callable[[GarageDoor], float | None]
SENSOR_TYPES: tuple[AladdinConnectSensorEntityDescription, ...] = (
AladdinConnectSensorEntityDescription(
key="battery_level",
device_class=SensorDeviceClass.BATTERY,
entity_registry_enabled_default=False,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda garage_door: garage_door.battery_level,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AladdinConnectConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Aladdin Connect sensor devices."""
coordinators = entry.runtime_data
async_add_entities(
AladdinConnectSensor(coordinator, description)
for coordinator in coordinators.values()
for description in SENSOR_TYPES
)
class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
"""A sensor implementation for Aladdin Connect device."""
entity_description: AladdinConnectSensorEntityDescription
def __init__(
self,
coordinator: AladdinConnectCoordinator,
entity_description: AladdinConnectSensorEntityDescription,
) -> None:
"""Initialize the Aladdin Connect sensor."""
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_unique_id = f"{coordinator.data.unique_id}-{entity_description.key}"
@property
def native_value(self) -> float | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data)
@@ -1,8 +1,30 @@
{
"issues": {
"integration_removed": {
"title": "The Aladdin Connect integration has been removed",
"description": "The Aladdin Connect integration has been removed from Home Assistant.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing Aladdin Connect integration entries]({entries})."
"config": {
"step": {
"pick_implementation": {
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "Aladdin Connect needs to re-authenticate your account"
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"wrong_account": "You are authenticated with a different account than the one set up. Please authenticate with the configured account."
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
}
}
}
@@ -1,11 +1,11 @@
"""Alexa Devices integration."""
from homeassistant.const import Platform
from homeassistant.const import CONF_COUNTRY, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
from .const import _LOGGER, CONF_LOGIN_DATA, CONF_SITE, COUNTRY_DOMAINS, DOMAIN
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
from .services import async_setup_services
@@ -40,6 +40,48 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo
return True
async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
"""Migrate old entry."""
if entry.version == 1 and entry.minor_version < 3:
if CONF_SITE in entry.data:
# Site in data (wrong place), just move to login data
new_data = entry.data.copy()
new_data[CONF_LOGIN_DATA][CONF_SITE] = new_data[CONF_SITE]
new_data.pop(CONF_SITE)
hass.config_entries.async_update_entry(
entry, data=new_data, version=1, minor_version=3
)
return True
if CONF_SITE in entry.data[CONF_LOGIN_DATA]:
# Site is there, just update version to avoid future migrations
hass.config_entries.async_update_entry(entry, version=1, minor_version=3)
return True
_LOGGER.debug(
"Migrating from version %s.%s", entry.version, entry.minor_version
)
# Convert country in domain
country = entry.data[CONF_COUNTRY].lower()
domain = COUNTRY_DOMAINS.get(country, country)
# Add site to login data
new_data = entry.data.copy()
new_data[CONF_LOGIN_DATA][CONF_SITE] = f"https://www.amazon.{domain}"
hass.config_entries.async_update_entry(
entry, data=new_data, version=1, minor_version=3
)
_LOGGER.info(
"Migration to version %s.%s successful", entry.version, entry.minor_version
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -10,16 +10,14 @@ from aioamazondevices.exceptions import (
CannotAuthenticate,
CannotConnect,
CannotRetrieveData,
WrongCountry,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.selector import CountrySelector
from .const import CONF_LOGIN_DATA, DOMAIN
@@ -29,6 +27,12 @@ STEP_REAUTH_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_CODE): cv.string,
}
)
STEP_RECONFIGURE = vol.Schema(
{
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_CODE): cv.string,
}
)
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
@@ -37,7 +41,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
session = aiohttp_client.async_create_clientsession(hass)
api = AmazonEchoApi(
session,
data[CONF_COUNTRY],
data[CONF_USERNAME],
data[CONF_PASSWORD],
)
@@ -48,6 +51,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Alexa Devices."""
VERSION = 1
MINOR_VERSION = 3
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -58,12 +64,10 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
data = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except CannotAuthenticate:
except (CannotAuthenticate, TypeError):
errors["base"] = "invalid_auth"
except CannotRetrieveData:
errors["base"] = "cannot_retrieve_data"
except WrongCountry:
errors["base"] = "wrong_country"
else:
await self.async_set_unique_id(data["customer_info"]["user_id"])
self._abort_if_unique_id_configured()
@@ -78,9 +82,6 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
data_schema=vol.Schema(
{
vol.Required(
CONF_COUNTRY, default=self.hass.config.country
): CountrySelector(),
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_CODE): cv.string,
@@ -106,10 +107,12 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
try:
await validate_input(self.hass, {**reauth_entry.data, **user_input})
data = await validate_input(
self.hass, {**reauth_entry.data, **user_input}
)
except CannotConnect:
errors["base"] = "cannot_connect"
except CannotAuthenticate:
except (CannotAuthenticate, TypeError):
errors["base"] = "invalid_auth"
except CannotRetrieveData:
errors["base"] = "cannot_retrieve_data"
@@ -118,8 +121,9 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
reauth_entry,
data={
CONF_USERNAME: entry_data[CONF_USERNAME],
CONF_PASSWORD: entry_data[CONF_PASSWORD],
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_CODE: user_input[CONF_CODE],
CONF_LOGIN_DATA: data,
},
)
@@ -129,3 +133,47 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=STEP_REAUTH_DATA_SCHEMA,
errors=errors,
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of the device."""
reconfigure_entry = self._get_reconfigure_entry()
if not user_input:
return self.async_show_form(
step_id="reconfigure",
data_schema=STEP_RECONFIGURE,
)
updated_password = user_input[CONF_PASSWORD]
self._async_abort_entries_match(
{CONF_USERNAME: reconfigure_entry.data[CONF_USERNAME]}
)
errors: dict[str, str] = {}
try:
data = await validate_input(
self.hass, {**reconfigure_entry.data, **user_input}
)
except CannotConnect:
errors["base"] = "cannot_connect"
except CannotAuthenticate:
errors["base"] = "invalid_auth"
except CannotRetrieveData:
errors["base"] = "cannot_retrieve_data"
else:
return self.async_update_reload_and_abort(
reconfigure_entry,
data_updates={
CONF_PASSWORD: updated_password,
CONF_LOGIN_DATA: data,
},
)
return self.async_show_form(
step_id="reconfigure",
data_schema=STEP_RECONFIGURE,
errors=errors,
)
@@ -6,3 +6,23 @@ _LOGGER = logging.getLogger(__package__)
DOMAIN = "alexa_devices"
CONF_LOGIN_DATA = "login_data"
CONF_SITE = "site"
DEFAULT_DOMAIN = "com"
COUNTRY_DOMAINS = {
"ar": DEFAULT_DOMAIN,
"at": DEFAULT_DOMAIN,
"au": "com.au",
"be": "com.be",
"br": DEFAULT_DOMAIN,
"gb": "co.uk",
"il": DEFAULT_DOMAIN,
"jp": "co.jp",
"mx": "com.mx",
"no": DEFAULT_DOMAIN,
"nz": "com.au",
"pl": DEFAULT_DOMAIN,
"tr": "com.tr",
"us": DEFAULT_DOMAIN,
"za": "co.za",
}
@@ -11,7 +11,7 @@ from aioamazondevices.exceptions import (
from aiohttp import ClientSession
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -44,7 +44,6 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
)
self.api = AmazonEchoApi(
session,
entry.data[CONF_COUNTRY],
entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD],
entry.data[CONF_LOGIN_DATA],
@@ -67,7 +66,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
translation_key="cannot_retrieve_data_with_error",
translation_placeholders={"error": repr(err)},
) from err
except CannotAuthenticate as err:
except (CannotAuthenticate, TypeError) as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "silver",
"requirements": ["aioamazondevices==4.0.0"]
"requirements": ["aioamazondevices==6.0.0"]
}
@@ -60,7 +60,7 @@ rules:
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow: todo
reconfiguration-flow: done
repair-issues:
status: exempt
comment: no known use cases for repair issues or flows, yet
@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import LIGHT_LUX, UnitOfTemperature
from homeassistant.core import HomeAssistant
@@ -41,11 +42,13 @@ SENSORS: Final = (
if device.sensors[_key].scale == "CELSIUS"
else UnitOfTemperature.FAHRENHEIT
),
state_class=SensorStateClass.MEASUREMENT,
),
AmazonSensorEntityDescription(
key="illuminance",
device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement=LIGHT_LUX,
state_class=SensorStateClass.MEASUREMENT,
),
)
@@ -14,14 +14,12 @@ from .coordinator import AmazonConfigEntry
ATTR_TEXT_COMMAND = "text_command"
ATTR_SOUND = "sound"
ATTR_SOUND_VARIANT = "sound_variant"
SERVICE_TEXT_COMMAND = "send_text_command"
SERVICE_SOUND_NOTIFICATION = "send_sound"
SCHEMA_SOUND_SERVICE = vol.Schema(
{
vol.Required(ATTR_SOUND): cv.string,
vol.Required(ATTR_SOUND_VARIANT): cv.positive_int,
vol.Required(ATTR_DEVICE_ID): cv.string,
},
)
@@ -75,17 +73,14 @@ async def _async_execute_action(call: ServiceCall, attribute: str) -> None:
coordinator = config_entry.runtime_data
if attribute == ATTR_SOUND:
variant: int = call.data[ATTR_SOUND_VARIANT]
pad = "_" if variant > 10 else "_0"
file = f"{value}{pad}{variant!s}"
if value not in SOUNDS_LIST or variant > SOUNDS_LIST[value]:
if value not in SOUNDS_LIST:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_sound_value",
translation_placeholders={"sound": value, "variant": str(variant)},
translation_placeholders={"sound": value},
)
await coordinator.api.call_alexa_sound(
coordinator.data[device.serial_number], file
coordinator.data[device.serial_number], value
)
elif attribute == ATTR_TEXT_COMMAND:
await coordinator.api.call_alexa_text_command(
@@ -18,14 +18,6 @@ send_sound:
selector:
device:
integration: alexa_devices
sound_variant:
required: true
example: 1
default: 1
selector:
number:
min: 1
max: 50
sound:
required: true
example: amzn_sfx_doorbell_chime
@@ -33,472 +25,45 @@ send_sound:
selector:
select:
options:
- air_horn
- air_horns
- airboat
- airport
- aliens
- amzn_sfx_airplane_takeoff_whoosh
- amzn_sfx_army_march_clank_7x
- amzn_sfx_army_march_large_8x
- amzn_sfx_army_march_small_8x
- amzn_sfx_baby_big_cry
- amzn_sfx_baby_cry
- amzn_sfx_baby_fuss
- amzn_sfx_battle_group_clanks
- amzn_sfx_battle_man_grunts
- amzn_sfx_battle_men_grunts
- amzn_sfx_battle_men_horses
- amzn_sfx_battle_noisy_clanks
- amzn_sfx_battle_yells_men
- amzn_sfx_battle_yells_men_run
- amzn_sfx_bear_groan_roar
- amzn_sfx_bear_roar_grumble
- amzn_sfx_bear_roar_small
- amzn_sfx_beep_1x
- amzn_sfx_bell_med_chime
- amzn_sfx_bell_short_chime
- amzn_sfx_bell_timer
- amzn_sfx_bicycle_bell_ring
- amzn_sfx_bird_chickadee_chirp_1x
- amzn_sfx_bird_chickadee_chirps
- amzn_sfx_bird_forest
- amzn_sfx_bird_forest_short
- amzn_sfx_bird_robin_chirp_1x
- amzn_sfx_boing_long_1x
- amzn_sfx_boing_med_1x
- amzn_sfx_boing_short_1x
- amzn_sfx_bus_drive_past
- amzn_sfx_buzz_electronic
- amzn_sfx_buzzer_loud_alarm
- amzn_sfx_buzzer_small
- amzn_sfx_car_accelerate
- amzn_sfx_car_accelerate_noisy
- amzn_sfx_car_click_seatbelt
- amzn_sfx_car_close_door_1x
- amzn_sfx_car_drive_past
- amzn_sfx_car_honk_1x
- amzn_sfx_car_honk_2x
- amzn_sfx_car_honk_3x
- amzn_sfx_car_honk_long_1x
- amzn_sfx_car_into_driveway
- amzn_sfx_car_into_driveway_fast
- amzn_sfx_car_slam_door_1x
- amzn_sfx_car_undo_seatbelt
- amzn_sfx_cat_angry_meow_1x
- amzn_sfx_cat_angry_screech_1x
- amzn_sfx_cat_long_meow_1x
- amzn_sfx_cat_meow_1x
- amzn_sfx_cat_purr
- amzn_sfx_cat_purr_meow
- amzn_sfx_chicken_cluck
- amzn_sfx_church_bell_1x
- amzn_sfx_church_bells_ringing
- amzn_sfx_clear_throat_ahem
- amzn_sfx_clock_ticking
- amzn_sfx_clock_ticking_long
- amzn_sfx_copy_machine
- amzn_sfx_cough
- amzn_sfx_crow_caw_1x
- amzn_sfx_crowd_applause
- amzn_sfx_crowd_bar
- amzn_sfx_crowd_bar_rowdy
- amzn_sfx_crowd_boo
- amzn_sfx_crowd_cheer_med
- amzn_sfx_crowd_excited_cheer
- amzn_sfx_dog_med_bark_1x
- amzn_sfx_dog_med_bark_2x
- amzn_sfx_dog_med_bark_growl
- amzn_sfx_dog_med_growl_1x
- amzn_sfx_dog_med_woof_1x
- amzn_sfx_dog_small_bark_2x
- amzn_sfx_door_open
- amzn_sfx_door_shut
- amzn_sfx_doorbell
- amzn_sfx_doorbell_buzz
- amzn_sfx_doorbell_chime
- amzn_sfx_drinking_slurp
- amzn_sfx_drum_and_cymbal
- amzn_sfx_drum_comedy
- amzn_sfx_earthquake_rumble
- amzn_sfx_electric_guitar
- amzn_sfx_electronic_beep
- amzn_sfx_electronic_major_chord
- amzn_sfx_elephant
- amzn_sfx_elevator_bell_1x
- amzn_sfx_elevator_open_bell
- amzn_sfx_fairy_melodic_chimes
- amzn_sfx_fairy_sparkle_chimes
- amzn_sfx_faucet_drip
- amzn_sfx_faucet_running
- amzn_sfx_fireplace_crackle
- amzn_sfx_fireworks
- amzn_sfx_fireworks_firecrackers
- amzn_sfx_fireworks_launch
- amzn_sfx_fireworks_whistles
- amzn_sfx_food_frying
- amzn_sfx_footsteps
- amzn_sfx_footsteps_muffled
- amzn_sfx_ghost_spooky
- amzn_sfx_glass_on_table
- amzn_sfx_glasses_clink
- amzn_sfx_horse_gallop_4x
- amzn_sfx_horse_huff_whinny
- amzn_sfx_horse_neigh
- amzn_sfx_horse_neigh_low
- amzn_sfx_horse_whinny
- amzn_sfx_human_walking
- amzn_sfx_jar_on_table_1x
- amzn_sfx_kitchen_ambience
- amzn_sfx_large_crowd_cheer
- amzn_sfx_large_fire_crackling
- amzn_sfx_laughter
- amzn_sfx_laughter_giggle
- amzn_sfx_lightning_strike
- amzn_sfx_lion_roar
- amzn_sfx_magic_blast_1x
- amzn_sfx_monkey_calls_3x
- amzn_sfx_monkey_chimp
- amzn_sfx_monkeys_chatter
- amzn_sfx_motorcycle_accelerate
- amzn_sfx_motorcycle_engine_idle
- amzn_sfx_motorcycle_engine_rev
- amzn_sfx_musical_drone_intro
- amzn_sfx_oars_splashing_rowboat
- amzn_sfx_object_on_table_2x
- amzn_sfx_ocean_wave_1x
- amzn_sfx_ocean_wave_on_rocks_1x
- amzn_sfx_ocean_wave_surf
- amzn_sfx_people_walking
- amzn_sfx_person_running
- amzn_sfx_piano_note_1x
- amzn_sfx_punch
- amzn_sfx_rain
- amzn_sfx_rain_on_roof
- amzn_sfx_rain_thunder
- amzn_sfx_rat_squeak_2x
- amzn_sfx_rat_squeaks
- amzn_sfx_raven_caw_1x
- amzn_sfx_raven_caw_2x
- amzn_sfx_restaurant_ambience
- amzn_sfx_rooster_crow
- amzn_sfx_scifi_air_escaping
- amzn_sfx_scifi_alarm
- amzn_sfx_scifi_alien_voice
- amzn_sfx_scifi_boots_walking
- amzn_sfx_scifi_close_large_explosion
- amzn_sfx_scifi_door_open
- amzn_sfx_scifi_engines_on
- amzn_sfx_scifi_engines_on_large
- amzn_sfx_scifi_engines_on_short_burst
- amzn_sfx_scifi_explosion
- amzn_sfx_scifi_explosion_2x
- amzn_sfx_scifi_incoming_explosion
- amzn_sfx_scifi_laser_gun_battle
- amzn_sfx_scifi_laser_gun_fires
- amzn_sfx_scifi_laser_gun_fires_large
- amzn_sfx_scifi_long_explosion_1x
- amzn_sfx_scifi_missile
- amzn_sfx_scifi_motor_short_1x
- amzn_sfx_scifi_open_airlock
- amzn_sfx_scifi_radar_high_ping
- amzn_sfx_scifi_radar_low
- amzn_sfx_scifi_radar_medium
- amzn_sfx_scifi_run_away
- amzn_sfx_scifi_sheilds_up
- amzn_sfx_scifi_short_low_explosion
- amzn_sfx_scifi_small_whoosh_flyby
- amzn_sfx_scifi_small_zoom_flyby
- amzn_sfx_scifi_sonar_ping_3x
- amzn_sfx_scifi_sonar_ping_4x
- amzn_sfx_scifi_spaceship_flyby
- amzn_sfx_scifi_timer_beep
- amzn_sfx_scifi_zap_backwards
- amzn_sfx_scifi_zap_electric
- amzn_sfx_sheep_baa
- amzn_sfx_sheep_bleat
- amzn_sfx_silverware_clank
- amzn_sfx_sirens
- amzn_sfx_sleigh_bells
- amzn_sfx_small_stream
- amzn_sfx_sneeze
- amzn_sfx_stream
- amzn_sfx_strong_wind_desert
- amzn_sfx_strong_wind_whistling
- amzn_sfx_subway_leaving
- amzn_sfx_subway_passing
- amzn_sfx_subway_stopping
- amzn_sfx_swoosh_cartoon_fast
- amzn_sfx_swoosh_fast_1x
- amzn_sfx_swoosh_fast_6x
- amzn_sfx_test_tone
- amzn_sfx_thunder_rumble
- amzn_sfx_toilet_flush
- amzn_sfx_trumpet_bugle
- amzn_sfx_turkey_gobbling
- amzn_sfx_typing_medium
- amzn_sfx_typing_short
- amzn_sfx_typing_typewriter
- amzn_sfx_vacuum_off
- amzn_sfx_vacuum_on
- amzn_sfx_walking_in_mud
- amzn_sfx_walking_in_snow
- amzn_sfx_walking_on_grass
- amzn_sfx_water_dripping
- amzn_sfx_water_droplets
- amzn_sfx_wind_strong_gusting
- amzn_sfx_wind_whistling_desert
- amzn_sfx_wings_flap_4x
- amzn_sfx_wings_flap_fast
- amzn_sfx_wolf_howl
- amzn_sfx_wolf_young_howl
- amzn_sfx_wooden_door
- amzn_sfx_wooden_door_creaks_long
- amzn_sfx_wooden_door_creaks_multiple
- amzn_sfx_wooden_door_creaks_open
- amzn_ui_sfx_gameshow_bridge
- amzn_ui_sfx_gameshow_countdown_loop_32s_full
- amzn_ui_sfx_gameshow_countdown_loop_64s_full
- amzn_ui_sfx_gameshow_countdown_loop_64s_minimal
- amzn_ui_sfx_gameshow_intro
- amzn_ui_sfx_gameshow_negative_response
- amzn_ui_sfx_gameshow_neutral_response
- amzn_ui_sfx_gameshow_outro
- amzn_ui_sfx_gameshow_player1
- amzn_ui_sfx_gameshow_player2
- amzn_ui_sfx_gameshow_player3
- amzn_ui_sfx_gameshow_player4
- amzn_ui_sfx_gameshow_positive_response
- amzn_ui_sfx_gameshow_tally_negative
- amzn_ui_sfx_gameshow_tally_positive
- amzn_ui_sfx_gameshow_waiting_loop_30s
- anchor
- answering_machines
- arcs_sparks
- arrows_bows
- baby
- back_up_beeps
- bars_restaurants
- baseball
- basketball
- battles
- beeps_tones
- bell
- bikes
- billiards
- board_games
- body
- boing
- books
- bow_wash
- box
- break_shatter_smash
- breaks
- brooms_mops
- bullets
- buses
- buzz
- buzz_hums
- buzzers
- buzzers_pistols
- cables_metal
- camera
- cannons
- car_alarm
- car_alarms
- car_cell_phones
- carnivals_fairs
- cars
- casino
- casinos
- cellar
- chimes
- chimes_bells
- chorus
- christmas
- church_bells
- clock
- cloth
- concrete
- construction
- construction_factory
- crashes
- crowds
- debris
- dining_kitchens
- dinosaurs
- dripping
- drops
- electric
- electrical
- elevator
- evolution_monsters
- explosions
- factory
- falls
- fax_scanner_copier
- feedback_mics
- fight
- fire
- fire_extinguisher
- fireballs
- fireworks
- fishing_pole
- flags
- football
- footsteps
- futuristic
- futuristic_ship
- gameshow
- gear
- ghosts_demons
- giant_monster
- glass
- glasses_clink
- golf
- gorilla
- grenade_lanucher
- griffen
- gyms_locker_rooms
- handgun_loading
- handgun_shot
- handle
- hands
- heartbeats_ekg
- helicopter
- high_tech
- hit_punch_slap
- hits
- horns
- horror
- hot_tub_filling_up
- human
- human_vocals
- hygene # codespell:ignore
- ice_skating
- ignitions
- infantry
- intro
- jet
- juggling
- key_lock
- kids
- knocks
- lab_equip
- lacrosse
- lamps_lanterns
- leather
- liquid_suction
- locker_doors
- machine_gun
- magic_spells
- medium_large_explosions
- metal
- modern_rings
- money_coins
- motorcycles
- movement
- moves
- nature
- oar_boat
- pagers
- paintball
- paper
- parachute
- pay_phones
- phone_beeps
- pigmy_bats
- pills
- pour_water
- power_up_down
- printers
- prison
- public_space
- racquetball
- radios_static
- rain
- rc_airplane
- rc_car
- refrigerators_freezers
- regular
- respirator
- rifle
- roller_coaster
- rollerskates_rollerblades
- room_tones
- ropes_climbing
- rotary_rings
- rowboat_canoe
- rubber
- running
- sails
- sand_gravel
- screen_doors
- screens
- seats_stools
- servos
- shoes_boots
- shotgun
- shower
- sink_faucet
- sink_filling_water
- sink_run_and_off
- sink_water_splatter
- sirens
- skateboards
- ski
- skids_tires
- sled
- slides
- small_explosions
- snow
- snowmobile
- soldiers
- splash_water
- splashes_sprays
- sports_whistles
- squeaks
- squeaky
- stairs
- steam
- submarine_diesel
- swing_doors
- switches_levers
- swords
- tape
- tape_machine
- televisions_shows
- tennis_pingpong
- textile
- throw
- thunder
- ticks
- timer
- toilet_flush
- tone
- tones_noises
- toys
- tractors
- traffic
- train
- trucks_vans
- turnstiles
- typing
- umbrella
- underwater
- vampires
- various
- video_tunes
- volcano_earthquake
- watches
- water
- water_running
- werewolves
- winches_gears
- wind
- wood
- wood_boat
- woosh
- zap
- zippers
- air_horn_03
- amzn_sfx_cat_meow_1x_01
- amzn_sfx_church_bell_1x_02
- amzn_sfx_crowd_applause_01
- amzn_sfx_dog_med_bark_1x_02
- amzn_sfx_doorbell_01
- amzn_sfx_doorbell_chime_01
- amzn_sfx_doorbell_chime_02
- amzn_sfx_large_crowd_cheer_01
- amzn_sfx_lion_roar_02
- amzn_sfx_rooster_crow_01
- amzn_sfx_scifi_alarm_01
- amzn_sfx_scifi_alarm_04
- amzn_sfx_scifi_engines_on_02
- amzn_sfx_scifi_sheilds_up_01
- amzn_sfx_trumpet_bugle_04
- amzn_sfx_wolf_howl_02
- bell_02
- boing_01
- boing_03
- buzzers_pistols_01
- camera_01
- christmas_05
- clock_01
- futuristic_10
- halloween_bats
- halloween_crows
- halloween_footsteps
- halloween_wind
- halloween_wolf
- holiday_halloween_ghost
- horror_10
- med_system_alerts_minimal_dragon_short
- med_system_alerts_minimal_owl_short
- med_system_alerts_minimals_blue_wave_small
- med_system_alerts_minimals_galaxy_short
- med_system_alerts_minimals_panda_short
- med_system_alerts_minimals_tiger_short
- med_ui_success_generic_1-1
- squeaky_12
- zap_01
translation_key: sound
@@ -1,7 +1,6 @@
{
"common": {
"data_code": "One-time password (OTP code)",
"data_description_country": "The country where your Amazon account is registered.",
"data_description_username": "The email address of your Amazon account.",
"data_description_password": "The password of your Amazon account.",
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported.",
@@ -12,13 +11,11 @@
"step": {
"user": {
"data": {
"country": "[%key:common::config_flow::data::country%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"code": "[%key:component::alexa_devices::common::data_code%]"
},
"data_description": {
"country": "[%key:component::alexa_devices::common::data_description_country%]",
"username": "[%key:component::alexa_devices::common::data_description_username%]",
"password": "[%key:component::alexa_devices::common::data_description_password%]",
"code": "[%key:component::alexa_devices::common::data_description_code%]"
@@ -33,6 +30,16 @@
"password": "[%key:component::alexa_devices::common::data_description_password%]",
"code": "[%key:component::alexa_devices::common::data_description_code%]"
}
},
"reconfigure": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"code": "[%key:component::alexa_devices::common::data_code%]"
},
"data_description": {
"password": "[%key:component::alexa_devices::common::data_description_password%]",
"code": "[%key:component::alexa_devices::common::data_description_code%]"
}
}
},
"abort": {
@@ -40,13 +47,13 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"cannot_retrieve_data": "Unable to retrieve data from Amazon. Please try again later.",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"wrong_country": "Wrong country selected. Please select the country where your Amazon account is registered.",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
@@ -122,474 +129,47 @@
"selector": {
"sound": {
"options": {
"air_horn": "Air Horn",
"air_horns": "Air Horns",
"airboat": "Airboat",
"airport": "Airport",
"aliens": "Aliens",
"amzn_sfx_airplane_takeoff_whoosh": "Airplane Takeoff Whoosh",
"amzn_sfx_army_march_clank_7x": "Army March Clank 7x",
"amzn_sfx_army_march_large_8x": "Army March Large 8x",
"amzn_sfx_army_march_small_8x": "Army March Small 8x",
"amzn_sfx_baby_big_cry": "Baby Big Cry",
"amzn_sfx_baby_cry": "Baby Cry",
"amzn_sfx_baby_fuss": "Baby Fuss",
"amzn_sfx_battle_group_clanks": "Battle Group Clanks",
"amzn_sfx_battle_man_grunts": "Battle Man Grunts",
"amzn_sfx_battle_men_grunts": "Battle Men Grunts",
"amzn_sfx_battle_men_horses": "Battle Men Horses",
"amzn_sfx_battle_noisy_clanks": "Battle Noisy Clanks",
"amzn_sfx_battle_yells_men": "Battle Yells Men",
"amzn_sfx_battle_yells_men_run": "Battle Yells Men Run",
"amzn_sfx_bear_groan_roar": "Bear Groan Roar",
"amzn_sfx_bear_roar_grumble": "Bear Roar Grumble",
"amzn_sfx_bear_roar_small": "Bear Roar Small",
"amzn_sfx_beep_1x": "Beep 1x",
"amzn_sfx_bell_med_chime": "Bell Med Chime",
"amzn_sfx_bell_short_chime": "Bell Short Chime",
"amzn_sfx_bell_timer": "Bell Timer",
"amzn_sfx_bicycle_bell_ring": "Bicycle Bell Ring",
"amzn_sfx_bird_chickadee_chirp_1x": "Bird Chickadee Chirp 1x",
"amzn_sfx_bird_chickadee_chirps": "Bird Chickadee Chirps",
"amzn_sfx_bird_forest": "Bird Forest",
"amzn_sfx_bird_forest_short": "Bird Forest Short",
"amzn_sfx_bird_robin_chirp_1x": "Bird Robin Chirp 1x",
"amzn_sfx_boing_long_1x": "Boing Long 1x",
"amzn_sfx_boing_med_1x": "Boing Med 1x",
"amzn_sfx_boing_short_1x": "Boing Short 1x",
"amzn_sfx_bus_drive_past": "Bus Drive Past",
"amzn_sfx_buzz_electronic": "Buzz Electronic",
"amzn_sfx_buzzer_loud_alarm": "Buzzer Loud Alarm",
"amzn_sfx_buzzer_small": "Buzzer Small",
"amzn_sfx_car_accelerate": "Car Accelerate",
"amzn_sfx_car_accelerate_noisy": "Car Accelerate Noisy",
"amzn_sfx_car_click_seatbelt": "Car Click Seatbelt",
"amzn_sfx_car_close_door_1x": "Car Close Door 1x",
"amzn_sfx_car_drive_past": "Car Drive Past",
"amzn_sfx_car_honk_1x": "Car Honk 1x",
"amzn_sfx_car_honk_2x": "Car Honk 2x",
"amzn_sfx_car_honk_3x": "Car Honk 3x",
"amzn_sfx_car_honk_long_1x": "Car Honk Long 1x",
"amzn_sfx_car_into_driveway": "Car Into Driveway",
"amzn_sfx_car_into_driveway_fast": "Car Into Driveway Fast",
"amzn_sfx_car_slam_door_1x": "Car Slam Door 1x",
"amzn_sfx_car_undo_seatbelt": "Car Undo Seatbelt",
"amzn_sfx_cat_angry_meow_1x": "Cat Angry Meow 1x",
"amzn_sfx_cat_angry_screech_1x": "Cat Angry Screech 1x",
"amzn_sfx_cat_long_meow_1x": "Cat Long Meow 1x",
"amzn_sfx_cat_meow_1x": "Cat Meow 1x",
"amzn_sfx_cat_purr": "Cat Purr",
"amzn_sfx_cat_purr_meow": "Cat Purr Meow",
"amzn_sfx_chicken_cluck": "Chicken Cluck",
"amzn_sfx_church_bell_1x": "Church Bell 1x",
"amzn_sfx_church_bells_ringing": "Church Bells Ringing",
"amzn_sfx_clear_throat_ahem": "Clear Throat Ahem",
"amzn_sfx_clock_ticking": "Clock Ticking",
"amzn_sfx_clock_ticking_long": "Clock Ticking Long",
"amzn_sfx_copy_machine": "Copy Machine",
"amzn_sfx_cough": "Cough",
"amzn_sfx_crow_caw_1x": "Crow Caw 1x",
"amzn_sfx_crowd_applause": "Crowd Applause",
"amzn_sfx_crowd_bar": "Crowd Bar",
"amzn_sfx_crowd_bar_rowdy": "Crowd Bar Rowdy",
"amzn_sfx_crowd_boo": "Crowd Boo",
"amzn_sfx_crowd_cheer_med": "Crowd Cheer Med",
"amzn_sfx_crowd_excited_cheer": "Crowd Excited Cheer",
"amzn_sfx_dog_med_bark_1x": "Dog Med Bark 1x",
"amzn_sfx_dog_med_bark_2x": "Dog Med Bark 2x",
"amzn_sfx_dog_med_bark_growl": "Dog Med Bark Growl",
"amzn_sfx_dog_med_growl_1x": "Dog Med Growl 1x",
"amzn_sfx_dog_med_woof_1x": "Dog Med Woof 1x",
"amzn_sfx_dog_small_bark_2x": "Dog Small Bark 2x",
"amzn_sfx_door_open": "Door Open",
"amzn_sfx_door_shut": "Door Shut",
"amzn_sfx_doorbell": "Doorbell",
"amzn_sfx_doorbell_buzz": "Doorbell Buzz",
"amzn_sfx_doorbell_chime": "Doorbell Chime",
"amzn_sfx_drinking_slurp": "Drinking Slurp",
"amzn_sfx_drum_and_cymbal": "Drum And Cymbal",
"amzn_sfx_drum_comedy": "Drum Comedy",
"amzn_sfx_earthquake_rumble": "Earthquake Rumble",
"amzn_sfx_electric_guitar": "Electric Guitar",
"amzn_sfx_electronic_beep": "Electronic Beep",
"amzn_sfx_electronic_major_chord": "Electronic Major Chord",
"amzn_sfx_elephant": "Elephant",
"amzn_sfx_elevator_bell_1x": "Elevator Bell 1x",
"amzn_sfx_elevator_open_bell": "Elevator Open Bell",
"amzn_sfx_fairy_melodic_chimes": "Fairy Melodic Chimes",
"amzn_sfx_fairy_sparkle_chimes": "Fairy Sparkle Chimes",
"amzn_sfx_faucet_drip": "Faucet Drip",
"amzn_sfx_faucet_running": "Faucet Running",
"amzn_sfx_fireplace_crackle": "Fireplace Crackle",
"amzn_sfx_fireworks": "Fireworks",
"amzn_sfx_fireworks_firecrackers": "Fireworks Firecrackers",
"amzn_sfx_fireworks_launch": "Fireworks Launch",
"amzn_sfx_fireworks_whistles": "Fireworks Whistles",
"amzn_sfx_food_frying": "Food Frying",
"amzn_sfx_footsteps": "Footsteps",
"amzn_sfx_footsteps_muffled": "Footsteps Muffled",
"amzn_sfx_ghost_spooky": "Ghost Spooky",
"amzn_sfx_glass_on_table": "Glass On Table",
"amzn_sfx_glasses_clink": "Glasses Clink",
"amzn_sfx_horse_gallop_4x": "Horse Gallop 4x",
"amzn_sfx_horse_huff_whinny": "Horse Huff Whinny",
"amzn_sfx_horse_neigh": "Horse Neigh",
"amzn_sfx_horse_neigh_low": "Horse Neigh Low",
"amzn_sfx_horse_whinny": "Horse Whinny",
"amzn_sfx_human_walking": "Human Walking",
"amzn_sfx_jar_on_table_1x": "Jar On Table 1x",
"amzn_sfx_kitchen_ambience": "Kitchen Ambience",
"amzn_sfx_large_crowd_cheer": "Large Crowd Cheer",
"amzn_sfx_large_fire_crackling": "Large Fire Crackling",
"amzn_sfx_laughter": "Laughter",
"amzn_sfx_laughter_giggle": "Laughter Giggle",
"amzn_sfx_lightning_strike": "Lightning Strike",
"amzn_sfx_lion_roar": "Lion Roar",
"amzn_sfx_magic_blast_1x": "Magic Blast 1x",
"amzn_sfx_monkey_calls_3x": "Monkey Calls 3x",
"amzn_sfx_monkey_chimp": "Monkey Chimp",
"amzn_sfx_monkeys_chatter": "Monkeys Chatter",
"amzn_sfx_motorcycle_accelerate": "Motorcycle Accelerate",
"amzn_sfx_motorcycle_engine_idle": "Motorcycle Engine Idle",
"amzn_sfx_motorcycle_engine_rev": "Motorcycle Engine Rev",
"amzn_sfx_musical_drone_intro": "Musical Drone Intro",
"amzn_sfx_oars_splashing_rowboat": "Oars Splashing Rowboat",
"amzn_sfx_object_on_table_2x": "Object On Table 2x",
"amzn_sfx_ocean_wave_1x": "Ocean Wave 1x",
"amzn_sfx_ocean_wave_on_rocks_1x": "Ocean Wave On Rocks 1x",
"amzn_sfx_ocean_wave_surf": "Ocean Wave Surf",
"amzn_sfx_people_walking": "People Walking",
"amzn_sfx_person_running": "Person Running",
"amzn_sfx_piano_note_1x": "Piano Note 1x",
"amzn_sfx_punch": "Punch",
"amzn_sfx_rain": "Rain",
"amzn_sfx_rain_on_roof": "Rain On Roof",
"amzn_sfx_rain_thunder": "Rain Thunder",
"amzn_sfx_rat_squeak_2x": "Rat Squeak 2x",
"amzn_sfx_rat_squeaks": "Rat Squeaks",
"amzn_sfx_raven_caw_1x": "Raven Caw 1x",
"amzn_sfx_raven_caw_2x": "Raven Caw 2x",
"amzn_sfx_restaurant_ambience": "Restaurant Ambience",
"amzn_sfx_rooster_crow": "Rooster Crow",
"amzn_sfx_scifi_air_escaping": "Scifi Air Escaping",
"amzn_sfx_scifi_alarm": "Scifi Alarm",
"amzn_sfx_scifi_alien_voice": "Scifi Alien Voice",
"amzn_sfx_scifi_boots_walking": "Scifi Boots Walking",
"amzn_sfx_scifi_close_large_explosion": "Scifi Close Large Explosion",
"amzn_sfx_scifi_door_open": "Scifi Door Open",
"amzn_sfx_scifi_engines_on": "Scifi Engines On",
"amzn_sfx_scifi_engines_on_large": "Scifi Engines On Large",
"amzn_sfx_scifi_engines_on_short_burst": "Scifi Engines On Short Burst",
"amzn_sfx_scifi_explosion": "Scifi Explosion",
"amzn_sfx_scifi_explosion_2x": "Scifi Explosion 2x",
"amzn_sfx_scifi_incoming_explosion": "Scifi Incoming Explosion",
"amzn_sfx_scifi_laser_gun_battle": "Scifi Laser Gun Battle",
"amzn_sfx_scifi_laser_gun_fires": "Scifi Laser Gun Fires",
"amzn_sfx_scifi_laser_gun_fires_large": "Scifi Laser Gun Fires Large",
"amzn_sfx_scifi_long_explosion_1x": "Scifi Long Explosion 1x",
"amzn_sfx_scifi_missile": "Scifi Missile",
"amzn_sfx_scifi_motor_short_1x": "Scifi Motor Short 1x",
"amzn_sfx_scifi_open_airlock": "Scifi Open Airlock",
"amzn_sfx_scifi_radar_high_ping": "Scifi Radar High Ping",
"amzn_sfx_scifi_radar_low": "Scifi Radar Low",
"amzn_sfx_scifi_radar_medium": "Scifi Radar Medium",
"amzn_sfx_scifi_run_away": "Scifi Run Away",
"amzn_sfx_scifi_sheilds_up": "Scifi Sheilds Up",
"amzn_sfx_scifi_short_low_explosion": "Scifi Short Low Explosion",
"amzn_sfx_scifi_small_whoosh_flyby": "Scifi Small Whoosh Flyby",
"amzn_sfx_scifi_small_zoom_flyby": "Scifi Small Zoom Flyby",
"amzn_sfx_scifi_sonar_ping_3x": "Scifi Sonar Ping 3x",
"amzn_sfx_scifi_sonar_ping_4x": "Scifi Sonar Ping 4x",
"amzn_sfx_scifi_spaceship_flyby": "Scifi Spaceship Flyby",
"amzn_sfx_scifi_timer_beep": "Scifi Timer Beep",
"amzn_sfx_scifi_zap_backwards": "Scifi Zap Backwards",
"amzn_sfx_scifi_zap_electric": "Scifi Zap Electric",
"amzn_sfx_sheep_baa": "Sheep Baa",
"amzn_sfx_sheep_bleat": "Sheep Bleat",
"amzn_sfx_silverware_clank": "Silverware Clank",
"amzn_sfx_sirens": "Sirens",
"amzn_sfx_sleigh_bells": "Sleigh Bells",
"amzn_sfx_small_stream": "Small Stream",
"amzn_sfx_sneeze": "Sneeze",
"amzn_sfx_stream": "Stream",
"amzn_sfx_strong_wind_desert": "Strong Wind Desert",
"amzn_sfx_strong_wind_whistling": "Strong Wind Whistling",
"amzn_sfx_subway_leaving": "Subway Leaving",
"amzn_sfx_subway_passing": "Subway Passing",
"amzn_sfx_subway_stopping": "Subway Stopping",
"amzn_sfx_swoosh_cartoon_fast": "Swoosh Cartoon Fast",
"amzn_sfx_swoosh_fast_1x": "Swoosh Fast 1x",
"amzn_sfx_swoosh_fast_6x": "Swoosh Fast 6x",
"amzn_sfx_test_tone": "Test Tone",
"amzn_sfx_thunder_rumble": "Thunder Rumble",
"amzn_sfx_toilet_flush": "Toilet Flush",
"amzn_sfx_trumpet_bugle": "Trumpet Bugle",
"amzn_sfx_turkey_gobbling": "Turkey Gobbling",
"amzn_sfx_typing_medium": "Typing Medium",
"amzn_sfx_typing_short": "Typing Short",
"amzn_sfx_typing_typewriter": "Typing Typewriter",
"amzn_sfx_vacuum_off": "Vacuum Off",
"amzn_sfx_vacuum_on": "Vacuum On",
"amzn_sfx_walking_in_mud": "Walking In Mud",
"amzn_sfx_walking_in_snow": "Walking In Snow",
"amzn_sfx_walking_on_grass": "Walking On Grass",
"amzn_sfx_water_dripping": "Water Dripping",
"amzn_sfx_water_droplets": "Water Droplets",
"amzn_sfx_wind_strong_gusting": "Wind Strong Gusting",
"amzn_sfx_wind_whistling_desert": "Wind Whistling Desert",
"amzn_sfx_wings_flap_4x": "Wings Flap 4x",
"amzn_sfx_wings_flap_fast": "Wings Flap Fast",
"amzn_sfx_wolf_howl": "Wolf Howl",
"amzn_sfx_wolf_young_howl": "Wolf Young Howl",
"amzn_sfx_wooden_door": "Wooden Door",
"amzn_sfx_wooden_door_creaks_long": "Wooden Door Creaks Long",
"amzn_sfx_wooden_door_creaks_multiple": "Wooden Door Creaks Multiple",
"amzn_sfx_wooden_door_creaks_open": "Wooden Door Creaks Open",
"amzn_ui_sfx_gameshow_bridge": "Gameshow Bridge",
"amzn_ui_sfx_gameshow_countdown_loop_32s_full": "Gameshow Countdown Loop 32s Full",
"amzn_ui_sfx_gameshow_countdown_loop_64s_full": "Gameshow Countdown Loop 64s Full",
"amzn_ui_sfx_gameshow_countdown_loop_64s_minimal": "Gameshow Countdown Loop 64s Minimal",
"amzn_ui_sfx_gameshow_intro": "Gameshow Intro",
"amzn_ui_sfx_gameshow_negative_response": "Gameshow Negative Response",
"amzn_ui_sfx_gameshow_neutral_response": "Gameshow Neutral Response",
"amzn_ui_sfx_gameshow_outro": "Gameshow Outro",
"amzn_ui_sfx_gameshow_player1": "Gameshow Player1",
"amzn_ui_sfx_gameshow_player2": "Gameshow Player2",
"amzn_ui_sfx_gameshow_player3": "Gameshow Player3",
"amzn_ui_sfx_gameshow_player4": "Gameshow Player4",
"amzn_ui_sfx_gameshow_positive_response": "Gameshow Positive Response",
"amzn_ui_sfx_gameshow_tally_negative": "Gameshow Tally Negative",
"amzn_ui_sfx_gameshow_tally_positive": "Gameshow Tally Positive",
"amzn_ui_sfx_gameshow_waiting_loop_30s": "Gameshow Waiting Loop 30s",
"anchor": "Anchor",
"answering_machines": "Answering Machines",
"arcs_sparks": "Arcs Sparks",
"arrows_bows": "Arrows Bows",
"baby": "Baby",
"back_up_beeps": "Back Up Beeps",
"bars_restaurants": "Bars Restaurants",
"baseball": "Baseball",
"basketball": "Basketball",
"battles": "Battles",
"beeps_tones": "Beeps Tones",
"bell": "Bell",
"bikes": "Bikes",
"billiards": "Billiards",
"board_games": "Board Games",
"body": "Body",
"boing": "Boing",
"books": "Books",
"bow_wash": "Bow Wash",
"box": "Box",
"break_shatter_smash": "Break Shatter Smash",
"breaks": "Breaks",
"brooms_mops": "Brooms Mops",
"bullets": "Bullets",
"buses": "Buses",
"buzz": "Buzz",
"buzz_hums": "Buzz Hums",
"buzzers": "Buzzers",
"buzzers_pistols": "Buzzers Pistols",
"cables_metal": "Cables Metal",
"camera": "Camera",
"cannons": "Cannons",
"car_alarm": "Car Alarm",
"car_alarms": "Car Alarms",
"car_cell_phones": "Car Cell Phones",
"carnivals_fairs": "Carnivals Fairs",
"cars": "Cars",
"casino": "Casino",
"casinos": "Casinos",
"cellar": "Cellar",
"chimes": "Chimes",
"chimes_bells": "Chimes Bells",
"chorus": "Chorus",
"christmas": "Christmas",
"church_bells": "Church Bells",
"clock": "Clock",
"cloth": "Cloth",
"concrete": "Concrete",
"construction": "Construction",
"construction_factory": "Construction Factory",
"crashes": "Crashes",
"crowds": "Crowds",
"debris": "Debris",
"dining_kitchens": "Dining Kitchens",
"dinosaurs": "Dinosaurs",
"dripping": "Dripping",
"drops": "Drops",
"electric": "Electric",
"electrical": "Electrical",
"elevator": "Elevator",
"evolution_monsters": "Evolution Monsters",
"explosions": "Explosions",
"factory": "Factory",
"falls": "Falls",
"fax_scanner_copier": "Fax Scanner Copier",
"feedback_mics": "Feedback Mics",
"fight": "Fight",
"fire": "Fire",
"fire_extinguisher": "Fire Extinguisher",
"fireballs": "Fireballs",
"fireworks": "Fireworks",
"fishing_pole": "Fishing Pole",
"flags": "Flags",
"football": "Football",
"footsteps": "Footsteps",
"futuristic": "Futuristic",
"futuristic_ship": "Futuristic Ship",
"gameshow": "Gameshow",
"gear": "Gear",
"ghosts_demons": "Ghosts Demons",
"giant_monster": "Giant Monster",
"glass": "Glass",
"glasses_clink": "Glasses Clink",
"golf": "Golf",
"gorilla": "Gorilla",
"grenade_lanucher": "Grenade Lanucher",
"griffen": "Griffen",
"gyms_locker_rooms": "Gyms Locker Rooms",
"handgun_loading": "Handgun Loading",
"handgun_shot": "Handgun Shot",
"handle": "Handle",
"hands": "Hands",
"heartbeats_ekg": "Heartbeats EKG",
"helicopter": "Helicopter",
"high_tech": "High Tech",
"hit_punch_slap": "Hit Punch Slap",
"hits": "Hits",
"horns": "Horns",
"horror": "Horror",
"hot_tub_filling_up": "Hot Tub Filling Up",
"human": "Human",
"human_vocals": "Human Vocals",
"hygene": "Hygene",
"ice_skating": "Ice Skating",
"ignitions": "Ignitions",
"infantry": "Infantry",
"intro": "Intro",
"jet": "Jet",
"juggling": "Juggling",
"key_lock": "Key Lock",
"kids": "Kids",
"knocks": "Knocks",
"lab_equip": "Lab Equip",
"lacrosse": "Lacrosse",
"lamps_lanterns": "Lamps Lanterns",
"leather": "Leather",
"liquid_suction": "Liquid Suction",
"locker_doors": "Locker Doors",
"machine_gun": "Machine Gun",
"magic_spells": "Magic Spells",
"medium_large_explosions": "Medium Large Explosions",
"metal": "Metal",
"modern_rings": "Modern Rings",
"money_coins": "Money Coins",
"motorcycles": "Motorcycles",
"movement": "Movement",
"moves": "Moves",
"nature": "Nature",
"oar_boat": "Oar Boat",
"pagers": "Pagers",
"paintball": "Paintball",
"paper": "Paper",
"parachute": "Parachute",
"pay_phones": "Pay Phones",
"phone_beeps": "Phone Beeps",
"pigmy_bats": "Pigmy Bats",
"pills": "Pills",
"pour_water": "Pour Water",
"power_up_down": "Power Up Down",
"printers": "Printers",
"prison": "Prison",
"public_space": "Public Space",
"racquetball": "Racquetball",
"radios_static": "Radios Static",
"rain": "Rain",
"rc_airplane": "RC Airplane",
"rc_car": "RC Car",
"refrigerators_freezers": "Refrigerators Freezers",
"regular": "Regular",
"respirator": "Respirator",
"rifle": "Rifle",
"roller_coaster": "Roller Coaster",
"rollerskates_rollerblades": "RollerSkates RollerBlades",
"room_tones": "Room Tones",
"ropes_climbing": "Ropes Climbing",
"rotary_rings": "Rotary Rings",
"rowboat_canoe": "Rowboat Canoe",
"rubber": "Rubber",
"running": "Running",
"sails": "Sails",
"sand_gravel": "Sand Gravel",
"screen_doors": "Screen Doors",
"screens": "Screens",
"seats_stools": "Seats Stools",
"servos": "Servos",
"shoes_boots": "Shoes Boots",
"shotgun": "Shotgun",
"shower": "Shower",
"sink_faucet": "Sink Faucet",
"sink_filling_water": "Sink Filling Water",
"sink_run_and_off": "Sink Run And Off",
"sink_water_splatter": "Sink Water Splatter",
"sirens": "Sirens",
"skateboards": "Skateboards",
"ski": "Ski",
"skids_tires": "Skids Tires",
"sled": "Sled",
"slides": "Slides",
"small_explosions": "Small Explosions",
"snow": "Snow",
"snowmobile": "Snowmobile",
"soldiers": "Soldiers",
"splash_water": "Splash Water",
"splashes_sprays": "Splashes Sprays",
"sports_whistles": "Sports Whistles",
"squeaks": "Squeaks",
"squeaky": "Squeaky",
"stairs": "Stairs",
"steam": "Steam",
"submarine_diesel": "Submarine Diesel",
"swing_doors": "Swing Doors",
"switches_levers": "Switches Levers",
"swords": "Swords",
"tape": "Tape",
"tape_machine": "Tape Machine",
"televisions_shows": "Televisions Shows",
"tennis_pingpong": "Tennis PingPong",
"textile": "Textile",
"throw": "Throw",
"thunder": "Thunder",
"ticks": "Ticks",
"timer": "Timer",
"toilet_flush": "Toilet Flush",
"tone": "Tone",
"tones_noises": "Tones Noises",
"toys": "Toys",
"tractors": "Tractors",
"traffic": "Traffic",
"train": "Train",
"trucks_vans": "Trucks Vans",
"turnstiles": "Turnstiles",
"typing": "Typing",
"umbrella": "Umbrella",
"underwater": "Underwater",
"vampires": "Vampires",
"various": "Various",
"video_tunes": "Video Tunes",
"volcano_earthquake": "Volcano Earthquake",
"watches": "Watches",
"water": "Water",
"water_running": "Water Running",
"werewolves": "Werewolves",
"winches_gears": "Winches Gears",
"wind": "Wind",
"wood": "Wood",
"wood_boat": "Wood Boat",
"woosh": "Woosh",
"zap": "Zap",
"zippers": "Zippers"
"air_horn_03": "Air horn",
"amzn_sfx_cat_meow_1x_01": "Cat meow",
"amzn_sfx_church_bell_1x_02": "Church bell",
"amzn_sfx_crowd_applause_01": "Crowd applause",
"amzn_sfx_dog_med_bark_1x_02": "Dog bark",
"amzn_sfx_doorbell_01": "Doorbell 1",
"amzn_sfx_doorbell_chime_01": "Doorbell 2",
"amzn_sfx_doorbell_chime_02": "Doorbell 3",
"amzn_sfx_large_crowd_cheer_01": "Crowd cheers",
"amzn_sfx_lion_roar_02": "Lion roar",
"amzn_sfx_rooster_crow_01": "Rooster",
"amzn_sfx_scifi_alarm_01": "Sirens",
"amzn_sfx_scifi_alarm_04": "Red alert",
"amzn_sfx_scifi_engines_on_02": "Engines on",
"amzn_sfx_scifi_sheilds_up_01": "Shields up",
"amzn_sfx_trumpet_bugle_04": "Trumpet",
"amzn_sfx_wolf_howl_02": "Wolf howl",
"bell_02": "Bells",
"boing_01": "Boing 1",
"boing_03": "Boing 2",
"buzzers_pistols_01": "Buzzer",
"camera_01": "Camera",
"christmas_05": "Christmas bells",
"clock_01": "Ticking clock",
"futuristic_10": "Aircraft",
"halloween_bats": "Halloween bats",
"halloween_crows": "Halloween crows",
"halloween_footsteps": "Halloween spooky footsteps",
"halloween_wind": "Halloween wind",
"halloween_wolf": "Halloween wolf",
"holiday_halloween_ghost": "Halloween ghost",
"horror_10": "Halloween creepy door",
"med_system_alerts_minimal_dragon_short": "Friendly dragon",
"med_system_alerts_minimal_owl_short": "Happy owl",
"med_system_alerts_minimals_blue_wave_small": "Underwater World Sonata",
"med_system_alerts_minimals_galaxy_short": "Infinite Galaxy",
"med_system_alerts_minimals_panda_short": "Baby panda",
"med_system_alerts_minimals_tiger_short": "Playful tiger",
"med_ui_success_generic_1-1": "Success 1",
"squeaky_12": "Squeaky door",
"zap_01": "Zap"
}
}
},
@@ -607,7 +187,7 @@
"message": "Invalid device ID specified: {device_id}"
},
"invalid_sound_value": {
"message": "Invalid sound {sound} with variant {variant} specified"
"message": "Invalid sound {sound} specified"
},
"entry_not_loaded": {
"message": "Entry not loaded: {entry}"
@@ -16,7 +16,7 @@ from homeassistant.helpers.selector import (
SelectSelectorMode,
)
from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN
from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN, REQUEST_TIMEOUT
API_URL = "https://app.amber.com.au/developers"
@@ -64,7 +64,9 @@ class AmberElectricConfigFlow(ConfigFlow, domain=DOMAIN):
api = amberelectric.AmberApi(api_client)
try:
sites: list[Site] = filter_sites(api.get_sites())
sites: list[Site] = filter_sites(
api.get_sites(_request_timeout=REQUEST_TIMEOUT)
)
except amberelectric.ApiException as api_exception:
if api_exception.status == 403:
self._errors[CONF_API_TOKEN] = "invalid_api_token"
@@ -21,3 +21,5 @@ SERVICE_GET_FORECASTS = "get_forecasts"
GENERAL_CHANNEL = "general"
CONTROLLED_LOAD_CHANNEL = "controlled_load"
FEED_IN_CHANNEL = "feed_in"
REQUEST_TIMEOUT = 15
@@ -16,7 +16,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import LOGGER
from .const import LOGGER, REQUEST_TIMEOUT
from .helpers import normalize_descriptor
type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator]
@@ -82,7 +82,11 @@ class AmberUpdateCoordinator(DataUpdateCoordinator):
"grid": {},
}
try:
data = self._api.get_current_prices(self.site_id, next=288)
data = self._api.get_current_prices(
self.site_id,
next=288,
_request_timeout=REQUEST_TIMEOUT,
)
intervals = [interval.actual_instance for interval in data]
except ApiException as api_exception:
raise UpdateFailed("Missing price data, skipping update") from api_exception
@@ -449,5 +449,6 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
return {
"version": "home-assistant:1",
"home_assistant": HA_VERSION,
"devices": devices,
}
@@ -129,9 +129,9 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
entity_disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY
and not all_disabled
):
# Device and entity registries don't update the disabled_by flag
# when moving a device or entity from one config entry to another,
# so we need to do it manually.
# Device and entity registries will set the disabled_by flag to None
# when moving a device or entity disabled by CONFIG_ENTRY to an enabled
# config entry, but we want to set it to DEVICE or USER instead,
entity_disabled_by = (
er.RegistryEntryDisabler.DEVICE
if device
@@ -146,9 +146,9 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
)
if device is not None:
# Device and entity registries don't update the disabled_by flag when
# moving a device or entity from one config entry to another, so we
# need to do it manually.
# Device and entity registries will set the disabled_by flag to None
# when moving a device or entity disabled by CONFIG_ENTRY to an enabled
# config entry, but we want to set it to USER instead,
device_disabled_by = device.disabled_by
if (
device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY
@@ -10,9 +10,9 @@ from homeassistant.components.binary_sensor import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator
from .entity import APCUPSdEntity
PARALLEL_UPDATES = 0
@@ -40,22 +40,16 @@ async def async_setup_entry(
async_add_entities([OnlineStatus(coordinator, _DESCRIPTION)])
class OnlineStatus(CoordinatorEntity[APCUPSdCoordinator], BinarySensorEntity):
class OnlineStatus(APCUPSdEntity, BinarySensorEntity):
"""Representation of a UPS online status."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: APCUPSdCoordinator,
description: BinarySensorEntityDescription,
) -> None:
"""Initialize the APCUPSd binary device."""
super().__init__(coordinator, context=description.key.upper())
self.entity_description = description
self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}"
self._attr_device_info = coordinator.device_info
super().__init__(coordinator, description)
@property
def is_on(self) -> bool | None:
@@ -0,0 +1,26 @@
"""Base entity for APCUPSd integration."""
from __future__ import annotations
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import APCUPSdCoordinator
class APCUPSdEntity(CoordinatorEntity[APCUPSdCoordinator]):
"""Base entity for APCUPSd integration."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: APCUPSdCoordinator,
description: EntityDescription,
) -> None:
"""Initialize the APCUPSd entity."""
super().__init__(coordinator, context=description.key.upper())
self.entity_description = description
self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}"
self._attr_device_info = coordinator.device_info
@@ -3,10 +3,7 @@ rules:
action-setup: done
appropriate-polling: done
brands: done
common-modules:
status: done
comment: |
Consider deriving a base entity.
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
+3 -9
View File
@@ -23,10 +23,10 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import LAST_S_TEST
from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator
from .entity import APCUPSdEntity
PARALLEL_UPDATES = 0
@@ -490,22 +490,16 @@ def infer_unit(value: str) -> tuple[str, str | None]:
return value, None
class APCUPSdSensor(CoordinatorEntity[APCUPSdCoordinator], SensorEntity):
class APCUPSdSensor(APCUPSdEntity, SensorEntity):
"""Representation of a sensor entity for APCUPSd status values."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: APCUPSdCoordinator,
description: SensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator=coordinator, context=description.key.upper())
self.entity_description = description
self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}"
self._attr_device_info = coordinator.device_info
super().__init__(coordinator, description)
# Initial update of attributes.
self._update_attrs()
-1
View File
@@ -1 +0,0 @@
"""Virtual integration: Arizona Public Service (APS)."""
@@ -1,6 +0,0 @@
{
"domain": "aps",
"name": "Arizona Public Service (APS)",
"integration_type": "virtual",
"supported_by": "opower"
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.1.0"]
"requirements": ["hassil==3.2.0"]
}
+24 -4
View File
@@ -12,9 +12,11 @@ from typing import Any, cast
from aioasuswrt.asuswrt import AsusWrt as AsusWrtLegacy
from aiohttp import ClientSession
from asusrouter import AsusRouter, AsusRouterError
from asusrouter.config import ARConfigKey
from asusrouter.modules.client import AsusClient
from asusrouter.modules.data import AsusData
from asusrouter.modules.homeassistant import convert_to_ha_data, convert_to_ha_sensors
from asusrouter.tools.connection import get_cookie_jar
from homeassistant.const import (
CONF_HOST,
@@ -25,7 +27,7 @@ from homeassistant.const import (
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.update_coordinator import UpdateFailed
@@ -109,7 +111,10 @@ class AsusWrtBridge(ABC):
) -> AsusWrtBridge:
"""Get Bridge instance."""
if conf[CONF_PROTOCOL] in (PROTOCOL_HTTPS, PROTOCOL_HTTP):
session = async_get_clientsession(hass)
session = async_create_clientsession(
hass,
cookie_jar=get_cookie_jar(),
)
return AsusWrtHttpBridge(conf, session)
return AsusWrtLegacyBridge(conf, options)
@@ -310,10 +315,14 @@ class AsusWrtHttpBridge(AsusWrtBridge):
def __init__(self, conf: dict[str, Any], session: ClientSession) -> None:
"""Initialize Bridge that use HTTP library."""
super().__init__(conf[CONF_HOST])
self._api = self._get_api(conf, session)
# Get API configuration
config = self._get_api_config()
self._api = self._get_api(conf, session, config)
@staticmethod
def _get_api(conf: dict[str, Any], session: ClientSession) -> AsusRouter:
def _get_api(
conf: dict[str, Any], session: ClientSession, config: dict[ARConfigKey, Any]
) -> AsusRouter:
"""Get the AsusRouter API."""
return AsusRouter(
hostname=conf[CONF_HOST],
@@ -322,8 +331,19 @@ class AsusWrtHttpBridge(AsusWrtBridge):
use_ssl=conf[CONF_PROTOCOL] == PROTOCOL_HTTPS,
port=conf.get(CONF_PORT),
session=session,
config=config,
)
def _get_api_config(self) -> dict[ARConfigKey, Any]:
"""Get configuration for the API."""
return {
# Enable automatic temperature data correction in the library
ARConfigKey.OPTIMISTIC_TEMPERATURE: True,
# Disable `warning`-level log message when temperature
# is corrected by setting it to already notified.
ARConfigKey.NOTIFIED_OPTIMISTIC_TEMPERATURE: True,
}
@property
def is_connected(self) -> bool:
"""Get connected status."""
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioasuswrt", "asusrouter", "asyncssh"],
"requirements": ["aioasuswrt==1.4.0", "asusrouter==1.19.0"]
"requirements": ["aioasuswrt==1.4.0", "asusrouter==1.20.1"]
}
+20 -28
View File
@@ -6,18 +6,21 @@ from pathlib import Path
from typing import cast
from aiohttp import ClientResponseError
from yalexs.const import Brand
from yalexs.exceptions import AugustApiAIOHTTPError
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
from yalexs.manager.gateway import Config as YaleXSConfig
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from homeassistant.helpers import (
config_entry_oauth2_flow,
device_registry as dr,
issue_registry as ir,
)
from .const import DOMAIN, PLATFORMS
from .const import DEFAULT_AUGUST_BRAND, DOMAIN, PLATFORMS
from .data import AugustData
from .gateway import AugustGateway
from .util import async_create_august_clientsession
@@ -25,30 +28,21 @@ from .util import async_create_august_clientsession
type AugustConfigEntry = ConfigEntry[AugustData]
@callback
def _async_create_yale_brand_migration_issue(
hass: HomeAssistant, entry: AugustConfigEntry
) -> None:
"""Create an issue for a brand migration."""
ir.async_create_issue(
hass,
DOMAIN,
"yale_brand_migration",
breaks_in_ha_version="2024.9",
learn_more_url="https://www.home-assistant.io/integrations/yale",
translation_key="yale_brand_migration",
is_fixable=False,
severity=ir.IssueSeverity.CRITICAL,
translation_placeholders={
"migrate_url": "https://my.home-assistant.io/redirect/config_flow_start?domain=yale"
},
)
async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bool:
"""Set up August from a config entry."""
# Check if this is a legacy config entry that needs migration to OAuth
if "auth_implementation" not in entry.data:
# This is a legacy entry using username/password, trigger reauth
raise ConfigEntryAuthFailed("Migration to OAuth required")
session = async_create_august_clientsession(hass)
august_gateway = AugustGateway(Path(hass.config.config_dir), session)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session)
try:
await async_setup_august(hass, entry, august_gateway)
except (RequireValidation, InvalidAuth) as err:
@@ -76,9 +70,7 @@ async def async_setup_august(
) -> None:
"""Set up the August component."""
config = cast(YaleXSConfig, entry.data)
await august_gateway.async_setup(config)
if august_gateway.api.brand == Brand.YALE_HOME:
_async_create_yale_brand_migration_issue(hass, entry)
await august_gateway.async_setup({**config, "brand": DEFAULT_AUGUST_BRAND})
await august_gateway.async_authenticate()
await august_gateway.async_refresh_access_token_if_needed()
data = entry.runtime_data = AugustData(hass, august_gateway)
@@ -0,0 +1,15 @@
"""application_credentials platform for the august integration."""
from homeassistant.components.application_credentials import AuthorizationServer
from homeassistant.core import HomeAssistant
OAUTH2_AUTHORIZE = "https://auth.august.com/authorization"
OAUTH2_TOKEN = "https://auth.august.com/access_token"
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
"""Return authorization server."""
return AuthorizationServer(
authorize_url=OAUTH2_AUTHORIZE,
token_url=OAUTH2_TOKEN,
)
+58 -256
View File
@@ -1,284 +1,86 @@
"""Config flow for August integration."""
from collections.abc import Mapping
from dataclasses import dataclass
import logging
from pathlib import Path
from typing import Any
import aiohttp
import voluptuous as vol
from yalexs.authenticator_common import ValidationResult
from yalexs.const import BRANDS_WITHOUT_OAUTH, DEFAULT_BRAND, Brand
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
import jwt
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from .const import (
CONF_ACCESS_TOKEN_CACHE_FILE,
CONF_BRAND,
CONF_LOGIN_METHOD,
DEFAULT_LOGIN_METHOD,
DOMAIN,
LOGIN_METHODS,
VERIFICATION_CODE_KEY,
)
from .gateway import AugustGateway
from .util import async_create_august_clientsession
# The Yale Home Brand is not supported by the August integration
# anymore and should migrate to the Yale integration
AVAILABLE_BRANDS = BRANDS_WITHOUT_OAUTH.copy()
del AVAILABLE_BRANDS[Brand.YALE_HOME]
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.helpers import config_entry_oauth2_flow
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
async def async_validate_input(
data: dict[str, Any], august_gateway: AugustGateway
) -> dict[str, Any]:
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
Request configuration steps from the user.
"""
assert august_gateway.authenticator is not None
authenticator = august_gateway.authenticator
if (code := data.get(VERIFICATION_CODE_KEY)) is not None:
result = await authenticator.async_validate_verification_code(code)
_LOGGER.debug("Verification code validation: %s", result)
if result != ValidationResult.VALIDATED:
raise RequireValidation
try:
await august_gateway.async_authenticate()
except RequireValidation:
_LOGGER.debug(
"Requesting new verification code for %s via %s",
data.get(CONF_USERNAME),
data.get(CONF_LOGIN_METHOD),
)
if code is None:
await august_gateway.authenticator.async_send_verification_code()
raise
return {
"title": data.get(CONF_USERNAME),
"data": august_gateway.config_entry(),
}
@dataclass(slots=True)
class ValidateResult:
"""Result from validation."""
validation_required: bool
info: dict[str, Any]
errors: dict[str, str]
description_placeholders: dict[str, str]
class AugustConfigFlow(ConfigFlow, domain=DOMAIN):
class AugustConfigFlow(
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
):
"""Handle a config flow for August."""
VERSION = 1
DOMAIN = DOMAIN
def __init__(self) -> None:
"""Store an AugustGateway()."""
self._august_gateway: AugustGateway | None = None
self._aiohttp_session: aiohttp.ClientSession | None = None
self._user_auth_details: dict[str, Any] = {}
self._needs_reset = True
super().__init__()
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
return await self.async_step_user_validate()
async def async_step_user_validate(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle authentication."""
errors: dict[str, str] = {}
description_placeholders: dict[str, str] = {}
if user_input is not None:
self._user_auth_details.update(user_input)
validate_result = await self._async_auth_or_validate()
description_placeholders = validate_result.description_placeholders
if validate_result.validation_required:
return await self.async_step_validation()
if not (errors := validate_result.errors):
return await self._async_update_or_create_entry(validate_result.info)
return self.async_show_form(
step_id="user_validate",
data_schema=vol.Schema(
{
vol.Required(
CONF_BRAND,
default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND),
): vol.In(AVAILABLE_BRANDS),
vol.Required(
CONF_LOGIN_METHOD,
default=self._user_auth_details.get(
CONF_LOGIN_METHOD, DEFAULT_LOGIN_METHOD
),
): vol.In(LOGIN_METHODS),
vol.Required(
CONF_USERNAME,
default=self._user_auth_details.get(CONF_USERNAME),
): str,
vol.Required(CONF_PASSWORD): str,
}
),
errors=errors,
description_placeholders=description_placeholders,
)
async def async_step_validation(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle validation (2fa) step."""
if user_input:
if self.source == SOURCE_REAUTH:
return await self.async_step_reauth_validate(user_input)
return await self.async_step_user_validate(user_input)
previously_failed = VERIFICATION_CODE_KEY in self._user_auth_details
return self.async_show_form(
step_id="validation",
data_schema=vol.Schema(
{vol.Required(VERIFICATION_CODE_KEY): vol.All(str, vol.Strip)}
),
errors={"base": "invalid_verification_code"} if previously_failed else None,
description_placeholders={
CONF_BRAND: self._user_auth_details[CONF_BRAND],
CONF_USERNAME: self._user_auth_details[CONF_USERNAME],
CONF_LOGIN_METHOD: self._user_auth_details[CONF_LOGIN_METHOD],
},
)
@callback
def _async_get_gateway(self) -> AugustGateway:
"""Set up the gateway."""
if self._august_gateway is not None:
return self._august_gateway
self._aiohttp_session = async_create_august_clientsession(self.hass)
self._august_gateway = AugustGateway(
Path(self.hass.config.config_dir), self._aiohttp_session
)
return self._august_gateway
@callback
def _async_shutdown_gateway(self) -> None:
"""Shutdown the gateway."""
if self._aiohttp_session is not None:
self._aiohttp_session.detach()
self._august_gateway = None
@property
def logger(self) -> logging.Logger:
"""Return logger."""
return _LOGGER
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
self._user_auth_details = dict(entry_data)
return await self.async_step_reauth_validate()
return await self.async_step_user()
async def async_step_reauth_validate(
self, user_input: dict[str, Any] | None = None
def _async_decode_jwt(self, encoded: str) -> dict[str, Any]:
"""Decode JWT token."""
return jwt.decode(
encoded,
"",
verify=False,
options={"verify_signature": False},
algorithms=["HS256"],
)
async def _async_handle_reauth(
self, data: dict, decoded: dict[str, Any], user_id: str
) -> ConfigFlowResult:
"""Handle reauth and validation."""
errors: dict[str, str] = {}
description_placeholders: dict[str, str] = {}
if user_input is not None:
self._user_auth_details.update(user_input)
validate_result = await self._async_auth_or_validate()
description_placeholders = validate_result.description_placeholders
if validate_result.validation_required:
return await self.async_step_validation()
if not (errors := validate_result.errors):
return await self._async_update_or_create_entry(validate_result.info)
"""Handle reauth flow."""
reauth_entry = self._get_reauth_entry()
assert reauth_entry.unique_id is not None
# Check if this is a migration from username (contains @) to user ID
if "@" not in reauth_entry.unique_id:
# This is a normal oauth reauth, enforce ID matching for security
await self.async_set_unique_id(user_id)
self._abort_if_unique_id_mismatch(reason="reauth_invalid_user")
return self.async_update_reload_and_abort(reauth_entry, data=data)
return self.async_show_form(
step_id="reauth_validate",
data_schema=vol.Schema(
{
vol.Required(
CONF_BRAND,
default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND),
): vol.In(BRANDS_WITHOUT_OAUTH),
vol.Required(CONF_PASSWORD): str,
}
),
errors=errors,
description_placeholders=description_placeholders
| {
CONF_USERNAME: self._user_auth_details[CONF_USERNAME],
},
# This is a one-time migration from username to user ID
# Only validate if the account has emails
emails: list[str]
if emails := decoded.get("email", []):
# Validate that the email matches before allowing migration
email_to_check_lower = reauth_entry.unique_id.casefold()
if not any(email.casefold() == email_to_check_lower for email in emails):
# Email doesn't match - this is a different account
return self.async_abort(reason="reauth_invalid_user")
# Email matches or no emails on account, update with new unique ID
return self.async_update_reload_and_abort(
reauth_entry, data=data, unique_id=user_id
)
async def _async_reset_access_token_cache_if_needed(
self, gateway: AugustGateway, username: str, access_token_cache_file: str | None
) -> None:
"""Reset the access token cache if needed."""
# We need to configure the access token cache file before we setup the gateway
# since we need to reset it if the brand changes BEFORE we setup the gateway
gateway.async_configure_access_token_cache_file(
username, access_token_cache_file
)
if self._needs_reset:
self._needs_reset = False
await gateway.async_reset_authentication()
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
"""Create an entry for the flow."""
# Decode JWT once
access_token = data["token"]["access_token"]
decoded = self._async_decode_jwt(access_token)
user_id = decoded["userId"]
async def _async_auth_or_validate(self) -> ValidateResult:
"""Authenticate or validate."""
user_auth_details = self._user_auth_details
gateway = self._async_get_gateway()
assert gateway is not None
await self._async_reset_access_token_cache_if_needed(
gateway,
user_auth_details[CONF_USERNAME],
user_auth_details.get(CONF_ACCESS_TOKEN_CACHE_FILE),
)
await gateway.async_setup(user_auth_details)
if self.source == SOURCE_REAUTH:
return await self._async_handle_reauth(data, decoded, user_id)
errors: dict[str, str] = {}
info: dict[str, Any] = {}
description_placeholders: dict[str, str] = {}
validation_required = False
try:
info = await async_validate_input(user_auth_details, gateway)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except RequireValidation:
validation_required = True
except Exception as ex:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unhandled"
description_placeholders = {"error": str(ex)}
return ValidateResult(
validation_required, info, errors, description_placeholders
)
async def _async_update_or_create_entry(
self, info: dict[str, Any]
) -> ConfigFlowResult:
"""Update existing entry or create a new one."""
self._async_shutdown_gateway()
existing_entry = await self.async_set_unique_id(
self._user_auth_details[CONF_USERNAME]
)
if not existing_entry:
return self.async_create_entry(title=info["title"], data=info["data"])
return self.async_update_reload_and_abort(existing_entry, data=info["data"])
await self.async_set_unique_id(user_id)
self._abort_if_unique_id_configured()
return await super().async_oauth_create_entry(data)
+4
View File
@@ -1,5 +1,7 @@
"""Constants for August devices."""
from yalexs.const import Brand
from homeassistant.const import Platform
DEFAULT_TIMEOUT = 25
@@ -9,6 +11,8 @@ CONF_BRAND = "brand"
CONF_LOGIN_METHOD = "login_method"
CONF_INSTALL_ID = "install_id"
DEFAULT_AUGUST_BRAND = Brand.YALE_AUGUST
VERIFICATION_CODE_KEY = "verification_code"
NOTIFICATION_ID = "august_notification"
+32 -19
View File
@@ -1,30 +1,43 @@
"""Handle August connection setup and authentication."""
from typing import Any
import logging
from pathlib import Path
from yalexs.const import DEFAULT_BRAND
from aiohttp import ClientSession
from yalexs.authenticator_common import Authentication, AuthenticationState
from yalexs.manager.gateway import Gateway
from homeassistant.const import CONF_USERNAME
from homeassistant.helpers import config_entry_oauth2_flow
from .const import (
CONF_ACCESS_TOKEN_CACHE_FILE,
CONF_BRAND,
CONF_INSTALL_ID,
CONF_LOGIN_METHOD,
)
_LOGGER = logging.getLogger(__name__)
class AugustGateway(Gateway):
"""Handle the connection to August."""
def config_entry(self) -> dict[str, Any]:
"""Config entry."""
assert self._config is not None
return {
CONF_BRAND: self._config.get(CONF_BRAND, DEFAULT_BRAND),
CONF_LOGIN_METHOD: self._config[CONF_LOGIN_METHOD],
CONF_USERNAME: self._config[CONF_USERNAME],
CONF_INSTALL_ID: self._config.get(CONF_INSTALL_ID),
CONF_ACCESS_TOKEN_CACHE_FILE: self._access_token_cache_file,
}
def __init__(
self,
config_path: Path,
aiohttp_session: ClientSession,
oauth_session: config_entry_oauth2_flow.OAuth2Session,
) -> None:
"""Init the connection."""
super().__init__(config_path, aiohttp_session)
self._oauth_session = oauth_session
async def async_get_access_token(self) -> str:
"""Get access token."""
await self._oauth_session.async_ensure_token_valid()
return self._oauth_session.token["access_token"]
async def async_refresh_access_token_if_needed(self) -> None:
"""Refresh the access token if needed."""
await self._oauth_session.async_ensure_token_valid()
async def async_authenticate(self) -> Authentication:
"""Authenticate with the details provided to setup."""
await self._oauth_session.async_ensure_token_valid()
self.authentication = Authentication(
AuthenticationState.AUTHENTICATED, None, None, None
)
return self.authentication
@@ -3,6 +3,7 @@
"name": "August",
"codeowners": ["@bdraco"],
"config_flow": true,
"dependencies": ["application_credentials", "cloud"],
"dhcp": [
{
"hostname": "connect",
@@ -28,5 +29,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==8.11.1", "yalexs-ble==3.1.2"]
"requirements": ["yalexs==9.0.1", "yalexs-ble==3.1.2"]
}
+24 -32
View File
@@ -6,42 +6,34 @@
}
},
"config": {
"error": {
"unhandled": "Unhandled error: {error}",
"invalid_verification_code": "Invalid verification code",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
"step": {
"pick_implementation": {
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]",
"data": {
"implementation": "[%key:common::config_flow::data::implementation%]"
},
"data_description": {
"implementation": "[%key:common::config_flow::description::implementation%]"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"missing_credentials": "[%key:common::config_flow::abort::oauth2_missing_credentials%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
"reauth_invalid_user": "Reauthenticate must use the same account."
},
"step": {
"validation": {
"title": "Two-factor authentication",
"data": {
"verification_code": "Verification code"
},
"description": "Please check your {login_method} ({username}) and enter the verification code below. Codes may take a few minutes to arrive."
},
"user_validate": {
"description": "It is recommended to use the 'email' login method as some brands may not work with the 'phone' method. If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'. If you choose the wrong brand, you may be able to authenticate initially; however, you will not be able to operate devices. If you are unsure of the brand, create the integration again and try another brand.",
"data": {
"brand": "Brand",
"login_method": "Login Method",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"title": "Set up an August account"
},
"reauth_validate": {
"description": "Choose the correct brand for your device, and enter the password for {username}. If you choose the wrong brand, you may be able to authenticate initially; however, you will not be able to operate devices. If you are unsure of the brand, create the integration again and try another brand.",
"data": {
"brand": "[%key:component::august::config::step::user_validate::data::brand%]",
"password": "[%key:common::config_flow::data::password%]"
},
"title": "Reauthenticate an August account"
}
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
}
},
"entity": {
+9 -13
View File
@@ -199,23 +199,19 @@ class AuthProvidersView(HomeAssistantView):
)
def _prepare_result_json(
result: AuthFlowResult,
) -> AuthFlowResult:
"""Convert result to JSON."""
def _prepare_result_json(result: AuthFlowResult) -> dict[str, Any]:
"""Convert result to JSON serializable dict."""
if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY:
data = result.copy()
data.pop("result")
data.pop("data")
return data
return {
key: val for key, val in result.items() if key not in ("result", "data")
}
if result["type"] != data_entry_flow.FlowResultType.FORM:
return result
return result # type: ignore[return-value]
data = result.copy()
if (schema := data["data_schema"]) is None:
data["data_schema"] = [] # type: ignore[typeddict-item] # json result type
data = dict(result)
if (schema := result["data_schema"]) is None:
data["data_schema"] = []
else:
data["data_schema"] = voluptuous_serialize.convert(schema)
@@ -149,20 +149,16 @@ def websocket_depose_mfa(
hass.async_create_task(async_depose(msg))
def _prepare_result_json(
result: data_entry_flow.FlowResult,
) -> data_entry_flow.FlowResult:
"""Convert result to JSON."""
def _prepare_result_json(result: data_entry_flow.FlowResult) -> dict[str, Any]:
"""Convert result to JSON serializable dict."""
if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY:
return result.copy()
return dict(result)
if result["type"] != data_entry_flow.FlowResultType.FORM:
return result
return result # type: ignore[return-value]
data = result.copy()
if (schema := data["data_schema"]) is None:
data["data_schema"] = [] # type: ignore[typeddict-item] # json result type
data = dict(result)
if (schema := result["data_schema"]) is None:
data["data_schema"] = []
else:
data["data_schema"] = voluptuous_serialize.convert(schema)
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/azure_devops",
"iot_class": "cloud_polling",
"loggers": ["aioazuredevops"],
"requirements": ["aioazuredevops==2.2.1"]
"requirements": ["aioazuredevops==2.2.2"]
}
+2 -1
View File
@@ -896,7 +896,8 @@ class BackupManager:
)
agent_errors = {
backup_id: error
for backup_id, error in zip(backup_ids, delete_results, strict=True)
for backup_id, error_dict in zip(backup_ids, delete_results, strict=True)
for error in error_dict.values()
if error and not isinstance(error, BackupNotFound)
}
if agent_errors:
+21 -3
View File
@@ -1,6 +1,24 @@
"""The bayesian component."""
from homeassistant.const import Platform
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
DOMAIN = "bayesian"
PLATFORMS = [Platform.BINARY_SENSOR]
from .const import PLATFORMS
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Bayesian from a config entry."""
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Bayesian config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
hass.config_entries.async_schedule_reload(entry.entry_id)
@@ -16,6 +16,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_ABOVE,
CONF_BELOW,
@@ -32,7 +33,10 @@ from homeassistant.const import (
from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback
from homeassistant.exceptions import ConditionError, TemplateError
from homeassistant.helpers import condition, config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.helpers.event import (
TrackTemplate,
TrackTemplateResult,
@@ -44,7 +48,6 @@ from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.template import Template, result_as_boolean
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN, PLATFORMS
from .const import (
ATTR_OBSERVATIONS,
ATTR_OCCURRED_OBSERVATION_ENTITIES,
@@ -60,6 +63,8 @@ from .const import (
CONF_TO_STATE,
DEFAULT_NAME,
DEFAULT_PROBABILITY_THRESHOLD,
DOMAIN,
PLATFORMS,
)
from .helpers import Observation
from .issues import raise_mirrored_entries, raise_no_prob_given_false
@@ -67,7 +72,13 @@ from .issues import raise_mirrored_entries, raise_no_prob_given_false
_LOGGER = logging.getLogger(__name__)
def _above_greater_than_below(config: dict[str, Any]) -> dict[str, Any]:
def above_greater_than_below(config: dict[str, Any]) -> dict[str, Any]:
"""Validate above and below options.
If the observation is of type/platform NUMERIC_STATE, then ensure that the
value given for 'above' is not greater than that for 'below'. Also check
that at least one of the two is specified.
"""
if config[CONF_PLATFORM] == CONF_NUMERIC_STATE:
above = config.get(CONF_ABOVE)
below = config.get(CONF_BELOW)
@@ -76,9 +87,7 @@ def _above_greater_than_below(config: dict[str, Any]) -> dict[str, Any]:
"For bayesian numeric state for entity: %s at least one of 'above' or 'below' must be specified",
config[CONF_ENTITY_ID],
)
raise vol.Invalid(
"For bayesian numeric state at least one of 'above' or 'below' must be specified."
)
raise vol.Invalid("above_or_below")
if above is not None and below is not None:
if above > below:
_LOGGER.error(
@@ -86,7 +95,7 @@ def _above_greater_than_below(config: dict[str, Any]) -> dict[str, Any]:
above,
below,
)
raise vol.Invalid("'above' is greater than 'below'")
raise vol.Invalid("above_below")
return config
@@ -102,11 +111,16 @@ NUMERIC_STATE_SCHEMA = vol.All(
},
required=True,
),
_above_greater_than_below,
above_greater_than_below,
)
def _no_overlapping(configs: list[dict]) -> list[dict]:
def no_overlapping(configs: list[dict]) -> list[dict]:
"""Validate that intervals are not overlapping.
For a list of observations ensure that there are no overlapping intervals
for NUMERIC_STATE observations for the same entity.
"""
numeric_configs = [
config for config in configs if config[CONF_PLATFORM] == CONF_NUMERIC_STATE
]
@@ -129,11 +143,16 @@ def _no_overlapping(configs: list[dict]) -> list[dict]:
for i, tup in enumerate(intervals):
if len(intervals) > i + 1 and tup.below > intervals[i + 1].above:
_LOGGER.error(
"Ranges for bayesian numeric state entities must not overlap, but %s has overlapping ranges, above:%s, below:%s overlaps with above:%s, below:%s",
ent_id,
tup.above,
tup.below,
intervals[i + 1].above,
intervals[i + 1].below,
)
raise vol.Invalid(
"Ranges for bayesian numeric state entities must not overlap, "
f"but {ent_id} has overlapping ranges, above:{tup.above}, "
f"below:{tup.below} overlaps with above:{intervals[i + 1].above}, "
f"below:{intervals[i + 1].below}."
"overlapping_ranges",
)
return configs
@@ -168,7 +187,7 @@ PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend(
vol.All(
cv.ensure_list,
[vol.Any(TEMPLATE_SCHEMA, STATE_SCHEMA, NUMERIC_STATE_SCHEMA)],
_no_overlapping,
no_overlapping,
)
),
vol.Required(CONF_PRIOR): vol.Coerce(float),
@@ -194,9 +213,13 @@ async def async_setup_platform(
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Bayesian Binary sensor."""
"""Set up the Bayesian Binary sensor from a yaml config."""
_LOGGER.debug(
"Setting up config entry for Bayesian sensor: '%s' with %s observations",
config[CONF_NAME],
len(config.get(CONF_OBSERVATIONS, [])),
)
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
name: str = config[CONF_NAME]
unique_id: str | None = config.get(CONF_UNIQUE_ID)
observations: list[ConfigType] = config[CONF_OBSERVATIONS]
@@ -231,6 +254,42 @@ async def async_setup_platform(
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Bayesian Binary sensor from a config entry."""
_LOGGER.debug(
"Setting up config entry for Bayesian sensor: '%s' with %s observations",
config_entry.options[CONF_NAME],
len(config_entry.subentries),
)
config = config_entry.options
name: str = config[CONF_NAME]
unique_id: str | None = config.get(CONF_UNIQUE_ID, config_entry.entry_id)
observations: list[ConfigType] = [
dict(subentry.data) for subentry in config_entry.subentries.values()
]
prior: float = config[CONF_PRIOR]
probability_threshold: float = config[CONF_PROBABILITY_THRESHOLD]
device_class: BinarySensorDeviceClass | None = config.get(CONF_DEVICE_CLASS)
async_add_entities(
[
BayesianBinarySensor(
name,
unique_id,
prior,
observations,
probability_threshold,
device_class,
)
]
)
class BayesianBinarySensor(BinarySensorEntity):
"""Representation of a Bayesian sensor."""
@@ -248,6 +307,7 @@ class BayesianBinarySensor(BinarySensorEntity):
"""Initialize the Bayesian sensor."""
self._attr_name = name
self._attr_unique_id = unique_id and f"bayesian-{unique_id}"
self._observations = [
Observation(
entity_id=observation.get(CONF_ENTITY_ID),
@@ -432,7 +492,7 @@ class BayesianBinarySensor(BinarySensorEntity):
1 - observation.prob_given_false,
)
continue
# observation.observed is None
# Entity exists but observation.observed is None
if observation.entity_id is not None:
_LOGGER.debug(
(
@@ -495,7 +555,10 @@ class BayesianBinarySensor(BinarySensorEntity):
for observation in self._observations:
if observation.value_template is None:
continue
if isinstance(observation.value_template, str):
observation.value_template = Template(
observation.value_template, hass=self.hass
)
template = observation.value_template
observations_by_template.setdefault(template, []).append(observation)
@@ -0,0 +1,646 @@
"""Config flow for the Bayesian integration."""
from collections.abc import Mapping
from enum import StrEnum
import logging
from typing import Any
import voluptuous as vol
from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
)
from homeassistant.components.calendar import DOMAIN as CALENDAR_DOMAIN
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN
from homeassistant.components.input_boolean import DOMAIN as INPUT_BOOLEAN_DOMAIN
from homeassistant.components.input_number import DOMAIN as INPUT_NUMBER_DOMAIN
from homeassistant.components.input_text import DOMAIN as INPUT_TEXT_DOMAIN
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
from homeassistant.components.person import DOMAIN as PERSON_DOMAIN
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.sun import DOMAIN as SUN_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.todo import DOMAIN as TODO_DOMAIN
from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN
from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlowResult,
ConfigSubentry,
ConfigSubentryData,
ConfigSubentryFlow,
SubentryFlowResult,
)
from homeassistant.const import (
CONF_ABOVE,
CONF_BELOW,
CONF_DEVICE_CLASS,
CONF_ENTITY_ID,
CONF_NAME,
CONF_PLATFORM,
CONF_STATE,
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import callback
from homeassistant.helpers import selector, translation
from homeassistant.helpers.schema_config_entry_flow import (
SchemaCommonFlowHandler,
SchemaConfigFlowHandler,
SchemaFlowError,
SchemaFlowFormStep,
SchemaFlowMenuStep,
)
from .binary_sensor import above_greater_than_below, no_overlapping
from .const import (
CONF_OBSERVATIONS,
CONF_P_GIVEN_F,
CONF_P_GIVEN_T,
CONF_PRIOR,
CONF_PROBABILITY_THRESHOLD,
CONF_TEMPLATE,
CONF_TO_STATE,
DEFAULT_NAME,
DEFAULT_PROBABILITY_THRESHOLD,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
USER = "user"
OBSERVATION_SELECTOR = "observation_selector"
ALLOWED_STATE_DOMAINS = [
ALARM_DOMAIN,
BINARY_SENSOR_DOMAIN,
CALENDAR_DOMAIN,
CLIMATE_DOMAIN,
COVER_DOMAIN,
DEVICE_TRACKER_DOMAIN,
INPUT_BOOLEAN_DOMAIN,
INPUT_NUMBER_DOMAIN,
INPUT_TEXT_DOMAIN,
LIGHT_DOMAIN,
MEDIA_PLAYER_DOMAIN,
NOTIFY_DOMAIN,
NUMBER_DOMAIN,
PERSON_DOMAIN,
"schedule", # Avoids an import that would introduce a dependency.
SELECT_DOMAIN,
SENSOR_DOMAIN,
SUN_DOMAIN,
SWITCH_DOMAIN,
TODO_DOMAIN,
UPDATE_DOMAIN,
WEATHER_DOMAIN,
]
ALLOWED_NUMERIC_DOMAINS = [
SENSOR_DOMAIN,
INPUT_NUMBER_DOMAIN,
NUMBER_DOMAIN,
TODO_DOMAIN,
ZONE_DOMAIN,
]
class ObservationTypes(StrEnum):
"""StrEnum for all the different observation types."""
STATE = CONF_STATE
NUMERIC_STATE = "numeric_state"
TEMPLATE = CONF_TEMPLATE
class OptionsFlowSteps(StrEnum):
"""StrEnum for all the different options flow steps."""
INIT = "init"
ADD_OBSERVATION = OBSERVATION_SELECTOR
OPTIONS_SCHEMA = vol.Schema(
{
vol.Required(
CONF_PROBABILITY_THRESHOLD, default=DEFAULT_PROBABILITY_THRESHOLD * 100
): vol.All(
selector.NumberSelector(
selector.NumberSelectorConfig(
mode=selector.NumberSelectorMode.SLIDER,
step=1.0,
min=0,
max=100,
unit_of_measurement="%",
),
),
vol.Range(
min=0,
max=100,
min_included=False,
max_included=False,
msg="extreme_threshold_error",
),
),
vol.Required(CONF_PRIOR, default=DEFAULT_PROBABILITY_THRESHOLD * 100): vol.All(
selector.NumberSelector(
selector.NumberSelectorConfig(
mode=selector.NumberSelectorMode.SLIDER,
step=1.0,
min=0,
max=100,
unit_of_measurement="%",
),
),
vol.Range(
min=0,
max=100,
min_included=False,
max_included=False,
msg="extreme_prior_error",
),
),
vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector(
selector.SelectSelectorConfig(
options=[cls.value for cls in BinarySensorDeviceClass],
mode=selector.SelectSelectorMode.DROPDOWN,
translation_key="binary_sensor_device_class",
sort=True,
),
),
}
)
CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME, default=DEFAULT_NAME): selector.TextSelector(),
}
).extend(OPTIONS_SCHEMA.schema)
OBSERVATION_BOILERPLATE = vol.Schema(
{
vol.Required(CONF_P_GIVEN_T): vol.All(
selector.NumberSelector(
selector.NumberSelectorConfig(
mode=selector.NumberSelectorMode.SLIDER,
step=1.0,
min=0,
max=100,
unit_of_measurement="%",
),
),
vol.Range(
min=0,
max=100,
min_included=False,
max_included=False,
msg="extreme_prob_given_error",
),
),
vol.Required(CONF_P_GIVEN_F): vol.All(
selector.NumberSelector(
selector.NumberSelectorConfig(
mode=selector.NumberSelectorMode.SLIDER,
step=1.0,
min=0,
max=100,
unit_of_measurement="%",
),
),
vol.Range(
min=0,
max=100,
min_included=False,
max_included=False,
msg="extreme_prob_given_error",
),
),
vol.Required(CONF_NAME): selector.TextSelector(),
}
)
STATE_SUBSCHEMA = vol.Schema(
{
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
selector.EntitySelectorConfig(domain=ALLOWED_STATE_DOMAINS)
),
vol.Required(CONF_TO_STATE): selector.TextSelector(
selector.TextSelectorConfig(
multiline=False, type=selector.TextSelectorType.TEXT, multiple=False
) # ideally this would be a state selector context-linked to the above entity.
),
},
).extend(OBSERVATION_BOILERPLATE.schema)
NUMERIC_STATE_SUBSCHEMA = vol.Schema(
{
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
selector.EntitySelectorConfig(domain=ALLOWED_NUMERIC_DOMAINS)
),
vol.Optional(CONF_ABOVE): selector.NumberSelector(
selector.NumberSelectorConfig(
mode=selector.NumberSelectorMode.BOX, step="any"
),
),
vol.Optional(CONF_BELOW): selector.NumberSelector(
selector.NumberSelectorConfig(
mode=selector.NumberSelectorMode.BOX, step="any"
),
),
},
).extend(OBSERVATION_BOILERPLATE.schema)
TEMPLATE_SUBSCHEMA = vol.Schema(
{
vol.Required(CONF_VALUE_TEMPLATE): selector.TemplateSelector(
selector.TemplateSelectorConfig(),
),
},
).extend(OBSERVATION_BOILERPLATE.schema)
def _convert_percentages_to_fractions(
data: dict[str, str | float | int],
) -> dict[str, str | float]:
"""Convert percentage probability values in a dictionary to fractions for storing in the config entry."""
probabilities = [
CONF_P_GIVEN_T,
CONF_P_GIVEN_F,
CONF_PRIOR,
CONF_PROBABILITY_THRESHOLD,
]
return {
key: (
value / 100
if isinstance(value, (int, float)) and key in probabilities
else value
)
for key, value in data.items()
}
def _convert_fractions_to_percentages(
data: dict[str, str | float],
) -> dict[str, str | float]:
"""Convert fraction probability values in a dictionary to percentages for loading into the UI."""
probabilities = [
CONF_P_GIVEN_T,
CONF_P_GIVEN_F,
CONF_PRIOR,
CONF_PROBABILITY_THRESHOLD,
]
return {
key: (
value * 100
if isinstance(value, (int, float)) and key in probabilities
else value
)
for key, value in data.items()
}
def _select_observation_schema(
obs_type: ObservationTypes,
) -> vol.Schema:
"""Return the schema for editing the correct observation (SubEntry) type."""
if obs_type == str(ObservationTypes.STATE):
return STATE_SUBSCHEMA
if obs_type == str(ObservationTypes.NUMERIC_STATE):
return NUMERIC_STATE_SUBSCHEMA
return TEMPLATE_SUBSCHEMA
async def _get_base_suggested_values(
handler: SchemaCommonFlowHandler,
) -> dict[str, Any]:
"""Return suggested values for the base sensor options."""
return _convert_fractions_to_percentages(dict(handler.options))
def _get_observation_values_for_editing(
subentry: ConfigSubentry,
) -> dict[str, Any]:
"""Return the values for editing in the observation subentry."""
return _convert_fractions_to_percentages(dict(subentry.data))
async def _validate_user(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Modify user input to convert to fractions for storage. Validation is done entirely by the schemas."""
user_input = _convert_percentages_to_fractions(user_input)
return {**user_input}
def _validate_observation_subentry(
obs_type: ObservationTypes,
user_input: dict[str, Any],
other_subentries: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Validate an observation input and manually update options with observations as they are nested items."""
if user_input[CONF_P_GIVEN_T] == user_input[CONF_P_GIVEN_F]:
raise SchemaFlowError("equal_probabilities")
user_input = _convert_percentages_to_fractions(user_input)
# Save the observation type in the user input as it is needed in binary_sensor.py
user_input[CONF_PLATFORM] = str(obs_type)
# Additional validation for multiple numeric state observations
if (
user_input[CONF_PLATFORM] == ObservationTypes.NUMERIC_STATE
and other_subentries is not None
):
_LOGGER.debug(
"Comparing with other subentries: %s", [*other_subentries, user_input]
)
try:
above_greater_than_below(user_input)
no_overlapping([*other_subentries, user_input])
except vol.Invalid as err:
raise SchemaFlowError(err) from err
_LOGGER.debug("Processed observation with settings: %s", user_input)
return user_input
async def _validate_subentry_from_config_entry(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
# Standard behavior is to merge the result with the options.
# In this case, we want to add a subentry so we update the options directly.
observations: list[dict[str, Any]] = handler.options.setdefault(
CONF_OBSERVATIONS, []
)
if handler.parent_handler.cur_step is not None:
user_input[CONF_PLATFORM] = handler.parent_handler.cur_step["step_id"]
user_input = _validate_observation_subentry(
user_input[CONF_PLATFORM],
user_input,
other_subentries=handler.options[CONF_OBSERVATIONS],
)
observations.append(user_input)
return {}
async def _get_description_placeholders(
handler: SchemaCommonFlowHandler,
) -> dict[str, str]:
# Current step is None when were are about to start the first step
if handler.parent_handler.cur_step is None:
return {"url": "https://www.home-assistant.io/integrations/bayesian/"}
return {
"parent_sensor_name": handler.options[CONF_NAME],
"device_class_on": translation.async_translate_state(
handler.parent_handler.hass,
"on",
BINARY_SENSOR_DOMAIN,
platform=None,
translation_key=None,
device_class=handler.options.get(CONF_DEVICE_CLASS, None),
),
"device_class_off": translation.async_translate_state(
handler.parent_handler.hass,
"off",
BINARY_SENSOR_DOMAIN,
platform=None,
translation_key=None,
device_class=handler.options.get(CONF_DEVICE_CLASS, None),
),
}
async def _get_observation_menu_options(handler: SchemaCommonFlowHandler) -> list[str]:
"""Return the menu options for the observation selector."""
options = [typ.value for typ in ObservationTypes]
if handler.options.get(CONF_OBSERVATIONS):
options.append("finish")
return options
CONFIG_FLOW: dict[str, SchemaFlowMenuStep | SchemaFlowFormStep] = {
str(USER): SchemaFlowFormStep(
CONFIG_SCHEMA,
validate_user_input=_validate_user,
next_step=str(OBSERVATION_SELECTOR),
description_placeholders=_get_description_placeholders,
),
str(OBSERVATION_SELECTOR): SchemaFlowMenuStep(
_get_observation_menu_options,
),
str(ObservationTypes.STATE): SchemaFlowFormStep(
STATE_SUBSCHEMA,
next_step=str(OBSERVATION_SELECTOR),
validate_user_input=_validate_subentry_from_config_entry,
# Prevent the name of the bayesian sensor from being used as the suggested
# name of the observations
suggested_values=None,
description_placeholders=_get_description_placeholders,
),
str(ObservationTypes.NUMERIC_STATE): SchemaFlowFormStep(
NUMERIC_STATE_SUBSCHEMA,
next_step=str(OBSERVATION_SELECTOR),
validate_user_input=_validate_subentry_from_config_entry,
suggested_values=None,
description_placeholders=_get_description_placeholders,
),
str(ObservationTypes.TEMPLATE): SchemaFlowFormStep(
TEMPLATE_SUBSCHEMA,
next_step=str(OBSERVATION_SELECTOR),
validate_user_input=_validate_subentry_from_config_entry,
suggested_values=None,
description_placeholders=_get_description_placeholders,
),
"finish": SchemaFlowFormStep(),
}
OPTIONS_FLOW: dict[str, SchemaFlowMenuStep | SchemaFlowFormStep] = {
str(OptionsFlowSteps.INIT): SchemaFlowFormStep(
OPTIONS_SCHEMA,
suggested_values=_get_base_suggested_values,
validate_user_input=_validate_user,
description_placeholders=_get_description_placeholders,
),
}
class BayesianConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
"""Bayesian config flow."""
VERSION = 1
MINOR_VERSION = 1
config_flow = CONFIG_FLOW
options_flow = OPTIONS_FLOW
@classmethod
@callback
def async_get_supported_subentry_types(
cls, config_entry: ConfigEntry
) -> dict[str, type[ConfigSubentryFlow]]:
"""Return subentries supported by this integration."""
return {"observation": ObservationSubentryFlowHandler}
def async_config_entry_title(self, options: Mapping[str, str]) -> str:
"""Return config entry title."""
name: str = options[CONF_NAME]
return name
@callback
def async_create_entry(
self,
data: Mapping[str, Any],
**kwargs: Any,
) -> ConfigFlowResult:
"""Finish config flow and create a config entry."""
data = dict(data)
observations = data.pop(CONF_OBSERVATIONS)
subentries: list[ConfigSubentryData] = [
ConfigSubentryData(
data=observation,
title=observation[CONF_NAME],
subentry_type="observation",
unique_id=None,
)
for observation in observations
]
self.async_config_flow_finished(data)
return super().async_create_entry(data=data, subentries=subentries, **kwargs)
class ObservationSubentryFlowHandler(ConfigSubentryFlow):
"""Handle subentry flow for adding and modifying a topic."""
async def step_common(
self,
user_input: dict[str, Any] | None,
obs_type: ObservationTypes,
reconfiguring: bool = False,
) -> SubentryFlowResult:
"""Use common logic within the named steps."""
errors: dict[str, str] = {}
other_subentries = None
if obs_type == str(ObservationTypes.NUMERIC_STATE):
other_subentries = [
dict(se.data) for se in self._get_entry().subentries.values()
]
# If we are reconfiguring a subentry we don't want to compare with self
if reconfiguring:
sub_entry = self._get_reconfigure_subentry()
if other_subentries is not None:
other_subentries.remove(dict(sub_entry.data))
if user_input is not None:
try:
user_input = _validate_observation_subentry(
obs_type,
user_input,
other_subentries=other_subentries,
)
if reconfiguring:
return self.async_update_and_abort(
self._get_entry(),
sub_entry,
title=user_input.get(CONF_NAME, sub_entry.data[CONF_NAME]),
data_updates=user_input,
)
return self.async_create_entry(
title=user_input.get(CONF_NAME),
data=user_input,
)
except SchemaFlowError as err:
errors["base"] = str(err)
return self.async_show_form(
step_id="reconfigure" if reconfiguring else str(obs_type),
data_schema=self.add_suggested_values_to_schema(
data_schema=_select_observation_schema(obs_type),
suggested_values=_get_observation_values_for_editing(sub_entry)
if reconfiguring
else None,
),
errors=errors,
description_placeholders={
"parent_sensor_name": self._get_entry().title,
"device_class_on": translation.async_translate_state(
self.hass,
"on",
BINARY_SENSOR_DOMAIN,
platform=None,
translation_key=None,
device_class=self._get_entry().options.get(CONF_DEVICE_CLASS, None),
),
"device_class_off": translation.async_translate_state(
self.hass,
"off",
BINARY_SENSOR_DOMAIN,
platform=None,
translation_key=None,
device_class=self._get_entry().options.get(CONF_DEVICE_CLASS, None),
),
},
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""User flow to add a new observation."""
return self.async_show_menu(
step_id="user",
menu_options=[typ.value for typ in ObservationTypes],
)
async def async_step_state(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""User flow to add a state observation. Function name must be in the format async_step_{observation_type}."""
return await self.step_common(
user_input=user_input, obs_type=ObservationTypes.STATE
)
async def async_step_numeric_state(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""User flow to add a new numeric state observation, (a numeric range). Function name must be in the format async_step_{observation_type}."""
return await self.step_common(
user_input=user_input, obs_type=ObservationTypes.NUMERIC_STATE
)
async def async_step_template(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""User flow to add a new template observation. Function name must be in the format async_step_{observation_type}."""
return await self.step_common(
user_input=user_input, obs_type=ObservationTypes.TEMPLATE
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""Enable the reconfigure button for observations. Function name must be async_step_reconfigure to be recognised by hass."""
sub_entry = self._get_reconfigure_subentry()
return await self.step_common(
user_input=user_input,
obs_type=ObservationTypes(sub_entry.data[CONF_PLATFORM]),
reconfiguring=True,
)
@@ -1,5 +1,9 @@
"""Consts for using in modules."""
from homeassistant.const import Platform
DOMAIN = "bayesian"
PLATFORMS = [Platform.BINARY_SENSOR]
ATTR_OBSERVATIONS = "observations"
ATTR_OCCURRED_OBSERVATION_ENTITIES = "occurred_observation_entities"
ATTR_PROBABILITY = "probability"
+1 -1
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from . import DOMAIN
from .const import DOMAIN
from .helpers import Observation
@@ -2,8 +2,9 @@
"domain": "bayesian",
"name": "Bayesian",
"codeowners": ["@HarvsG"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bayesian",
"integration_type": "helper",
"iot_class": "local_polling",
"integration_type": "service",
"iot_class": "calculated",
"quality_scale": "internal"
}
@@ -14,5 +14,264 @@
"name": "[%key:common::action::reload%]",
"description": "Reloads Bayesian sensors from the YAML-configuration."
}
},
"options": {
"error": {
"extreme_prior_error": "[%key:component::bayesian::config::error::extreme_prior_error%]",
"extreme_threshold_error": "[%key:component::bayesian::config::error::extreme_threshold_error%]",
"equal_probabilities": "[%key:component::bayesian::config::error::equal_probabilities%]",
"extreme_prob_given_error": "[%key:component::bayesian::config::error::extreme_prob_given_error%]"
},
"step": {
"init": {
"title": "Sensor options",
"description": "These options affect how much evidence is required for the Bayesian sensor to be considered 'on'.",
"data": {
"probability_threshold": "[%key:component::bayesian::config::step::user::data::probability_threshold%]",
"prior": "[%key:component::bayesian::config::step::user::data::prior%]",
"device_class": "[%key:component::bayesian::config::step::user::data::device_class%]",
"name": "[%key:common::config_flow::data::name%]"
},
"data_description": {
"probability_threshold": "[%key:component::bayesian::config::step::user::data_description::probability_threshold%]",
"prior": "[%key:component::bayesian::config::step::user::data_description::prior%]"
}
}
}
},
"config": {
"error": {
"extreme_prior_error": "'Prior' set to 0% means that it is impossible for the sensor to show 'on' and 100% means it will never show 'off', use a close number like 0.1% or 99.9% instead",
"extreme_threshold_error": "'Probability threshold' set to 0% means that the sensor will always be 'on' and 100% mean it will always be 'off', use a close number like 0.1% or 99.9% instead",
"equal_probabilities": "If 'Probability given true' and 'Probability given false' are equal, this observation can have no effect, and is therefore redundant",
"extreme_prob_given_error": "If either 'Probability given false' or 'Probability given true' is 0 or 100 this will create certainties that override all other observations, use numbers close to 0 or 100 instead",
"above_below": "Invalid range: 'Above' must be less than 'Below' when both are set.",
"above_or_below": "Invalid range: At least one of 'Above' or 'Below' must be set.",
"overlapping_ranges": "Invalid range: The 'Above' and 'Below' values overlap with another observation for the same entity."
},
"step": {
"user": {
"title": "Add a Bayesian sensor",
"description": "Create a binary sensor which observes the state of multiple sensors to estimate whether an event is occurring, or if something is true. See [the documentation]({url}) for more details.",
"data": {
"probability_threshold": "Probability threshold",
"prior": "Prior",
"device_class": "Device class",
"name": "[%key:common::config_flow::data::name%]"
},
"data_description": {
"probability_threshold": "The probability above which the sensor will show as 'on'. 50% should produce the most accurate result. Use numbers greater than 50% if avoiding false positives is important, or vice-versa.",
"prior": "The baseline probability the sensor should be 'on', this is usually the percentage of time it is true. For example, for a sensor 'Everyone sleeping' it might be 8 hours a day, 33%.",
"device_class": "Choose the device class you would like the sensor to show as."
}
},
"observation_selector": {
"title": "[%key:component::bayesian::config_subentries::observation::step::user::title%]",
"description": "[%key:component::bayesian::config_subentries::observation::step::user::description%]",
"menu_options": {
"state": "[%key:component::bayesian::config_subentries::observation::step::user::menu_options::state%]",
"numeric_state": "[%key:component::bayesian::config_subentries::observation::step::user::menu_options::numeric_state%]",
"template": "[%key:component::bayesian::config_subentries::observation::step::user::menu_options::template%]",
"finish": "Finish"
}
},
"state": {
"title": "[%key:component::bayesian::config_subentries::observation::step::state::title%]",
"description": "[%key:component::bayesian::config_subentries::observation::step::state::description%]",
"data": {
"name": "[%key:common::config_flow::data::name%]",
"entity_id": "[%key:component::bayesian::config_subentries::observation::step::state::data::entity_id%]",
"to_state": "[%key:component::bayesian::config_subentries::observation::step::state::data::to_state%]",
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_true%]",
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_false%]"
},
"data_description": {
"name": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::name%]",
"entity_id": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::entity_id%]",
"to_state": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::to_state%]",
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_true%]",
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_false%]"
}
},
"numeric_state": {
"title": "[%key:component::bayesian::config_subentries::observation::step::state::title%]",
"description": "[%key:component::bayesian::config_subentries::observation::step::numeric_state::description%]",
"data": {
"name": "[%key:common::config_flow::data::name%]",
"entity_id": "[%key:component::bayesian::config_subentries::observation::step::state::data::entity_id%]",
"above": "[%key:component::bayesian::config_subentries::observation::step::numeric_state::data::above%]",
"below": "[%key:component::bayesian::config_subentries::observation::step::numeric_state::data::below%]",
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_true%]",
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_false%]"
},
"data_description": {
"name": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::name%]",
"entity_id": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::entity_id%]",
"above": "[%key:component::bayesian::config_subentries::observation::step::numeric_state::data_description::above%]",
"below": "[%key:component::bayesian::config_subentries::observation::step::numeric_state::data_description::below%]",
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_true%]",
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_false%]"
}
},
"template": {
"title": "[%key:component::bayesian::config_subentries::observation::step::state::title%]",
"description": "[%key:component::bayesian::config_subentries::observation::step::template::description%]",
"data": {
"name": "[%key:common::config_flow::data::name%]",
"value_template": "[%key:component::bayesian::config_subentries::observation::step::template::data::value_template%]",
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_true%]",
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_false%]"
},
"data_description": {
"name": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::name%]",
"value_template": "[%key:component::bayesian::config_subentries::observation::step::template::data_description::value_template%]",
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_true%]",
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_false%]"
}
}
}
},
"config_subentries": {
"observation": {
"step": {
"user": {
"title": "Add an observation",
"description": "'Observations' are the sensor or template values that are monitored and then combined in order to inform the Bayesian sensor's final probability. Each observation will update the probability of the Bayesian sensor if it is detected, or if it is not detected. If the state of the entity becomes `unavailable` or `unknown` it will be ignored. If more than one state or more than one numeric range is configured for the same entity then inverse detections will be ignored.",
"menu_options": {
"state": "Add an observation for a sensor's state",
"numeric_state": "Add an observation for a numeric range",
"template": "Add an observation for a template"
}
},
"state": {
"title": "Add a Bayesian sensor",
"description": "Add an observation which evaluates to `True` when the value of the sensor exactly matches *'To state'*. When `False`, it will update the prior with probabilities that are the inverse of those set below. This behaviour can be overridden by adding observations for the same entity's other states.",
"data": {
"name": "[%key:common::config_flow::data::name%]",
"entity_id": "Entity",
"to_state": "To state",
"prob_given_true": "Probability when {parent_sensor_name} is {device_class_on}",
"prob_given_false": "Probability when {parent_sensor_name} is {device_class_off}"
},
"data_description": {
"name": "This name will be used for to identify this observation for editing in the future.",
"entity_id": "An entity that is correlated with `{parent_sensor_name}`.",
"to_state": "The state of the sensor for which the observation will be considered `True`.",
"prob_given_true": "The estimated probability or proportion of time this observation is `True` while `{parent_sensor_name}` is, or should be, `{device_class_on}`.",
"prob_given_false": "The estimated probability or proportion of time this observation is `True` while `{parent_sensor_name}` is, or should be, `{device_class_off}`."
}
},
"numeric_state": {
"title": "[%key:component::bayesian::config_subentries::observation::step::state::title%]",
"description": "Add an observation which evaluates to `True` when a numeric sensor is within a chosen range.",
"data": {
"name": "[%key:common::config_flow::data::name%]",
"entity_id": "[%key:component::bayesian::config_subentries::observation::step::state::data::entity_id%]",
"above": "Above",
"below": "Below",
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_true%]",
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_false%]"
},
"data_description": {
"name": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::name%]",
"entity_id": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::entity_id%]",
"above": "Optional - the lower end of the numeric range. Values exactly matching this will not count",
"below": "Optional - the upper end of the numeric range. Values exactly matching this will only count if more than one range is configured for the same entity.",
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_true%]",
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_false%]"
}
},
"template": {
"title": "[%key:component::bayesian::config_subentries::observation::step::state::title%]",
"description": "Add a custom observation which evaluates whether a template is observed (`True`) or not (`False`).",
"data": {
"name": "[%key:common::config_flow::data::name%]",
"value_template": "Template",
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_true%]",
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_false%]"
},
"data_description": {
"name": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::name%]",
"value_template": "A template that evaluates to `True` will update the prior accordingly, A template that returns `False` or `None` will update the prior with inverse probabilities. A template that returns an error will not update probabilities. Results are coerced into being `True` or `False`",
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_true%]",
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_false%]"
}
},
"reconfigure": {
"title": "Edit observation",
"description": "[%key:component::bayesian::config_subentries::observation::step::state::description%]",
"data": {
"name": "[%key:common::config_flow::data::name%]",
"entity_id": "[%key:component::bayesian::config_subentries::observation::step::state::data::entity_id%]",
"to_state": "[%key:component::bayesian::config_subentries::observation::step::state::data::to_state%]",
"above": "[%key:component::bayesian::config_subentries::observation::step::numeric_state::data::above%]",
"below": "[%key:component::bayesian::config_subentries::observation::step::numeric_state::data::below%]",
"value_template": "[%key:component::bayesian::config_subentries::observation::step::template::data::value_template%]",
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_true%]",
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_false%]"
},
"data_description": {
"name": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::name%]",
"entity_id": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::entity_id%]",
"to_state": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::to_state%]",
"above": "[%key:component::bayesian::config_subentries::observation::step::numeric_state::data_description::above%]",
"below": "[%key:component::bayesian::config_subentries::observation::step::numeric_state::data_description::below%]",
"value_template": "[%key:component::bayesian::config_subentries::observation::step::template::data_description::value_template%]",
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_true%]",
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_false%]"
}
}
},
"initiate_flow": {
"user": "[%key:component::bayesian::config_subentries::observation::step::user::title%]"
},
"entry_type": "Observation",
"error": {
"equal_probabilities": "[%key:component::bayesian::config::error::equal_probabilities%]",
"extreme_prob_given_error": "[%key:component::bayesian::config::error::extreme_prob_given_error%]",
"above_below": "[%key:component::bayesian::config::error::above_below%]",
"above_or_below": "[%key:component::bayesian::config::error::above_or_below%]",
"overlapping_ranges": "[%key:component::bayesian::config::error::overlapping_ranges%]"
},
"abort": {
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
}
}
},
"selector": {
"binary_sensor_device_class": {
"options": {
"battery": "[%key:component::binary_sensor::entity_component::battery::name%]",
"battery_charging": "[%key:component::binary_sensor::entity_component::battery_charging::name%]",
"carbon_monoxide": "[%key:component::binary_sensor::entity_component::carbon_monoxide::name%]",
"cold": "[%key:component::binary_sensor::entity_component::cold::name%]",
"connectivity": "[%key:component::binary_sensor::entity_component::connectivity::name%]",
"door": "[%key:component::binary_sensor::entity_component::door::name%]",
"garage_door": "[%key:component::binary_sensor::entity_component::garage_door::name%]",
"gas": "[%key:component::binary_sensor::entity_component::gas::name%]",
"heat": "[%key:component::binary_sensor::entity_component::heat::name%]",
"light": "[%key:component::binary_sensor::entity_component::light::name%]",
"lock": "[%key:component::binary_sensor::entity_component::lock::name%]",
"moisture": "[%key:component::binary_sensor::entity_component::moisture::name%]",
"motion": "[%key:component::binary_sensor::entity_component::motion::name%]",
"moving": "[%key:component::binary_sensor::entity_component::moving::name%]",
"occupancy": "[%key:component::binary_sensor::entity_component::occupancy::name%]",
"opening": "[%key:component::binary_sensor::entity_component::opening::name%]",
"plug": "[%key:component::binary_sensor::entity_component::plug::name%]",
"power": "[%key:component::binary_sensor::entity_component::power::name%]",
"presence": "[%key:component::binary_sensor::entity_component::presence::name%]",
"problem": "[%key:component::binary_sensor::entity_component::problem::name%]",
"running": "[%key:component::binary_sensor::entity_component::running::name%]",
"safety": "[%key:component::binary_sensor::entity_component::safety::name%]",
"smoke": "[%key:component::binary_sensor::entity_component::smoke::name%]",
"sound": "[%key:component::binary_sensor::entity_component::sound::name%]",
"tamper": "[%key:component::binary_sensor::entity_component::tamper::name%]",
"update": "[%key:component::binary_sensor::entity_component::update::name%]",
"vibration": "[%key:component::binary_sensor::entity_component::vibration::name%]",
"window": "[%key:component::binary_sensor::entity_component::window::name%]"
}
}
}
}
@@ -385,10 +385,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
f"Bluetooth adapter {adapter} with address {address} not found"
)
passive = entry.options.get(CONF_PASSIVE)
adapters = await manager.async_get_bluetooth_adapters()
mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE
scanner = HaScanner(mode, adapter, address)
scanner.async_setup()
adapters = await manager.async_get_bluetooth_adapters()
details = adapters[adapter]
if entry.title == address:
hass.config_entries.async_update_entry(
@@ -16,11 +16,11 @@
"quality_scale": "internal",
"requirements": [
"bleak==1.0.1",
"bleak-retry-connector==4.0.1",
"bluetooth-adapters==2.0.0",
"bleak-retry-connector==4.4.3",
"bluetooth-adapters==2.1.0",
"bluetooth-auto-recovery==1.5.2",
"bluetooth-data-tools==1.28.2",
"dbus-fast==2.44.3",
"habluetooth==5.0.1"
"habluetooth==5.6.2"
]
}
@@ -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.17.2"]
"requirements": ["bimmer-connected[china]==0.17.3"]
}
+10 -4
View File
@@ -1,5 +1,7 @@
"""A entity class for Bravia TV integration."""
from typing import TYPE_CHECKING
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -17,11 +19,15 @@ class BraviaTVEntity(CoordinatorEntity[BraviaTVCoordinator]):
super().__init__(coordinator)
self._attr_unique_id = unique_id
if TYPE_CHECKING:
assert coordinator.client.mac is not None
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
connections={(CONNECTION_NETWORK_MAC, coordinator.system_info["macAddr"])},
connections={(CONNECTION_NETWORK_MAC, coordinator.client.mac)},
manufacturer=ATTR_MANUFACTURER,
model_id=coordinator.system_info["model"],
hw_version=coordinator.system_info["generation"],
serial_number=coordinator.system_info["serial"],
model_id=coordinator.system_info.get("model"),
hw_version=coordinator.system_info.get("generation"),
serial_number=coordinator.system_info.get("serial"),
)
@@ -8,7 +8,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["brother", "pyasn1", "pysmi", "pysnmp"],
"requirements": ["brother==5.0.0"],
"requirements": ["brother==5.0.1"],
"zeroconf": [
{
"type": "_printer._tcp.local.",
@@ -81,11 +81,15 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
if self.coordinator.data.state.current_temperature is None:
return None
return self.coordinator.data.state.current_temperature.value
@property
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
if self.coordinator.data.state.target_temperature is None:
return None
return self.coordinator.data.state.target_temperature.value
@property
@@ -25,7 +25,7 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize BSBLan flow."""
self.host: str | None = None
self.host: str = ""
self.port: int = DEFAULT_PORT
self.mac: str | None = None
self.passkey: str | None = None
+23 -3
View File
@@ -28,6 +28,7 @@ class BSBLanSensorEntityDescription(SensorEntityDescription):
"""Describes BSB-Lan sensor entity."""
value_fn: Callable[[BSBLanCoordinatorData], StateType]
exists_fn: Callable[[BSBLanCoordinatorData], bool] = lambda data: True
SENSOR_TYPES: tuple[BSBLanSensorEntityDescription, ...] = (
@@ -37,7 +38,12 @@ SENSOR_TYPES: tuple[BSBLanSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data.sensor.current_temperature.value,
value_fn=lambda data: (
data.sensor.current_temperature.value
if data.sensor.current_temperature is not None
else None
),
exists_fn=lambda data: data.sensor.current_temperature is not None,
),
BSBLanSensorEntityDescription(
key="outside_temperature",
@@ -45,7 +51,12 @@ SENSOR_TYPES: tuple[BSBLanSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data.sensor.outside_temperature.value,
value_fn=lambda data: (
data.sensor.outside_temperature.value
if data.sensor.outside_temperature is not None
else None
),
exists_fn=lambda data: data.sensor.outside_temperature is not None,
),
)
@@ -57,7 +68,16 @@ async def async_setup_entry(
) -> None:
"""Set up BSB-Lan sensor based on a config entry."""
data = entry.runtime_data
async_add_entities(BSBLanSensor(data, description) for description in SENSOR_TYPES)
# Only create sensors for available data points
entities = [
BSBLanSensor(data, description)
for description in SENSOR_TYPES
if description.exists_fn(data.coordinator.data)
]
if entities:
async_add_entities(entities)
class BSBLanSensor(BSBLanEntity, SensorEntity):
@@ -41,6 +41,18 @@ async def async_setup_entry(
) -> None:
"""Set up BSBLAN water heater based on a config entry."""
data = entry.runtime_data
# Only create water heater entity if DHW (Domestic Hot Water) is available
# Check if we have any DHW-related data indicating water heater support
dhw_data = data.coordinator.data.dhw
if (
dhw_data.operating_mode is None
and dhw_data.nominal_setpoint is None
and dhw_data.dhw_actual_value_top_temperature is None
):
# No DHW functionality available, skip water heater setup
return
async_add_entities([BSBLANWaterHeater(data)])
@@ -61,23 +73,31 @@ class BSBLANWaterHeater(BSBLanEntity, WaterHeaterEntity):
# Set temperature limits based on device capabilities
self._attr_temperature_unit = data.coordinator.client.get_temperature_unit
self._attr_min_temp = data.coordinator.data.dhw.reduced_setpoint.value
self._attr_max_temp = data.coordinator.data.dhw.nominal_setpoint_max.value
if data.coordinator.data.dhw.reduced_setpoint is not None:
self._attr_min_temp = data.coordinator.data.dhw.reduced_setpoint.value
if data.coordinator.data.dhw.nominal_setpoint_max is not None:
self._attr_max_temp = data.coordinator.data.dhw.nominal_setpoint_max.value
@property
def current_operation(self) -> str | None:
"""Return current operation."""
if self.coordinator.data.dhw.operating_mode is None:
return None
current_mode = self.coordinator.data.dhw.operating_mode.desc
return OPERATION_MODES.get(current_mode)
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
if self.coordinator.data.dhw.dhw_actual_value_top_temperature is None:
return None
return self.coordinator.data.dhw.dhw_actual_value_top_temperature.value
@property
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
if self.coordinator.data.dhw.nominal_setpoint is None:
return None
return self.coordinator.data.dhw.nominal_setpoint.value
async def async_set_temperature(self, **kwargs: Any) -> None:
+4 -4
View File
@@ -72,7 +72,7 @@ SENSOR_DESCRIPTIONS = {
key=str(BTHomeExtendedSensorDeviceClass.CHANNEL),
state_class=SensorStateClass.MEASUREMENT,
),
# Conductivity (µS/cm)
# Conductivity (μS/cm)
(
BTHomeSensorDeviceClass.CONDUCTIVITY,
Units.CONDUCTIVITY,
@@ -215,7 +215,7 @@ SENSOR_DESCRIPTIONS = {
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
# PM10 (µg/m3)
# PM10 (μg/m3)
(
BTHomeSensorDeviceClass.PM10,
Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
@@ -225,7 +225,7 @@ SENSOR_DESCRIPTIONS = {
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
# PM2.5 (µg/m3)
# PM2.5 (μg/m3)
(
BTHomeSensorDeviceClass.PM25,
Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
@@ -318,7 +318,7 @@ SENSOR_DESCRIPTIONS = {
key=str(BTHomeSensorDeviceClass.UV_INDEX),
state_class=SensorStateClass.MEASUREMENT,
),
# Volatile organic Compounds (VOC) (µg/m3)
# Volatile organic Compounds (VOC) (μg/m3)
(
BTHomeSensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+1 -1
View File
@@ -14,7 +14,7 @@
"documentation": "https://www.home-assistant.io/integrations/cast",
"iot_class": "local_polling",
"loggers": ["casttube", "pychromecast"],
"requirements": ["PyChromecast==14.0.7"],
"requirements": ["PyChromecast==14.0.9"],
"single_config_entry": true,
"zeroconf": ["_googlecast._tcp.local."]
}
+8 -5
View File
@@ -3,9 +3,10 @@
import logging
from typing import Any
from ccm15 import CCM15DeviceState
from ccm15 import CCM15DeviceState, CCM15SlaveDevice
from homeassistant.components.climate import (
ATTR_HVAC_MODE,
FAN_AUTO,
FAN_HIGH,
FAN_LOW,
@@ -88,7 +89,7 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
)
@property
def data(self) -> CCM15DeviceState | None:
def data(self) -> CCM15SlaveDevice | None:
"""Return device data."""
return self.coordinator.get_ac_data(self._ac_index)
@@ -144,15 +145,17 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the target temperature."""
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None:
await self.coordinator.async_set_temperature(self._ac_index, temperature)
await self.coordinator.async_set_temperature(
self._ac_index, self.data, temperature, kwargs.get(ATTR_HVAC_MODE)
)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the hvac mode."""
await self.coordinator.async_set_hvac_mode(self._ac_index, hvac_mode)
await self.coordinator.async_set_hvac_mode(self._ac_index, self.data, hvac_mode)
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set the fan mode."""
await self.coordinator.async_set_fan_mode(self._ac_index, fan_mode)
await self.coordinator.async_set_fan_mode(self._ac_index, self.data, fan_mode)
async def async_turn_off(self) -> None:
"""Turn off."""
+24 -9
View File
@@ -55,9 +55,9 @@ class CCM15Coordinator(DataUpdateCoordinator[CCM15DeviceState]):
"""Get the current status of all AC devices."""
return await self._ccm15.get_status_async()
async def async_set_state(self, ac_index: int, state: str, value: int) -> None:
async def async_set_state(self, ac_index: int, data) -> None:
"""Set new target states."""
if await self._ccm15.async_set_state(ac_index, state, value):
if await self._ccm15.async_set_state(ac_index, data):
await self.async_request_refresh()
def get_ac_data(self, ac_index: int) -> CCM15SlaveDevice | None:
@@ -67,17 +67,32 @@ class CCM15Coordinator(DataUpdateCoordinator[CCM15DeviceState]):
return None
return self.data.devices[ac_index]
async def async_set_hvac_mode(self, ac_index, hvac_mode: HVACMode) -> None:
"""Set the hvac mode."""
async def async_set_hvac_mode(
self, ac_index: int, data: CCM15SlaveDevice, hvac_mode: HVACMode
) -> None:
"""Set the HVAC mode."""
_LOGGER.debug("Set Hvac[%s]='%s'", ac_index, str(hvac_mode))
await self.async_set_state(ac_index, "mode", CONST_STATE_CMD_MAP[hvac_mode])
data.ac_mode = CONST_STATE_CMD_MAP[hvac_mode]
await self.async_set_state(ac_index, data)
async def async_set_fan_mode(self, ac_index, fan_mode: str) -> None:
async def async_set_fan_mode(
self, ac_index: int, data: CCM15SlaveDevice, fan_mode: str
) -> None:
"""Set the fan mode."""
_LOGGER.debug("Set Fan[%s]='%s'", ac_index, fan_mode)
await self.async_set_state(ac_index, "fan", CONST_FAN_CMD_MAP[fan_mode])
data.fan_mode = CONST_FAN_CMD_MAP[fan_mode]
await self.async_set_state(ac_index, data)
async def async_set_temperature(self, ac_index, temp) -> None:
async def async_set_temperature(
self,
ac_index: int,
data: CCM15SlaveDevice,
temp: int,
hvac_mode: HVACMode | None,
) -> None:
"""Set the target temperature mode."""
_LOGGER.debug("Set Temp[%s]='%s'", ac_index, temp)
await self.async_set_state(ac_index, "temp", temp)
data.temperature_setpoint = temp
if hvac_mode is not None:
data.ac_mode = CONST_STATE_CMD_MAP[hvac_mode]
await self.async_set_state(ac_index, data)
+1 -1
View File
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ccm15",
"iot_class": "local_polling",
"requirements": ["py-ccm15==0.0.9"]
"requirements": ["py_ccm15==0.1.2"]
}
+1 -1
View File
@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==1.0.0"],
"requirements": ["hass-nabucasa==1.1.1"],
"single_config_entry": true
}
@@ -6,10 +6,10 @@ from datetime import timedelta
import logging
from aioelectricitymaps import (
CarbonIntensityResponse,
ElectricityMaps,
ElectricityMapsError,
ElectricityMapsInvalidTokenError,
HomeAssistantCarbonIntensityResponse,
)
from homeassistant.config_entries import ConfigEntry
@@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__)
type CO2SignalConfigEntry = ConfigEntry[CO2SignalCoordinator]
class CO2SignalCoordinator(DataUpdateCoordinator[CarbonIntensityResponse]):
class CO2SignalCoordinator(DataUpdateCoordinator[HomeAssistantCarbonIntensityResponse]):
"""Data update coordinator."""
config_entry: CO2SignalConfigEntry
@@ -51,7 +51,7 @@ class CO2SignalCoordinator(DataUpdateCoordinator[CarbonIntensityResponse]):
"""Return entry ID."""
return self.config_entry.entry_id
async def _async_update_data(self) -> CarbonIntensityResponse:
async def _async_update_data(self) -> HomeAssistantCarbonIntensityResponse:
"""Fetch the latest data from the source."""
try:
+15 -9
View File
@@ -5,8 +5,12 @@ from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from aioelectricitymaps import ElectricityMaps
from aioelectricitymaps.models import CarbonIntensityResponse
from aioelectricitymaps import (
CoordinatesRequest,
ElectricityMaps,
HomeAssistantCarbonIntensityResponse,
ZoneRequest,
)
from homeassistant.const import CONF_COUNTRY_CODE, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant
@@ -16,14 +20,16 @@ async def fetch_latest_carbon_intensity(
hass: HomeAssistant,
em: ElectricityMaps,
config: Mapping[str, Any],
) -> CarbonIntensityResponse:
) -> HomeAssistantCarbonIntensityResponse:
"""Fetch the latest carbon intensity based on country code or location coordinates."""
if CONF_COUNTRY_CODE in config:
return await em.latest_carbon_intensity_by_country_code(
code=config[CONF_COUNTRY_CODE]
)
return await em.latest_carbon_intensity_by_coordinates(
request: CoordinatesRequest | ZoneRequest = CoordinatesRequest(
lat=config.get(CONF_LATITUDE, hass.config.latitude),
lon=config.get(CONF_LONGITUDE, hass.config.longitude),
)
if CONF_COUNTRY_CODE in config:
request = ZoneRequest(
zone=config[CONF_COUNTRY_CODE],
)
return await em.carbon_intensity_for_home_assistant(request)
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["aioelectricitymaps"],
"requirements": ["aioelectricitymaps==0.4.0"]
"requirements": ["aioelectricitymaps==1.1.1"]
}
+5 -5
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from aioelectricitymaps.models import CarbonIntensityResponse
from aioelectricitymaps import HomeAssistantCarbonIntensityResponse
from homeassistant.components.sensor import (
SensorEntity,
@@ -28,10 +28,10 @@ class CO2SensorEntityDescription(SensorEntityDescription):
# For backwards compat, allow description to override unique ID key to use
unique_id: str | None = None
unit_of_measurement_fn: Callable[[CarbonIntensityResponse], str | None] | None = (
None
)
value_fn: Callable[[CarbonIntensityResponse], float | None]
unit_of_measurement_fn: (
Callable[[HomeAssistantCarbonIntensityResponse], str | None] | None
) = None
value_fn: Callable[[HomeAssistantCarbonIntensityResponse], float | None]
SENSORS = (
@@ -62,6 +62,7 @@ def async_setup(hass: HomeAssistant) -> bool:
websocket_api.async_register_command(hass, config_entries_flow_subscribe)
websocket_api.async_register_command(hass, ignore_config_flow)
websocket_api.async_register_command(hass, config_subentry_update)
websocket_api.async_register_command(hass, config_subentry_delete)
websocket_api.async_register_command(hass, config_subentry_list)
@@ -137,20 +138,16 @@ class ConfigManagerEntryResourceReloadView(HomeAssistantView):
def _prepare_config_flow_result_json(
result: data_entry_flow.FlowResult,
prepare_result_json: Callable[
[data_entry_flow.FlowResult], data_entry_flow.FlowResult
],
) -> data_entry_flow.FlowResult:
prepare_result_json: Callable[[data_entry_flow.FlowResult], dict[str, Any]],
) -> dict[str, Any]:
"""Convert result to JSON."""
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
return prepare_result_json(result)
data = result.copy()
entry: config_entries.ConfigEntry = data["result"] # type: ignore[typeddict-item]
data = {key: val for key, val in result.items() if key not in ("data", "context")}
entry: config_entries.ConfigEntry = result["result"] # type: ignore[typeddict-item]
# We overwrite the ConfigEntry object with its json representation.
data["result"] = entry.as_json_fragment # type: ignore[typeddict-unknown-key]
data.pop("data")
data.pop("context")
data["result"] = entry.as_json_fragment
return data
@@ -204,8 +201,8 @@ class ConfigManagerFlowIndexView(
def _prepare_result_json(
self, result: data_entry_flow.FlowResult
) -> data_entry_flow.FlowResult:
"""Convert result to JSON."""
) -> dict[str, Any]:
"""Convert result to JSON serializable dict."""
return _prepare_config_flow_result_json(result, super()._prepare_result_json)
@@ -229,8 +226,8 @@ class ConfigManagerFlowResourceView(
def _prepare_result_json(
self, result: data_entry_flow.FlowResult
) -> data_entry_flow.FlowResult:
"""Convert result to JSON."""
) -> dict[str, Any]:
"""Convert result to JSON serializable dict."""
return _prepare_config_flow_result_json(result, super()._prepare_result_json)
@@ -735,6 +732,47 @@ async def config_subentry_list(
connection.send_result(msg["id"], result)
@websocket_api.require_admin
@websocket_api.websocket_command(
{
"type": "config_entries/subentries/update",
"entry_id": str,
"subentry_id": str,
vol.Optional("title"): str,
}
)
@websocket_api.async_response
async def config_subentry_update(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Update a subentry of a config entry."""
entry = get_entry(hass, connection, msg["entry_id"], msg["id"])
if entry is None:
connection.send_error(
msg["entry_id"], websocket_api.const.ERR_NOT_FOUND, "Config entry not found"
)
return
subentry = entry.subentries.get(msg["subentry_id"])
if subentry is None:
connection.send_error(
msg["id"], websocket_api.const.ERR_NOT_FOUND, "Config subentry not found"
)
return
changes = dict(msg)
changes.pop("id")
changes.pop("type")
changes.pop("entry_id")
changes.pop("subentry_id")
hass.config_entries.async_update_subentry(entry, subentry, **changes)
connection.send_result(msg["id"])
@websocket_api.require_admin
@websocket_api.websocket_command(
{
@@ -35,7 +35,7 @@ from hassil.recognize import (
)
from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity
from hassil.trie import Trie
from hassil.util import merge_dict
from hassil.util import merge_dict, remove_punctuation
from home_assistant_intents import (
ErrorKey,
FuzzyConfig,
@@ -327,12 +327,10 @@ class DefaultAgent(ConversationEntity):
if self._exposed_names_trie is not None:
# Filter by input string
text_lower = user_input.text.strip().lower()
text = remove_punctuation(user_input.text).strip().lower()
slot_lists["name"] = TextSlotList(
name="name",
values=[
result[2] for result in self._exposed_names_trie.find(text_lower)
],
values=[result[2] for result in self._exposed_names_trie.find(text)],
)
start = time.monotonic()
@@ -1263,7 +1261,7 @@ class DefaultAgent(ConversationEntity):
name_list = TextSlotList.from_tuples(exposed_entity_names, allow_template=False)
for name_value in name_list.values:
assert isinstance(name_value.text_in, TextChunk)
name_text = name_value.text_in.text.strip().lower()
name_text = remove_punctuation(name_value.text_in.text).strip().lower()
self._exposed_names_trie.insert(name_text, name_value)
self._slot_lists = {
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["hassil==3.1.0", "home-assistant-intents==2025.7.30"]
"requirements": ["hassil==3.2.0", "home-assistant-intents==2025.9.3"]
}
@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/daikin",
"iot_class": "local_polling",
"loggers": ["pydaikin"],
"requirements": ["pydaikin==2.15.0"],
"requirements": ["pydaikin==2.16.0"],
"zeroconf": ["_dkapi._tcp.local."]
}
+1 -1
View File
@@ -177,7 +177,7 @@ class DeconzSceneMixin(DeconzDevice[PydeconzScene]):
"""Return a device description for device registry."""
return DeviceInfo(
identifiers={(DOMAIN, self._group_identifier)},
manufacturer="Dresden Elektronik",
manufacturer="dresden elektronik",
model="deCONZ group",
name=self.group.name,
via_device=(DOMAIN, self.hub.api.config.bridge_id),

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