Compare commits

...

384 Commits

Author SHA1 Message Date
epenet 8a6376a41f Cleanup unused modbus constant 2025-09-24 08:00:13 +00:00
Erik Montnemery ddfc528d63 Fix apparent copy-paste error in tests of trigger helper (#152855) 2025-09-24 08:38:32 +02:00
Nick Kuiper ddea2206c3 Add start charge session action for blue current integration. (#145446) 2025-09-24 08:11:33 +02:00
J. Nick Koston 32aacac550 Fix async_get_scanner return type for BleakScanner compatibility (#152840) 2025-09-23 23:14:08 -05:00
Luke Lashley dadba274aa Bump python-roborock to 2.47.1 (#152844) 2025-09-23 22:16:32 -04:00
TheJulianJES 14b5b9742c Bump ZHA to 0.0.72 (#152850) 2025-09-23 22:15:00 -04:00
Michael Hansen a0be737925 Auto select first active wake word (#152562) 2025-09-23 17:19:04 -04:00
Marcel van der Veldt ff47839c61 Fix support for new Hue bulbs with very wide color temperature support (#152834) 2025-09-23 16:56:16 -04:00
Copilot 9ba7dda864 Rename logbook integration to "Activity" in user-facing strings (#150950) 2025-09-23 22:55:11 +02:00
J. Nick Koston 911f901d9d Bump aioesphomeapi to 41.9.0 (#152841) 2025-09-23 22:54:31 +02:00
Marcel van der Veldt 2008a73657 Add support for Hue MotionAware sensors (#152811)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-09-23 16:52:09 -04:00
Pete Sage 60bf298ca6 File add read_file action with Response (#139216) 2025-09-23 22:25:56 +02:00
jan iversen 3bc2ea7b5f Use DOMAIN not MODBUS_DOMAIN (#152823) 2025-09-23 22:07:24 +02:00
Kevin Stillhammer 3bac6b86df Fix multiple_here_travel_time_entries issue description (#152839) 2025-09-23 22:06:06 +02:00
Stefan Agner 20293e2a11 Bump aiohasupervisor to 0.3.3b0 (#152835)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-23 22:05:38 +02:00
Martin Hjelmare 15cc28e6c1 Move first probe firmware to firmware progress in hardware flow (#152819) 2025-09-23 22:03:04 +02:00
puddly 874ca1323b Simplified ZHA adapter migration and setup flow (#152389) 2025-09-23 22:00:31 +02:00
Ludovic BOUÉ ca186925af Add Matter Thermostat OutdoorTemperature sensor (#152632) 2025-09-23 21:28:24 +02:00
andreimoraru 2ab051b716 Bump yt-dlp to 2025.09.23 (#152818) 2025-09-23 20:03:53 +01:00
epenet a2a726de34 Rename function arguments in modbus (#152814) 2025-09-23 20:54:52 +02:00
Sarah Seidman 5d543d2185 Bump pydroplet version to 2.3.3 (#152832) 2025-09-23 19:36:06 +01:00
epenet a78c909b34 Rename cover property in tuya (#152822) 2025-09-23 19:35:47 +01:00
Artur Pragacz f00ab80d17 Add analytics platform to template (#152824) 2025-09-23 13:53:53 -04:00
Joakim Sørensen 014881d985 Fix error handling in subscription info retrieval and update tests (#148397)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-09-23 19:53:12 +02:00
Artur Pragacz 29a42a8e58 Add analytics platform to automation (#152828) 2025-09-23 13:52:58 -04:00
Rohan Kapoor 3f70084d7f Handle ignored and disabled entries correctly in zeroconf discovery for Music Assistant (#152792)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-09-23 10:15:52 -07:00
Felipe Santos b1ae9c95c9 Add a switch entity for add-ons (#151431)
Co-authored-by: Stefan Agner <stefan@agner.ch>
2025-09-23 17:27:35 +02:00
Artur Pragacz 8be79ecdb0 Move conversation trigger registration to manager (#152749) 2025-09-23 10:01:46 -05:00
Jan Bouwhuis f6b8aa893b Add mqtt image subentry support (#151586) 2025-09-23 16:31:40 +02:00
jan iversen c867026bdd Add test to validate multiple host/port for modbus. (#152658)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-09-23 16:03:42 +02:00
Kevin Stillhammer da3a164e66 Change here_travel_time update interval to 30min (#147222) 2025-09-23 15:56:13 +02:00
Josef Zweck 32688e1108 Bump aioacaia to 0.1.17 (#152815) 2025-09-23 15:29:16 +02:00
Artur Pragacz 4305ea9b4c Create analytics platform (#151974) 2025-09-23 09:16:37 -04:00
epenet 61153ec456 Deduplicate code in modbus service call (#152808)
Co-authored-by: jan iversen <jancasacondor@gmail.com>
2025-09-23 13:44:28 +02:00
Marcel van der Veldt 9e4a2d5fa9 Bump aiohue to 4.8.0 (#152807) 2025-09-23 13:39:58 +02:00
Marcel van der Veldt 72e608918b Handle toggling of the 'expose_to_ha' setting in Music Assistant integration (#152779) 2025-09-23 13:22:16 +02:00
Erik Montnemery 86db60c442 Freeze time in irm_kmi tests (#152810) 2025-09-23 13:21:59 +02:00
cdnninja 25806615a9 Bump pyvesync to 3.0.0 (#152726) 2025-09-23 13:00:59 +02:00
Paulus Schoutsen a0f67381e5 Allow configuring Z-Wave JS to talk via ESPHome (#152590)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-09-23 12:58:36 +02:00
Jan Čermák 90bfadda9b Support all reported preset modes in Smartthings climate (#148056)
Co-authored-by: abmantis <amfcalt@gmail.com>
2025-09-23 12:56:27 +02:00
Maciej Bieniek 0f8e700965 Add a cable unplugged sensor for Shelly Flood Gen4 (#152559) 2025-09-23 12:55:44 +02:00
Jan Čermák 21d4ed2837 Add profiler service for dumping sockets used by HA (#152440)
Co-authored-by: Stefan Agner <stefan@agner.ch>
2025-09-23 12:55:29 +02:00
Luke Lashley ce363b3835 Make Roborock load_multi_map always cloud dependent. (#152698) 2025-09-23 12:55:07 +02:00
G Johansson dd3e6b8df5 Only load selected processes in systemmonitor (#152777) 2025-09-23 12:41:31 +02:00
epenet abbf8390ac Move switch to valve for Tuya sfkzq category (#152478) 2025-09-23 12:39:37 +02:00
epenet 689039959c Improve current_state support in Tuya curtains (#152801) 2025-09-23 12:38:47 +02:00
epenet 52c25cfc88 Rename modbus internal variable (#152805) 2025-09-23 12:14:05 +02:00
G Johansson 00b2017767 Fix resource and payload template in scrape (#152670) 2025-09-23 11:59:00 +02:00
Martin Hjelmare dd7f7be6ad Move hardware thread add-on install after firmware install (#152800) 2025-09-23 11:47:30 +02:00
Robert Resch 22709506c6 Add Ecovacs custom water amount entity (#152782)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-23 11:21:11 +02:00
Lukas f0c0492375 Add MAC address to Pooldose device (#152760)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-23 11:11:08 +02:00
Robert Resch 58459cb80f Bump deebot-client to 14.0.0 (#152448) 2025-09-23 10:47:37 +02:00
epenet a19e378447 Add Tuya test fixture files (#152795) 2025-09-23 10:44:36 +02:00
J. Nick Koston 38a5a3ed4b Handle wrong ESPHome device without encryption appearing at the configured IP (#152758) 2025-09-23 10:27:13 +02:00
Matthias Lohr e76bed4a83 Add reconfigure flow to tolo (#137609)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-09-23 10:21:06 +02:00
Karsten Bade d73309ba60 Bump SoCo to 0.30.12 (#152797) 2025-09-23 09:49:33 +02:00
Przemko92 19fdea024c Bump compit-inext-api to 0.3.1 (#152781) 2025-09-23 09:48:53 +02:00
Manu a3cfd7f707 Fix coordinator data handling in Bring integration (#152786) 2025-09-23 09:03:01 +02:00
Petro31 3dd941eff7 Fix section and entity variable resolution for template platforms (#149660)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-09-23 08:12:24 +02:00
J. Nick Koston d389141aee Bump aioesphomeapi to 41.6.0 (#152787) 2025-09-22 21:43:20 -05:00
Rohan Kapoor 3c542b8d43 Only update Music Assistant URL on zeroconf discovery when current URL is unreachable (#152030) 2025-09-22 13:49:41 -07:00
starkillerOG 2367df89d9 Bump reolink-aio to 0.15.2 (#152775) 2025-09-22 21:39:06 +01:00
Manu 7bfdfb3fc7 Bump pynecil to v4.2.0 (#152776) 2025-09-22 21:35:19 +01:00
G Johansson 485916265a Fix manual updating of Nord Pool sensors (#152773) 2025-09-22 22:04:17 +02:00
Stefan Agner 1bb3c96fc1 Drop Windows compatibility code from systemmonitor integration (#152545) 2025-09-22 21:26:26 +02:00
Artur Pragacz 4eaf6784af Use satellite entity area in the default agent (#152762) 2025-09-22 13:34:31 -05:00
Abílio Costa 7b7265a6b0 Revert "Add EZVIZ battery camera power status and online status sensor (#146822)" (#152767) 2025-09-22 19:03:32 +02:00
Thomas D 9059e3dadc Prepare Volvo integration for new platforms (#152042) 2025-09-22 18:41:44 +02:00
J. Nick Koston d9d42b3ad5 Pass timezone to aioesphomeapi to ensure HA timezone takes precedence (#152756) 2025-09-22 09:54:15 -06:00
epenet d565fb3cb4 Bump mcp to 1.14.1 (#152737) 2025-09-22 17:33:48 +02:00
G Johansson 6e93e480d1 Use automatic reload options flow in integration (#152686) 2025-09-22 15:27:19 +01:00
Maciej Bieniek 5a3570702d Add re-auth flow to AccuWeather integration (#152755) 2025-09-22 16:27:09 +02:00
Abílio Costa b26b1df143 Fix unitless converter missing valid units (#152665) 2025-09-22 15:19:31 +01:00
Manu fdbff76733 Add collapse checklist field to Habitica create/update task actions (#150988) 2025-09-22 16:16:47 +02:00
epenet 018d59a892 Drop hass argument from service extraction helpers (#152738) 2025-09-22 16:08:07 +02:00
Andrew Jackson 4b6dd0eb8f Add optional language to Mastodon post action (#151072) 2025-09-22 16:01:09 +02:00
Wendelin b7db87bd3d Update regex for core logs path to include latest logs (#152747) 2025-09-22 14:07:49 +01:00
Jules Dejaeghere 86dc453c55 Add integration for Belgian weather provider meteo.be (#144689)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-09-22 13:28:41 +02:00
Abílio Costa a4f2c88c7f Add TriggerConfig to reduce ambiguity (#152563) 2025-09-22 12:24:47 +01:00
Erik Montnemery 3cdb894e61 Small improvement of exposed_entities test (#152744) 2025-09-22 13:16:02 +02:00
Bouwe Westerdijk cb837aaae5 Number snapshot testing for Plugwise (#152673) 2025-09-22 11:55:57 +02:00
G Johansson 82443ded34 Use already cached data in Nord Pool if valid (#152664) 2025-09-22 11:55:00 +02:00
Retha Runolfsson 71cc3b7fcd Add K11+ Vacuum for switchbot integration (#152643) 2025-09-22 11:51:39 +02:00
G Johansson e5658f9747 Use automatic reload options flow in statistics (#152682) 2025-09-22 11:48:58 +02:00
G Johansson 868ded141f Use automatic reload options flow in threshold (#152684) 2025-09-22 11:48:37 +02:00
LG-ThinQ-Integration 1151fa698d Add energy usage sensors of ThinQ devices. (#152141)
Co-authored-by: yunseon.park <yunseon.park@lge.com>
2025-09-22 11:47:18 +02:00
Paulus Schoutsen 2796d6110a Split up media source integration (#152721)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-22 11:46:24 +02:00
Tom Matheussen 844b97bd32 Add Satel Integra diagnostics (#152621)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-09-22 11:38:26 +02:00
Lukas 286b2500bd Pooldose: Add Dhcp discovery (#152253)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-09-22 11:31:40 +02:00
Joshua Leaper 4b7746ab51 Bump nessclient to 1.3.1 (#152700) 2025-09-22 11:01:04 +03:00
Abílio Costa ca1c366f4f Remove unused var from llm helper (#152724) 2025-09-22 09:57:16 +02:00
epenet de42ac14ac Drop unused hass argument from internal helper (#152733) 2025-09-22 09:56:52 +02:00
J. Nick Koston 7f7bd5a97f Bump aioesphomeapi to 41.5.0 (#152730) 2025-09-22 09:56:20 +02:00
epenet 8a70a1badb Drop hass argument from verify_domain_control (#147946) 2025-09-22 08:15:41 +02:00
Shay Levy 181741cab6 Use component role in Shelly sensor platform (#152710)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-22 01:03:29 +03:00
Maciej Bieniek 1e14fb6dab Use the common unique_id schema in the Shelly button platform (#152707) 2025-09-21 19:34:50 +02:00
Maciej Bieniek 2b6a125927 Support Shelly presencezone component (#152393)
Co-authored-by: Shay Levy <levyshay1@gmail.com>
Co-authored-by: Simone Chemelli <simone.chemelli@gmail.com>
2025-09-21 19:27:40 +02:00
Abílio Costa e61ad10708 Split sensor unit long condition (#152668) 2025-09-21 16:49:13 +01:00
Shay Levy 5177f9e8c2 Add support for Shelly object based entities (#152046)
Co-authored-by: Simone Chemelli <simone.chemelli@gmail.com>
2025-09-21 17:08:43 +03:00
Luke Lashley 850aeeb5eb Explicitly connect to the Roborock API before sending messages (#152697) 2025-09-21 12:37:15 +02:00
J. Nick Koston a1b9061060 Bump aiohomekit to 3.2.18 (#152694) 2025-09-21 12:14:21 +02:00
Maciej Bieniek 0ec1f27489 Use DeviceClass.PM4 in NAM integration (#152703) 2025-09-21 11:59:09 +02:00
J. Diego Rodríguez Royo befc93bc73 Delete Home Connect alarm clock entity from time platform (#152188) 2025-09-21 10:23:51 +01:00
Luke Lashley 1526d953bf Make Roborock A01 initilization threadsafe (#152699) 2025-09-21 07:52:26 +02:00
G Johansson d38082a5c8 Use automatic reload options flow in Scrape (#152681) 2025-09-20 22:02:23 +01:00
G Johansson 42850421d2 Use automatic reload options flow in wake_on_lan (#152683) 2025-09-20 22:01:11 +01:00
avee87 21a835c4b4 Expose pressure as a separate sensor for metoffice (#152685) 2025-09-20 21:59:14 +01:00
Manu e9294dbf72 Bump pyecotrend-ista to v3.4.0 (#152678) 2025-09-20 21:52:51 +01:00
Daniel Hjelseth Høyer 5c4dfbff1b Update Tibber lib 0.32.1 (#152677) 2025-09-20 20:52:24 +02:00
G Johansson abe628506d Use OptionsFlowWithReload in pvpc_hourly_pricing (#151255) 2025-09-20 18:45:06 +02:00
Franck Nijhof 12cc0ed18d Refactor template engine: Extract raise_no_default() into helper module (#152661)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-20 17:20:05 +01:00
Franck Nijhof 8ca7562390 Merge branch 'master' into dev 2025-09-20 14:28:37 +00:00
GSzabados 942f7eebb1 Add PM4 device class for Ecowitt (#152568) 2025-09-20 14:40:21 +02:00
Franck Nijhof 1a167e6aee Refactor template engine: Extract context and render info (#152630) 2025-09-20 14:15:24 +02:00
Stephan van Rooij 9531ae10f2 Remove volvooncall (#150725)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2025-09-20 14:08:53 +02:00
tronikos bfc9616abf Deprecate google_generative_ai_conversation.generate_content (#152644) 2025-09-20 07:50:31 -04:00
Jan Bouwhuis 054a5d751a Organize order MQTT subentry (test) globals and translation strings (#152576) 2025-09-20 13:24:30 +02:00
Andrea Turri a43ba4f966 Miele add new program phase mapping (#152647) 2025-09-20 11:33:12 +02:00
J. Nick Koston 1a5cae125f Handle unparsable responses during HomeKit Controller initial polling (#152636) 2025-09-19 22:51:21 -05:00
Franck Nijhof f3b9bda876 2025.9.4 (#152634) 2025-09-19 23:08:58 +02:00
Yevhenii Vaskivskyi 3f3aaa2815 Bump asusrouter to 1.21.0 (#151607) 2025-09-19 20:38:05 +00:00
Franck Nijhof 6dc7870779 Bump version to 2025.9.4 2025-09-19 20:07:56 +00:00
G Johansson be83416c72 Bump holidays to 0.81 (#152569) 2025-09-19 20:07:27 +00:00
G Johansson c745ee18eb Bump holidays to 0.80 (#152306) 2025-09-19 20:07:25 +00:00
tronikos cf907ae196 Bump opower to 0.15.5 (#152531) 2025-09-19 20:05:04 +00:00
Pete Sage 8eee53036a Fix Sonos set_volume float precision issue (#152493) 2025-09-19 20:00:42 +00:00
Alexandre CUER b37237d24b Bump pyemoncms to 0.1.3 (#152436) 2025-09-19 20:00:41 +00:00
Matthias Alphart 950e758b62 Fix KNX UI schema missing DPT (#152430) 2025-09-19 20:00:40 +00:00
Josef Zweck 9cd940b7df Add La Marzocco specific client headers (#152419) 2025-09-19 20:00:38 +00:00
Josef Zweck 10b186a20d Bump pylamarzocco to 2.1.0 (#152364) 2025-09-19 20:00:37 +00:00
Imeon-Energy 757aec1c6b Bump imeon_inverter_api to 0.4.0 (#152351)
Co-authored-by: TheBushBoy <theodavid@icloud.com>
2025-09-19 20:00:36 +00:00
Allen Porter 0b159bdb9c Update authorization server to prefer absolute urls (#152313)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-19 20:00:34 +00:00
J. Nick Koston 8728312e87 Bump aiohomekit to 3.2.17 (#152297) 2025-09-19 20:00:33 +00:00
Åke Strandberg bbb67db354 Add proper error handling for /actions endpoint for miele (#152290) 2025-09-19 20:00:31 +00:00
J. Nick Koston 265f5da21a Bump bluetooth-auto-recovery to 1.5.3 (#152256) 2025-09-19 20:00:30 +00:00
J. Nick Koston 54859e8a83 Bump aiohomekit to 3.2.16 (#152255) 2025-09-19 20:00:28 +00:00
Sean Dague c87dba878d Upgrade waterfurnace to 1.2.0 (#152241) 2025-09-19 20:00:27 +00:00
J. Nick Koston 8d8e008123 Fix HomeKit Controller overwhelming resource-limited devices by batching characteristic polling (#152209) 2025-09-19 20:00:25 +00:00
Yevhenii Vaskivskyi b30667a469 Fix bug with the hardcoded configuration_url (asuswrt) (#151858) 2025-09-19 20:00:23 +00:00
Manu 8920c548d5 Bump habiticalib to v0.4.5 (#151720) 2025-09-19 20:00:22 +00:00
Manu eac719f9af Bump habiticalib to v0.4.4 (#151332) 2025-09-19 20:00:20 +00:00
Aidan Timson 71c274cb91 Add power usage sensor to System Bridge (#152625) 2025-09-19 21:16:30 +02:00
Retha Runolfsson d4902361e6 Bump pySwitchbot to 0.71.0 (#152597) 2025-09-19 22:04:42 +03:00
Norbert Rittel f63eee3889 Fix typo and sentence-casing in honeywell exception string (#152619) 2025-09-19 17:07:46 +02:00
Aidan Timson 21bfe610d1 Update systembridgeconnector to 5.1.0 (#152623) 2025-09-19 17:07:15 +02:00
J. Nick Koston 21c174e895 Bump aioesphomeapi to 41.4.0 (#152618)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-09-19 08:20:00 -06:00
Simon Roberts ec148e0459 Add PM4 (particulates < 4μm) sensor and number device classes (#112867)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-09-19 08:12:09 -06:00
Maciej Bieniek 286763b998 Fix KeyError for Shelly Duo Bulb Gen3 (#152612) 2025-09-19 07:22:48 -06:00
Shay Levy 5f88122a2b Fix Shelly Wall Display virtual button platform (#152582) 2025-09-19 11:44:08 +03:00
Åke Strandberg 31968d16ab Refactor miele program phase codes part 2(3) (#144180) 2025-09-19 09:54:24 +02:00
epenet c125554817 Rename modbus base entities (#152595) 2025-09-19 09:17:31 +02:00
Abílio Costa 10f2955d34 Update Whirlpool quality scale to silver (#152505) 2025-09-19 08:30:10 +02:00
Paulus Schoutsen 55712b784c Bump aioesphomeapi to 41.3.0 (#152588) 2025-09-18 22:49:03 -04:00
Paulus Schoutsen fe3a929556 Fix reolink test (#152587) 2025-09-18 21:03:13 -04:00
J. Nick Koston 534801e80d Migrate yale and august integrations to use new lock API (#152579) 2025-09-18 17:20:38 -05:00
J. Nick Koston 8aeda5a0c0 Bump aioesphomeapi to 41.2.0 (#152578) 2025-09-19 01:03:59 +03:00
Ville Skyttä eb1cbbc75c Upgrade upcloud-api to 2.8.0 (#152577) 2025-09-19 01:03:31 +03:00
Przemko92 fa8a4d7098 Add Compit integration (#132164)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-09-18 21:53:49 +01:00
Andre Lengwenus 2623ebac4d Bump pypck to 0.8.12 (#152573) 2025-09-18 23:34:37 +03:00
Michael Hansen 1746c51ce4 Fix error with pipeline device removal due to multiple selects (#152560) 2025-09-18 14:34:03 -05:00
Joakim Sørensen 8b984a2105 Remove ludeeus as a codeowner for analytics (#152558) 2025-09-18 22:08:22 +03:00
Allen Porter ebee370a56 Bump python roborock to 2.44.1 (#152557) 2025-09-18 21:51:16 +03:00
starkillerOG dabd096587 Add color temperature support to Reolink light entity (#152546) 2025-09-18 21:48:18 +03:00
Simone Chemelli 21399818af Remove stale devices for Comelit SimpleHome (#151519) 2025-09-18 19:43:38 +01:00
G Johansson 4354214fbf Bump holidays to 0.81 (#152569) 2025-09-18 20:35:21 +02:00
Erik Montnemery 5bd39804f1 Remove EntityComponent.async_register_legacy_entity_service (#152539) 2025-09-18 20:34:25 +02:00
Norbert Rittel 6d3ad3ab9c Replace "iCloud account" with "Apple Account" (#152561) 2025-09-18 18:39:54 +02:00
Petar Petrov 4c212bdcd4 Enable thread migration for ZBT integration (#152550) 2025-09-18 18:33:06 +03:00
Petar Petrov b91b39580f Add migrate options to ZBT protocol picker (#152532) 2025-09-18 11:13:58 -04:00
Jan Bouwhuis 472d70b6c9 Add comment on conversion factor for Carbon monoxide on dependency molecular weight (#152535) 2025-09-18 15:36:12 +02:00
tronikos 017a84a859 Bump opower to 0.15.5 (#152531) 2025-09-18 14:29:27 +03:00
starkillerOG d184540967 Bump reolink-aio to 0.15.1 (#152533) 2025-09-18 14:28:16 +03:00
Erik Montnemery 1740984b3b Improve comments in SelectedEntities (#152540) 2025-09-18 14:12:33 +03:00
droans 4db8592c61 Add support for overriding entity_picture to universal (#149387) 2025-09-18 11:59:29 +02:00
Stefan Agner 27e630c107 Make systemmonitor tests timezone independent (#152537) 2025-09-18 12:58:09 +03:00
Åke Strandberg ea8833342d Bump dependency pymiele to v0.5.5 and subsequent code changes (#152534) 2025-09-18 10:33:55 +02:00
epenet 87be2ba823 Use compat UOM in _is_valid_suggested_unit (#152350)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-09-18 08:38:15 +02:00
Artur Pragacz 51c35eb631 Move default conversation agent to manager (#152479) 2025-09-18 08:22:56 +02:00
Samuel Xiao 24a86d042f Bumb switchbot api to v2.8.0 (#152506) 2025-09-18 07:46:51 +02:00
Manu cd6f653123 Bump aiontfy to v0.6.0 (#152520) 2025-09-18 07:45:22 +02:00
J. Nick Koston fd05ddca28 Bump yalexs to 9.2.0 (#152527) 2025-09-18 07:43:54 +02:00
Artur Pragacz a1f2eb44ae Move trigger-specific fields into options in new-style triggers (#151314) 2025-09-18 07:35:39 +02:00
Samuel Xiao c4ddc03dbc Update codeowner for switchbot cloud Integration (#152526) 2025-09-17 22:54:22 -05:00
J. Nick Koston 9db5aafb71 Bump yalexs to 9.1.0 (#152457) 2025-09-17 22:11:35 -05:00
Ivan Lopez Hernandez 64cdcfb613 Bump google-genai to 1.38.0 (#152523) 2025-09-17 22:14:04 -04:00
Paulus Schoutsen c761ce699c Tweak usage prediction common control algorithm (#152490) 2025-09-17 19:04:25 -04:00
Martin Hjelmare 40ebce4ae8 Improve Home Assistant Hardware flow (#152451)
Co-authored-by: puddly <32534428+puddly@users.noreply.github.com>
2025-09-17 18:23:38 -04:00
puddly 29914d6722 Bump ZHA to 0.0.71 (#152511) 2025-09-17 18:23:08 -04:00
Jan Bouwhuis 5eef6edded Add mg/m³ as a valid UOM for sensor/number Carbon Monoxide device class (#152456) 2025-09-17 22:04:23 +01:00
epenet db729273a5 Add pymodbus to PACKAGE_CHECK_VERSION_RANGE (#152494) 2025-09-17 17:22:45 +02:00
Abílio Costa 946d75d651 Merge similar Whirlpool init tests (#152497) 2025-09-17 17:18:54 +02:00
Abílio Costa 093f779edb Remove target humidity methods from Whirlpool climate (#152498) 2025-09-17 17:18:20 +02:00
Paulus Schoutsen 87658e77a7 Clean up stale comment in AI Task test (#152492) 2025-09-17 17:17:20 +02:00
dontinelli 38f65cda98 Bump solarlog_cli to 0.6.0 (#152500) 2025-09-17 16:02:51 +01:00
Jan Čermák 797c6ddedd Fix APT cache restore failures in CI (#152481)
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
2025-09-17 16:52:09 +02:00
NANI fe8a53407a Add Victron Remote Monitoring integration (#143687)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-09-17 16:38:04 +02:00
Abílio Costa ae5f57fd99 Add unique_id to Whirlpool config entry mock (#152496) 2025-09-17 14:38:53 +01:00
Abílio Costa a93c3cc23c Make Whirlpool log when entity goes unavailable (#152064) 2025-09-17 15:00:23 +02:00
Pete Sage 804b42e1fb Fix Sonos set_volume float precision issue (#152493) 2025-09-17 14:39:28 +02:00
karwosts a4f15e4840 Add debug logging to derivative (#152489) 2025-09-17 14:19:48 +02:00
Pete Sage 2471177c84 Set Sonos quality scale to bronze (#152487) 2025-09-17 14:16:08 +02:00
Yuxin Wang a494d3ec69 Sort the resources for deterministic sensor addition order in APCUPSD (#152467) 2025-09-17 10:41:19 +02:00
Paulus Schoutsen b10a9721a7 Add async_get_image helper to Image integration (#152465) 2025-09-17 10:35:55 +02:00
Paulus Schoutsen 04c0bb20d6 AI Task to store generated images in media dir (#152463) 2025-09-17 10:30:15 +02:00
Paulus Schoutsen 1598c4ebe8 Bump aioesphomeapi to 41.1.0 (#152461)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-09-16 20:18:54 -04:00
karwosts d67ec7593a Add diagnostics to history_stats (#152460) 2025-09-17 00:02:35 +01:00
Franck Nijhof 4a4c124181 Refactor template engine: Extract collection & data structure functions into CollectionExtension (#152446) 2025-09-16 18:48:50 -04:00
Franck Nijhof c34af4be86 Add active built-in and custom integrations to Cloud support package (#152452) 2025-09-16 18:47:00 -04:00
GSzabados 823071b722 Add LDS01 support (#151820)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-09-16 21:33:47 +02:00
Daniel Jansen 462fa77ba1 Improve waze_travel_time tests (#146495)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-09-16 21:24:51 +02:00
Yevhenii Vaskivskyi 24fc8b9297 Fix bug with the hardcoded configuration_url (asuswrt) (#151858) 2025-09-16 21:18:29 +02:00
Paulus Schoutsen 2596ab2940 OpenAI to use provided mimetype when available (#152407) 2025-09-16 22:11:46 +03:00
Robert Resch 23fa84e20e Verify that Ecovacs integration is setup without any errors in the tests (#152447) 2025-09-16 20:55:44 +02:00
Shay Levy 7f13141297 Bump aioshelly 13.10.0 (#152442) 2025-09-16 21:25:09 +03:00
karwosts 770f41d079 Diagnostics for derivative sensor (#152445) 2025-09-16 14:24:05 -04:00
Shay Levy df16e85359 Fix typo in update_not_available key in Shelly strings (#152444) 2025-09-16 14:23:10 -04:00
Nathan Spencer 3c6db923a3 Deprecate Litter-Robot 4 night light mode switch (#152249) 2025-09-16 20:18:26 +02:00
Thomas D 450c47f932 Use new method to get the access token in the Volvo integration (#151625)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-09-16 20:17:43 +02:00
Norbert Rittel 048f64eccf Improve two unsupported_xxx issue descriptions in hassio (#152387)
Co-authored-by: Stefan Agner <stefan@agner.ch>
2025-09-16 19:52:12 +02:00
Maciej Bieniek c4c523e8b7 Open a repair issue if Shelly Wall Display firmware is older than 2.3.0 (#152399) 2025-09-16 19:48:47 +02:00
Matthias Alphart 87e30e0907 Fix KNX UI schema missing DPT (#152430) 2025-09-16 19:39:39 +02:00
Alexandre CUER 74660da2d2 Bump pyemoncms to 0.1.3 (#152436) 2025-09-16 18:32:13 +02:00
Maciej Bieniek 6b8c180509 Bump imgw_pib to version 1.5.6 (#152435) 2025-09-16 18:30:22 +02:00
Alessandro Manighetti eb4a873c43 Add m/min of speed sensors (#146441) 2025-09-16 18:02:22 +02:00
G Johansson 6aafa666d6 Add calendar to Workday (#150596) 2025-09-16 17:29:04 +02:00
Mike Degatano 9ee9bb368d Move Supervisor created persistent notifications into repairs (#152066) 2025-09-16 11:24:48 -04:00
Tom Matheussen 6e4258c8a9 Add Satel Integra config flow (#138946)
Co-authored-by: Shay Levy <levyshay1@gmail.com>
2025-09-16 17:24:15 +02:00
Paulus Schoutsen d65e704823 Add usage_prediction integration (#151206)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-09-16 10:33:46 -04:00
Retha Runolfsson aadaf87c16 Add switchbot relayswitch 2PM (#146140) 2025-09-16 15:59:13 +02:00
Timothy e70b147c0c Add missing content type to backup http endpoint (#152433) 2025-09-16 09:45:21 -04:00
yufeng 031b12752f Add sensors for Tuya energy storage systems (xnyjcn) (#149237)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-09-16 15:34:21 +02:00
Samuel Xiao df0cfd69a9 Add Climate Panel support to Switchbot Cloud (#152427) 2025-09-16 14:14:09 +02:00
marc7s b2c53f2d78 Add geocaching cache sensors (#145453) 2025-09-16 14:13:54 +02:00
Rafael López Diez 3649e949b1 Add support for sending chat actions in Telegram bot integration (#151378) 2025-09-16 14:06:15 +02:00
Rafael López Diez de7e2303a7 Add support for multi-tap action in Lutron Caseta integration (#150551) 2025-09-16 13:32:10 +02:00
onsmam 892f3f267b Added rain_start and lightningstrike event to publish on the event bus (#146652)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-09-16 13:31:43 +02:00
Marc Mueller 0254285285 Fix warning in template extensions tests (#152425) 2025-09-16 13:30:36 +02:00
Chris Oldfield 44a95242dc Add downloading and seeding counts to Deluge (#150623) 2025-09-16 13:06:14 +02:00
Marc Mueller f9b1c52d65 Fix warning in prowl tests (#152424) 2025-09-16 12:42:37 +02:00
Josef Zweck aa8d78622c Add La Marzocco specific client headers (#152419) 2025-09-16 13:15:57 +03:00
Franck Nijhof ca6289a576 Refactor template engine: Extract string functions into StringExtension (#152420) 2025-09-16 13:15:43 +03:00
Erik Montnemery 0f372f4b47 Improve condition schema validation (#144793) 2025-09-16 10:44:26 +02:00
Franck Nijhof 4bba167ab3 Refactor template engine: Extract regex functions into RegexExtension (#152417) 2025-09-16 10:38:01 +02:00
Jan-Philipp Benecke 962c0c443d Improve setup completion message of Improv BLE (#152412) 2025-09-16 10:37:47 +02:00
Duco Sebel c6b4cac28a Remember HomeWizard uptime sensor value as timestamp to prevent it spamming the state (#150680)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-16 10:29:37 +02:00
dependabot[bot] 3c7e3a5e30 Bump home-assistant/builder from 2025.03.0 to 2025.09.0 (#152413)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-16 10:28:49 +02:00
Maciej Bieniek fa698956c3 Fix the illuminance level entity name in Shelly integration (#152400)
Co-authored-by: Shay Levy <levyshay1@gmail.com>
2025-09-16 10:16:43 +02:00
Klaas Schoute 32f136b12f Update P1 Monitor integration to use settings method during config flow (#152391) 2025-09-16 10:11:29 +02:00
Josef Zweck e1f617df25 Bump pylamarzocco to 2.1.0 (#152364) 2025-09-16 10:08:08 +02:00
Tomeroeni 84f1b8a5cc Bump aiounifi version to 87 (#152395) 2025-09-16 10:04:06 +02:00
kylehakala e9cedf4852 Bump aioridwell to 2025.09.0 (#152405) 2025-09-16 10:01:50 +02:00
kingy444 9c72b40ab4 Bump HunterDouglas_Powerview dependency to aiopvapi 3.2.1 (#152409) 2025-09-16 09:58:41 +02:00
Marcus Gustavsson 65f655e5f5 Change Prowl to use the prowlpy library and add tests for the Prowl component (#149034)
Co-authored-by: Joostlek <joostlek@outlook.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-09-16 09:23:08 +02:00
Erik Montnemery af28573894 Refactor zwave js event trigger (#144885) 2025-09-16 07:49:09 +02:00
Jan-Philipp Benecke c5fc1de3df Update url in success message of Improv BLE to use markdown (#152388) 2025-09-15 19:50:19 +01:00
karwosts 1df1144eb9 Add 'stations near me' to radio browser (#150907) 2025-09-15 14:47:16 -04:00
Artur Pragacz d51c0e3752 Revert "Add Matter service actions for vacuum area (#151467)" (#152386) 2025-09-15 20:14:09 +02:00
Ernst Klamer f5157878c2 Bthome encryption fix (#152384) 2025-09-15 20:46:43 +03:00
Lennard Beers fb723571b6 Bump eq3btsmart to 2.3.0 (#152383) 2025-09-15 18:36:12 +01:00
puddly dbf80c3ce3 Bump universal-silabs-flasher to 0.0.32 (#152381) 2025-09-15 19:10:52 +02:00
Joost Lekkerkerker e0a774b598 Add sensor test to Nederlandse Spoorwegen (#152375) 2025-09-15 18:03:14 +01:00
Manu 168afc5f0e Bump pyrate-limiter to v3.9.0 (#152370) 2025-09-15 18:54:26 +02:00
Paulus Schoutsen af23670854 Add quality-scale-verifier Claude agent (#152333) 2025-09-15 17:40:15 +01:00
Jan Bouwhuis 935ce421df Remove unused const in MQTT JSON Light component (#152377) 2025-09-15 11:36:19 -05:00
Klaas Schoute c60ad8179d Bump p1-monitor to v3.2.0 (#152378) 2025-09-15 18:12:04 +02:00
Nc Hodges 14ad3364e3 Add Re-Configure workflow to the Elk M1 Integration (#146368)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-09-15 11:02:00 -05:00
J. Nick Koston e229f36648 Clarify contributor responsibility when using AI-generated code (#152379) 2025-09-15 17:59:06 +02:00
Norbert Rittel f4f99e015c Clarify "discovery_requires_supervisor" message in zwave_js (#152345) 2025-09-15 17:14:41 +02:00
Joost Lekkerkerker 5dc509cba0 Add typing to Nederlandse Spoorwegen (#152367) 2025-09-15 16:19:39 +02:00
Shay Levy 75597ac98d Add Shelly removal condition for virtual components (#152312) 2025-09-15 16:15:15 +03:00
Heindrich Paul b503f792b5 Add config flow to NS (#151567)
Signed-off-by: Heindrich Paul <heindrich.paul@gmail.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-09-15 15:13:43 +02:00
Ludovic BOUÉ 410c3df6dd Add Matter service actions for vacuum area (#151467)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-09-15 13:52:26 +02:00
virtualbitzz f1bf28df18 Add Matter climate running state heat fan and cool fan (#151535)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-09-15 13:28:40 +02:00
Patrick 99fb64af9b Add new USB drives to Synology DSM without reloading integration (#146829)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-09-15 13:12:57 +02:00
Shay Levy c0af0159e3 Use Entity Description in Shelly light platform (#137118) 2025-09-15 14:10:25 +03:00
Jan Bouwhuis 71749da3a3 Rename MQTT tag and device_automation setup helpers (#152344) 2025-09-15 11:52:24 +02:00
Norbert Rittel b01be94034 Update "Find my iPhone" to "Find My" in icloud (#152354) 2025-09-15 11:52:08 +02:00
Jan Gutowski 47ec8b7f12 Bump nibe to 2.18.0 (#152353) 2025-09-15 11:41:44 +02:00
dependabot[bot] 93ec9e448e Bump sigstore/cosign-installer from 3.9.2 to 3.10.0 (#152343)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-15 11:20:21 +02:00
Lennard Beers 90bc41dd02 Bump eq3btsmart to 2.2.0 (#152334) 2025-09-15 11:16:39 +02:00
epenet 410d869f3d Improve type hints in zha tests (#152347) 2025-09-15 11:16:10 +02:00
Imeon-Energy d75d9f2589 Bump imeon_inverter_api to 0.4.0 (#152351)
Co-authored-by: TheBushBoy <theodavid@icloud.com>
2025-09-15 11:14:31 +02:00
Christopher Fenner afbb832a57 Improve config flow for openweathermap integration (#152319) 2025-09-15 11:13:39 +02:00
epenet bdc881c87a Handle missing argument in hass_enforce_type_hints (#152342) 2025-09-15 09:45:27 +02:00
J. Nick Koston 22ea269ed8 Bump aioesphomeapi to 41.0.0 (#152332) 2025-09-14 23:22:10 -04:00
tronikos 10fecbaf4d Mark Opower as bronze (#148103)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-09-14 23:21:05 -04:00
Franck Nijhof cbdc1dc5b6 Refactor template engine: Extract math & statistical functions into MathExtension (#152266) 2025-09-14 20:48:29 -04:00
J. Nick Koston b203a831c9 Bump aioesphomeapi to 40.2.1 (#152327) 2025-09-14 19:31:55 -05:00
G Johansson 5ccbee4c9a Break long strings in entity platform/component tests (#152320) 2025-09-14 23:27:04 +02:00
Allen Porter 1483c9488f Update authorization server to prefer absolute urls (#152313)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-14 14:07:31 -07:00
Paulus Schoutsen f5535db24c Automatically generate entity platform enum (#152193)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-14 22:44:48 +02:00
Shay Levy e40ecdfb00 Remove Shelly empty sub-devices (#152251) 2025-09-14 22:43:37 +02:00
Norbert Rittel 2f4c69bbd5 Simplify description of direction_command_topic in mqtt (#150617) 2025-09-14 22:05:05 +02:00
Norbert Rittel dd0f6a702b Small fixes of user-facing strings in esphome (#152311) 2025-09-14 21:36:05 +03:00
Norbert Rittel 5ba580bc25 Capitalize "Supervisor" in two issues strings of hassio (#152303)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2025-09-14 21:35:47 +03:00
Samuel Xiao c13002bdd5 Add supported device[Plug-Mini-EU] for switchbot cloud (#151019) 2025-09-14 20:00:15 +02:00
Niklas Wagner 75d22191a0 Fix local_todo capitalization to preserve user input (#150814) 2025-09-14 19:53:41 +02:00
GSzabados 58d6549f1c Add display precision for rain rate and rain count (#151822) 2025-09-14 19:49:59 +02:00
Åke Strandberg 1fcc6df1fd Add proper error handling for /actions endpoint for miele (#152290) 2025-09-14 19:47:01 +02:00
J. Nick Koston 9bf467e6d1 Bump aioesphomeapi to 40.2.0 (#152272) 2025-09-14 19:39:44 +02:00
J. Nick Koston d877d6d93f Fix Lutron Caseta shade stuttering and improve stop functionality (#152207)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-14 19:35:18 +02:00
Lukas Waslowski d2b255ba92 nitpick: Add parameter types to _test_selector function signature (#152226) 2025-09-14 19:33:43 +02:00
Christopher Fenner 1509c429d6 Improve husqvarna_automower_ble config flow (#144877) 2025-09-14 19:32:10 +02:00
G Johansson af9717c1cd Raise error for entity services without a correct schema (#151165) 2025-09-14 19:17:26 +02:00
karwosts 49e75c9cf8 Fix browse by language in radio browser (#152296) 2025-09-14 19:04:59 +02:00
J. Nick Koston c97f16a96d Bump aiohomekit to 3.2.17 (#152297) 2025-09-14 19:02:11 +02:00
Bram Gerritsen a3a4433d62 Add missing unit conversion for BTU/h (#152300) 2025-09-14 19:00:44 +02:00
G Johansson f832002afd Bump holidays to 0.80 (#152306) 2025-09-14 17:51:47 +02:00
J. Diego Rodríguez Royo dbc7f2b43c Remove Home Connect stale code (#152307) 2025-09-14 17:51:11 +02:00
Galorhallen 1cd3a1eede Updated govee local api to 2.2.0 (#152289) 2025-09-14 16:16:26 +02:00
Norbert Rittel 7d6e0d44b0 Capitalize "Core" and "Supervisor" in backup issue strings (#152292) 2025-09-14 16:14:08 +02:00
jan iversen 2bb6d745ca Flexit: Fix wrong import from modbus. (#152225) 2025-09-14 12:04:07 +02:00
Todd Fast beb9d7856c Reduce PurpleAir sensor polling rate from every 2m to every 5m (#152271) 2025-09-14 11:59:48 +02:00
PaulCavill 6a4c8a550a Fix login issue with pyicloud (#129059)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-09-14 11:34:14 +02:00
Simon Lamon 7d23752a3f Unpin home-assistant/builder action (#152279) 2025-09-14 11:11:59 +02:00
Adam Goode c2b2a78db5 Change prusalink update cooldown to 1.0 seconds (#151060) 2025-09-14 10:58:00 +02:00
Manu 0fb6bbee59 Improve error logging for protected topic subscription in ntfy integration (#152244) 2025-09-14 10:02:48 +03:00
Paulus Schoutsen 3499ed7a98 2025.9.3 (#152237) 2025-09-13 08:44:08 -04:00
Paulus Schoutsen 2c809d5903 Bump version to 2025.9.3 2025-09-13 12:14:15 +00:00
J. Nick Koston 40988198f3 Bump habluetooth to 5.6.4 (#152227) 2025-09-13 12:14:08 +00:00
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
850 changed files with 51983 additions and 9528 deletions
@@ -0,0 +1,77 @@
---
name: quality-scale-rule-verifier
description: |
Use this agent when you need to verify that a Home Assistant integration follows a specific quality scale rule. This includes checking if the integration implements required patterns, configurations, or code structures defined by the quality scale system.
<example>
Context: The user wants to verify if an integration follows a specific quality scale rule.
user: "Check if the peblar integration follows the config-flow rule"
assistant: "I'll use the quality scale rule verifier to check if the peblar integration properly implements the config-flow rule."
<commentary>
Since the user is asking to verify a quality scale rule implementation, use the quality-scale-rule-verifier agent.
</commentary>
</example>
<example>
Context: The user is reviewing if an integration reaches a specific quality scale level.
user: "Verify that this integration reaches the bronze quality scale"
assistant: "Let me use the quality scale rule verifier to check the bronze quality scale implementation."
<commentary>
The user wants to verify the integration has reached a certain quality level, so use multiple quality-scale-rule-verifier agents to verify each bronze rule.
</commentary>
</example>
model: inherit
color: yellow
tools: Read, Bash, Grep, Glob, WebFetch
---
You are an expert Home Assistant integration quality scale auditor specializing in verifying compliance with specific quality scale rules. You have deep knowledge of Home Assistant's architecture, best practices, and the quality scale system that ensures integration consistency and reliability.
You will verify if an integration follows a specific quality scale rule by:
1. **Fetching Rule Documentation**: Retrieve the official rule documentation from:
`https://raw.githubusercontent.com/home-assistant/developers.home-assistant/refs/heads/master/docs/core/integration-quality-scale/rules/{rule_name}.md`
where `{rule_name}` is the rule identifier (e.g., 'config-flow', 'entity-unique-id', 'parallel-updates')
2. **Understanding Rule Requirements**: Parse the rule documentation to identify:
- Core requirements and mandatory implementations
- Specific code patterns or configurations required
- Common violations and anti-patterns
- Exemption criteria (when a rule might not apply)
- The quality tier this rule belongs to (Bronze, Silver, Gold, Platinum)
3. **Analyzing Integration Code**: Examine the integration's codebase at `homeassistant/components/<integration domain>` focusing on:
- `manifest.json` for quality scale declaration and configuration
- `quality_scale.yaml` for rule status (done, todo, exempt)
- Relevant Python modules based on the rule requirements
- Configuration files and service definitions as needed
4. **Verification Process**:
- Check if the rule is marked as 'done', 'todo', or 'exempt' in quality_scale.yaml
- If marked 'exempt', verify the exemption reason is valid
- If marked 'done', verify the actual implementation matches requirements
- Identify specific files and code sections that demonstrate compliance or violations
- Consider the integration's declared quality tier when applying rules
- To fetch the integration docs, use WebFetch to fetch from `https://raw.githubusercontent.com/home-assistant/home-assistant.io/refs/heads/current/source/_integrations/<integration domain>.markdown`
- To fetch information about a PyPI package, use the URL `https://pypi.org/pypi/<package>/json`
5. **Reporting Findings**: Provide a comprehensive verification report that includes:
- **Rule Summary**: Brief description of what the rule requires
- **Compliance Status**: Clear pass/fail/exempt determination
- **Evidence**: Specific code examples showing compliance or violations
- **Issues Found**: Detailed list of any non-compliance issues with file locations
- **Recommendations**: Actionable steps to achieve compliance if needed
- **Exemption Analysis**: If applicable, whether the exemption is justified
When examining code, you will:
- Look for exact implementation patterns specified in the rule
- Verify all required components are present and properly configured
- Check for common mistakes and anti-patterns
- Consider edge cases and error handling requirements
- Validate that implementations follow Home Assistant conventions
You will be thorough but focused, examining only the aspects relevant to the specific rule being verified. You will provide clear, actionable feedback that helps developers understand both what needs to be fixed and why it matters for integration quality.
If you cannot access the rule documentation or find the integration code, clearly state what information is missing and what you would need to complete the verification.
Remember that quality scale rules are cumulative - Bronze rules apply to all integrations with a quality scale, Silver rules apply to Silver+ integrations, and so on. Always consider the integration's target quality level when determining which rules should be enforced.
+5
View File
@@ -55,8 +55,12 @@
creating the PR. If you're unsure about any of them, don't hesitate to ask.
We're here to help! This is simply a reminder of what we are going to look
for before merging your code.
AI tools are welcome, but contributors are responsible for *fully*
understanding the code before submitting a PR.
-->
- [ ] I understand the code I am submitting and can explain how it works.
- [ ] The code change is tested and works locally.
- [ ] Local tests pass. **Your PR cannot be merged unless tests pass**
- [ ] There is no commented out code in this PR.
@@ -64,6 +68,7 @@
- [ ] I have followed the [perfect PR recommendations][perfect-pr]
- [ ] The code has been formatted using Ruff (`ruff format homeassistant tests`)
- [ ] Tests have been added to verify that the new code works.
- [ ] Any generated code has been carefully reviewed for correctness and compliance with project standards.
If user exposed functionality or configuration variables are added/changed:
+5 -3
View File
@@ -196,8 +196,9 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# home-assistant/builder doesn't support sha pinning
- name: Build base image
uses: home-assistant/builder@71885366c80f6ead6ae8c364b61d910e0dc5addc # 2025.03.0
uses: home-assistant/builder@2025.09.0
with:
args: |
$BUILD_ARGS \
@@ -262,8 +263,9 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# home-assistant/builder doesn't support sha pinning
- name: Build base image
uses: home-assistant/builder@71885366c80f6ead6ae8c364b61d910e0dc5addc # 2025.03.0
uses: home-assistant/builder@2025.09.0
with:
args: |
$BUILD_ARGS \
@@ -324,7 +326,7 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Cosign
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
with:
cosign-release: "v2.2.3"
+18 -7
View File
@@ -523,22 +523,24 @@ jobs:
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-uv-${{
env.UV_CACHE_VERSION }}-${{ steps.generate-uv-key.outputs.version }}-${{
env.HA_SHORT_VERSION }}-
- name: Restore apt cache
if: steps.cache-venv.outputs.cache-hit != 'true'
id: cache-apt
uses: actions/cache@v4.2.4
- name: Check if apt cache exists
id: cache-apt-check
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
lookup-only: ${{ steps.cache-venv.outputs.cache-hit == 'true' }}
path: |
${{ env.APT_CACHE_DIR }}
${{ env.APT_LIST_CACHE_DIR }}
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
- name: Install additional OS dependencies
if: steps.cache-venv.outputs.cache-hit != 'true'
if: |
steps.cache-venv.outputs.cache-hit != 'true'
|| steps.cache-apt-check.outputs.cache-hit != 'true'
timeout-minutes: 10
run: |
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
if [[ "${{ steps.cache-apt.outputs.cache-hit }}" != 'true' ]]; then
if [[ "${{ steps.cache-apt-check.outputs.cache-hit }}" != 'true' ]]; then
mkdir -p ${{ env.APT_CACHE_DIR }}
mkdir -p ${{ env.APT_LIST_CACHE_DIR }}
fi
@@ -563,9 +565,18 @@ jobs:
libswscale-dev \
libudev-dev
if [[ "${{ steps.cache-apt.outputs.cache-hit }}" != 'true' ]]; then
if [[ "${{ steps.cache-apt-check.outputs.cache-hit }}" != 'true' ]]; then
sudo chmod -R 755 ${{ env.APT_CACHE_BASE }}
fi
- name: Save apt cache
if: steps.cache-apt-check.outputs.cache-hit != 'true'
uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: |
${{ env.APT_CACHE_DIR }}
${{ env.APT_LIST_CACHE_DIR }}
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
+1 -1
View File
@@ -140,5 +140,5 @@ tmp_cache
pytest_buckets.txt
# AI tooling
.claude
.claude/settings.local.json
+1
View File
@@ -142,6 +142,7 @@ homeassistant.components.cloud.*
homeassistant.components.co2signal.*
homeassistant.components.comelit.*
homeassistant.components.command_line.*
homeassistant.components.compit.*
homeassistant.components.config.*
homeassistant.components.configurator.*
homeassistant.components.cookidoo.*
Generated
+18 -7
View File
@@ -107,8 +107,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/ambient_station/ @bachya
/tests/components/ambient_station/ @bachya
/homeassistant/components/amcrest/ @flacjacket
/homeassistant/components/analytics/ @home-assistant/core @ludeeus
/tests/components/analytics/ @home-assistant/core @ludeeus
/homeassistant/components/analytics/ @home-assistant/core
/tests/components/analytics/ @home-assistant/core
/homeassistant/components/analytics_insights/ @joostlek
/tests/components/analytics_insights/ @joostlek
/homeassistant/components/android_ip_webcam/ @engrbm87
@@ -292,6 +292,8 @@ build.json @home-assistant/supervisor
/tests/components/command_line/ @gjohansson-ST
/homeassistant/components/compensation/ @Petro31
/tests/components/compensation/ @Petro31
/homeassistant/components/compit/ @Przemko92
/tests/components/compit/ @Przemko92
/homeassistant/components/config/ @home-assistant/core
/tests/components/config/ @home-assistant/core
/homeassistant/components/configurator/ @home-assistant/core
@@ -770,6 +772,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/iqvia/ @bachya
/tests/components/iqvia/ @bachya
/homeassistant/components/irish_rail_transport/ @ttroy50
/homeassistant/components/irm_kmi/ @jdejaegh
/tests/components/irm_kmi/ @jdejaegh
/homeassistant/components/iron_os/ @tr4nt0r
/tests/components/iron_os/ @tr4nt0r
/homeassistant/components/isal/ @bdraco
@@ -1017,7 +1021,8 @@ build.json @home-assistant/supervisor
/tests/components/nanoleaf/ @milanmeu @joostlek
/homeassistant/components/nasweb/ @nasWebio
/tests/components/nasweb/ @nasWebio
/homeassistant/components/nederlandse_spoorwegen/ @YarmoM
/homeassistant/components/nederlandse_spoorwegen/ @YarmoM @heindrichpaul
/tests/components/nederlandse_spoorwegen/ @YarmoM @heindrichpaul
/homeassistant/components/ness_alarm/ @nickw444
/tests/components/ness_alarm/ @nickw444
/homeassistant/components/nest/ @allenporter
@@ -1349,6 +1354,8 @@ build.json @home-assistant/supervisor
/tests/components/samsungtv/ @chemelli74 @epenet
/homeassistant/components/sanix/ @tomaszsluszniak
/tests/components/sanix/ @tomaszsluszniak
/homeassistant/components/satel_integra/ @Tommatheussen
/tests/components/satel_integra/ @Tommatheussen
/homeassistant/components/scene/ @home-assistant/core
/tests/components/scene/ @home-assistant/core
/homeassistant/components/schedule/ @home-assistant/core
@@ -1530,8 +1537,8 @@ build.json @home-assistant/supervisor
/tests/components/switchbee/ @jafar-atili
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur @XiaoLing-git
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur @XiaoLing-git
/homeassistant/components/switcher_kis/ @thecode @YogevBokobza
/tests/components/switcher_kis/ @thecode @YogevBokobza
/homeassistant/components/switchmate/ @danielhiversen @qiz-li
@@ -1676,6 +1683,8 @@ build.json @home-assistant/supervisor
/tests/components/uptime_kuma/ @tr4nt0r
/homeassistant/components/uptimerobot/ @ludeeus @chemelli74
/tests/components/uptimerobot/ @ludeeus @chemelli74
/homeassistant/components/usage_prediction/ @home-assistant/core
/tests/components/usage_prediction/ @home-assistant/core
/homeassistant/components/usb/ @bdraco
/tests/components/usb/ @bdraco
/homeassistant/components/usgs_earthquakes_feed/ @exxamalte
@@ -1705,6 +1714,8 @@ build.json @home-assistant/supervisor
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
/homeassistant/components/vicare/ @CFenner
/tests/components/vicare/ @CFenner
/homeassistant/components/victron_remote_monitoring/ @AndyTempel
/tests/components/victron_remote_monitoring/ @AndyTempel
/homeassistant/components/vilfo/ @ManneW
/tests/components/vilfo/ @ManneW
/homeassistant/components/vivotek/ @HarlemSquirrel
@@ -1720,8 +1731,8 @@ build.json @home-assistant/supervisor
/tests/components/volumio/ @OnFreund
/homeassistant/components/volvo/ @thomasddn
/tests/components/volvo/ @thomasddn
/homeassistant/components/volvooncall/ @molobrakos
/tests/components/volvooncall/ @molobrakos
/homeassistant/components/volvooncall/ @molobrakos @svrooij
/tests/components/volvooncall/ @molobrakos @svrooij
/homeassistant/components/wake_on_lan/ @ntilley905
/tests/components/wake_on_lan/ @ntilley905
/homeassistant/components/wake_word/ @home-assistant/core @synesthesiam
@@ -4,10 +4,13 @@ from __future__ import annotations
from datetime import timedelta
import logging
from typing import cast
from aioacaia.acaiascale import AcaiaScale
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError
from bleak import BleakScanner
from homeassistant.components.bluetooth import async_get_scanner
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS
from homeassistant.core import HomeAssistant
@@ -42,6 +45,7 @@ class AcaiaCoordinator(DataUpdateCoordinator[None]):
name=entry.title,
is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE],
notify_callback=self.async_update_listeners,
scanner=cast(BleakScanner, async_get_scanner(hass)),
)
@property
+1 -1
View File
@@ -26,5 +26,5 @@
"iot_class": "local_push",
"loggers": ["aioacaia"],
"quality_scale": "platinum",
"requirements": ["aioacaia==0.1.14"]
"requirements": ["aioacaia==0.1.17"]
}
@@ -3,6 +3,7 @@
from __future__ import annotations
from asyncio import timeout
from collections.abc import Mapping
from typing import Any
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
@@ -22,6 +23,8 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for AccuWeather."""
VERSION = 1
_latitude: float | None = None
_longitude: float | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -74,3 +77,46 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
),
errors=errors,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
self._latitude = entry_data[CONF_LATITUDE]
self._longitude = entry_data[CONF_LONGITUDE]
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
errors: dict[str, str] = {}
if user_input is not None:
websession = async_get_clientsession(self.hass)
try:
async with timeout(10):
accuweather = AccuWeather(
user_input[CONF_API_KEY],
websession,
latitude=self._latitude,
longitude=self._longitude,
)
await accuweather.async_get_location()
except (ApiError, ClientConnectorError, TimeoutError, ClientError):
errors["base"] = "cannot_connect"
except InvalidApiKeyError:
errors["base"] = "invalid_api_key"
except RequestsExceededError:
errors["base"] = "requests_exceeded"
else:
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data_updates=user_input
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}),
errors=errors,
)
@@ -15,6 +15,7 @@ from aiohttp.client_exceptions import ClientConnectorError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator,
@@ -30,7 +31,7 @@ from .const import (
UPDATE_INTERVAL_OBSERVATION,
)
EXCEPTIONS = (ApiError, ClientConnectorError, InvalidApiKeyError, RequestsExceededError)
EXCEPTIONS = (ApiError, ClientConnectorError, RequestsExceededError)
_LOGGER = logging.getLogger(__name__)
@@ -52,6 +53,8 @@ class AccuWeatherObservationDataUpdateCoordinator(
):
"""Class to manage fetching AccuWeather data API."""
config_entry: AccuWeatherConfigEntry
def __init__(
self,
hass: HomeAssistant,
@@ -87,6 +90,12 @@ class AccuWeatherObservationDataUpdateCoordinator(
translation_key="current_conditions_update_error",
translation_placeholders={"error": repr(error)},
) from error
except InvalidApiKeyError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="auth_error",
translation_placeholders={"entry": self.config_entry.title},
) from err
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
@@ -98,6 +107,8 @@ class AccuWeatherForecastDataUpdateCoordinator(
):
"""Base class for AccuWeather forecast."""
config_entry: AccuWeatherConfigEntry
def __init__(
self,
hass: HomeAssistant,
@@ -137,6 +148,12 @@ class AccuWeatherForecastDataUpdateCoordinator(
translation_key="forecast_update_error",
translation_placeholders={"error": repr(error)},
) from error
except InvalidApiKeyError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="auth_error",
translation_placeholders={"entry": self.config_entry.title},
) from err
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
return result
@@ -7,6 +7,17 @@
"api_key": "[%key:common::config_flow::data::api_key%]",
"latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]"
},
"data_description": {
"api_key": "API key generated in the AccuWeather APIs portal."
}
},
"reauth_confirm": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "[%key:component::accuweather::config::step::user::data_description::api_key%]"
}
}
},
@@ -19,7 +30,8 @@
"requests_exceeded": "The allowed number of requests to the AccuWeather API has been exceeded. You have to wait or change the API key."
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"entity": {
@@ -239,6 +251,9 @@
}
},
"exceptions": {
"auth_error": {
"message": "Authentication failed for {entry}, please update your API key"
},
"current_conditions_update_error": {
"message": "An error occurred while retrieving weather current conditions data from the AccuWeather API: {error}"
},
@@ -2,21 +2,31 @@
from __future__ import annotations
from pathlib import Path
from homeassistant.components.media_source import MediaSource, local_source
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .const import DATA_MEDIA_SOURCE, DOMAIN, IMAGE_DIR
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
"""Set up local media source."""
media_dir = hass.config.path(f"{DOMAIN}/{IMAGE_DIR}")
media_dirs = list(hass.config.media_dirs.values())
if not media_dirs:
raise HomeAssistantError(
"AI Task media source requires at least one media directory configured"
)
media_dir = Path(media_dirs[0]) / DOMAIN / IMAGE_DIR
hass.data[DATA_MEDIA_SOURCE] = source = local_source.LocalSource(
hass,
DOMAIN,
"AI Generated Images",
{IMAGE_DIR: media_dir},
{IMAGE_DIR: str(media_dir)},
f"/{DOMAIN}",
)
return source
+17 -12
View File
@@ -12,7 +12,7 @@ from typing import Any
import voluptuous as vol
from homeassistant.components import camera, conversation, media_source
from homeassistant.components import camera, conversation, image, media_source
from homeassistant.components.http.auth import async_sign_path
from homeassistant.core import HomeAssistant, ServiceResponse, callback
from homeassistant.exceptions import HomeAssistantError
@@ -31,14 +31,14 @@ from .const import (
)
def _save_camera_snapshot(image: camera.Image) -> Path:
def _save_camera_snapshot(image_data: camera.Image | image.Image) -> Path:
"""Save camera snapshot to temp file."""
with tempfile.NamedTemporaryFile(
mode="wb",
suffix=mimetypes.guess_extension(image.content_type, False),
suffix=mimetypes.guess_extension(image_data.content_type, False),
delete=False,
) as temp_file:
temp_file.write(image.content)
temp_file.write(image_data.content)
return Path(temp_file.name)
@@ -54,26 +54,31 @@ async def _resolve_attachments(
for attachment in attachments or []:
media_content_id = attachment["media_content_id"]
# Special case for camera media sources
if media_content_id.startswith("media-source://camera/"):
# Extract entity_id from the media content ID
entity_id = media_content_id.removeprefix("media-source://camera/")
# Special case for certain media sources
for integration in camera, image:
media_source_prefix = f"media-source://{integration.DOMAIN}/"
if not media_content_id.startswith(media_source_prefix):
continue
# Get snapshot from camera
image = await camera.async_get_image(hass, entity_id)
# Extract entity_id from the media content ID
entity_id = media_content_id.removeprefix(media_source_prefix)
# Get snapshot from entity
image_data = await integration.async_get_image(hass, entity_id)
temp_filename = await hass.async_add_executor_job(
_save_camera_snapshot, image
_save_camera_snapshot, image_data
)
created_files.append(temp_filename)
resolved_attachments.append(
conversation.Attachment(
media_content_id=media_content_id,
mime_type=image.content_type,
mime_type=image_data.content_type,
path=temp_filename,
)
)
break
else:
# Handle regular media sources
media = await media_source.async_resolve_media(hass, media_content_id, None)
+1 -1
View File
@@ -41,7 +41,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE:
return []
call_ids = await async_extract_entity_ids(hass, call)
call_ids = await async_extract_entity_ids(call)
entity_ids = []
for entity_id in hass.data[DATA_AMCREST][CAMERAS]:
if entity_id not in call_ids:
+16 -1
View File
@@ -12,10 +12,25 @@ from homeassistant.helpers.event import async_call_later, async_track_time_inter
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
from .analytics import Analytics
from .analytics import (
Analytics,
AnalyticsInput,
AnalyticsModifications,
DeviceAnalyticsModifications,
EntityAnalyticsModifications,
async_devices_payload,
)
from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA
from .http import AnalyticsDevicesView
__all__ = [
"AnalyticsInput",
"AnalyticsModifications",
"DeviceAnalyticsModifications",
"EntityAnalyticsModifications",
"async_devices_payload",
]
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
DATA_COMPONENT: HassKey[Analytics] = HassKey(DOMAIN)
+255 -53
View File
@@ -4,9 +4,10 @@ from __future__ import annotations
import asyncio
from asyncio import timeout
from dataclasses import asdict as dataclass_asdict, dataclass
from collections.abc import Awaitable, Callable, Iterable, Mapping
from dataclasses import asdict as dataclass_asdict, dataclass, field
from datetime import datetime
from typing import Any
from typing import Any, Protocol
import uuid
import aiohttp
@@ -35,11 +36,14 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.storage import Store
from homeassistant.helpers.system_info import async_get_system_info
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from homeassistant.loader import (
Integration,
IntegrationNotFound,
async_get_integration,
async_get_integrations,
)
from homeassistant.setup import async_get_loaded_integrations
@@ -75,12 +79,116 @@ from .const import (
ATTR_USER_COUNT,
ATTR_UUID,
ATTR_VERSION,
DOMAIN,
LOGGER,
PREFERENCE_SCHEMA,
STORAGE_KEY,
STORAGE_VERSION,
)
DATA_ANALYTICS_MODIFIERS = "analytics_modifiers"
type AnalyticsModifier = Callable[
[HomeAssistant, AnalyticsInput], Awaitable[AnalyticsModifications]
]
@singleton(DATA_ANALYTICS_MODIFIERS)
def _async_get_modifiers(
hass: HomeAssistant,
) -> dict[str, AnalyticsModifier | None]:
"""Return the analytics modifiers."""
return {}
@dataclass
class AnalyticsInput:
"""Analytics input for a single integration.
This is sent to integrations that implement the platform.
"""
device_ids: Iterable[str] = field(default_factory=list)
entity_ids: Iterable[str] = field(default_factory=list)
@dataclass
class AnalyticsModifications:
"""Analytics config for a single integration.
This is used by integrations that implement the platform.
"""
remove: bool = False
devices: Mapping[str, DeviceAnalyticsModifications] | None = None
entities: Mapping[str, EntityAnalyticsModifications] | None = None
@dataclass
class DeviceAnalyticsModifications:
"""Analytics config for a single device.
This is used by integrations that implement the platform.
"""
remove: bool = False
@dataclass
class EntityAnalyticsModifications:
"""Analytics config for a single entity.
This is used by integrations that implement the platform.
"""
remove: bool = False
capabilities: dict[str, Any] | None | UndefinedType = UNDEFINED
class AnalyticsPlatformProtocol(Protocol):
"""Define the format of analytics platforms."""
async def async_modify_analytics(
self,
hass: HomeAssistant,
analytics_input: AnalyticsInput,
) -> AnalyticsModifications:
"""Modify the analytics."""
async def _async_get_analytics_platform(
hass: HomeAssistant, domain: str
) -> AnalyticsPlatformProtocol | None:
"""Get analytics platform."""
try:
integration = await async_get_integration(hass, domain)
except IntegrationNotFound:
return None
try:
return await integration.async_get_platform(DOMAIN)
except ImportError:
return None
async def _async_get_modifier(
hass: HomeAssistant, domain: str
) -> AnalyticsModifier | None:
"""Get analytics modifier."""
modifiers = _async_get_modifiers(hass)
modifier = modifiers.get(domain, UNDEFINED)
if modifier is not UNDEFINED:
return modifier
platform = await _async_get_analytics_platform(hass, domain)
if platform is None:
modifiers[domain] = None
return None
modifier = getattr(platform, "async_modify_analytics", None)
modifiers[domain] = modifier
return modifier
def gen_uuid() -> str:
"""Generate a new UUID."""
@@ -393,17 +501,20 @@ def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]:
return domains
async def async_devices_payload(hass: HomeAssistant) -> dict:
DEFAULT_ANALYTICS_CONFIG = AnalyticsModifications()
DEFAULT_DEVICE_ANALYTICS_CONFIG = DeviceAnalyticsModifications()
DEFAULT_ENTITY_ANALYTICS_CONFIG = EntityAnalyticsModifications()
async def async_devices_payload(hass: HomeAssistant) -> dict: # noqa: C901
"""Return detailed information about entities and devices."""
integrations_info: dict[str, dict[str, Any]] = {}
dev_reg = dr.async_get(hass)
ent_reg = er.async_get(hass)
# We need to refer to other devices, for example in `via_device` field.
# We don't however send the original device ids outside of Home Assistant,
# instead we refer to devices by (integration_domain, index_in_integration_device_list).
device_id_mapping: dict[str, tuple[str, int]] = {}
integration_inputs: dict[str, tuple[list[str], list[str]]] = {}
integration_configs: dict[str, AnalyticsModifications] = {}
# Get device list
for device_entry in dev_reg.devices.values():
if not device_entry.primary_config_entry:
continue
@@ -416,27 +527,96 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
continue
integration_domain = config_entry.domain
integration_input = integration_inputs.setdefault(integration_domain, ([], []))
integration_input[0].append(device_entry.id)
# Get entity list
for entity_entry in ent_reg.entities.values():
integration_domain = entity_entry.platform
integration_input = integration_inputs.setdefault(integration_domain, ([], []))
integration_input[1].append(entity_entry.entity_id)
# Call integrations that implement the analytics platform
for integration_domain, integration_input in integration_inputs.items():
if (
modifier := await _async_get_modifier(hass, integration_domain)
) is not None:
try:
integration_config = await modifier(
hass, AnalyticsInput(*integration_input)
)
except Exception as err: # noqa: BLE001
LOGGER.exception(
"Calling async_modify_analytics for integration '%s' failed: %s",
integration_domain,
err,
)
integration_configs[integration_domain] = AnalyticsModifications(
remove=True
)
continue
if not isinstance(integration_config, AnalyticsModifications):
LOGGER.error( # type: ignore[unreachable]
"Calling async_modify_analytics for integration '%s' did not return an AnalyticsConfig",
integration_domain,
)
integration_configs[integration_domain] = AnalyticsModifications(
remove=True
)
continue
integration_configs[integration_domain] = integration_config
integrations_info: dict[str, dict[str, Any]] = {}
# We need to refer to other devices, for example in `via_device` field.
# We don't however send the original device ids outside of Home Assistant,
# instead we refer to devices by (integration_domain, index_in_integration_device_list).
device_id_mapping: dict[str, tuple[str, int]] = {}
# Fill out information about devices
for integration_domain, integration_input in integration_inputs.items():
integration_config = integration_configs.get(
integration_domain, DEFAULT_ANALYTICS_CONFIG
)
if integration_config.remove:
continue
integration_info = integrations_info.setdefault(
integration_domain, {"devices": [], "entities": []}
)
devices_info = integration_info["devices"]
device_id_mapping[device_entry.id] = (integration_domain, len(devices_info))
for device_id in integration_input[0]:
device_config = DEFAULT_DEVICE_ANALYTICS_CONFIG
if integration_config.devices is not None:
device_config = integration_config.devices.get(device_id, device_config)
devices_info.append(
{
"entities": [],
"entry_type": device_entry.entry_type,
"has_configuration_url": device_entry.configuration_url is not None,
"hw_version": device_entry.hw_version,
"manufacturer": device_entry.manufacturer,
"model": device_entry.model,
"model_id": device_entry.model_id,
"sw_version": device_entry.sw_version,
"via_device": device_entry.via_device_id,
}
)
if device_config.remove:
continue
device_entry = dev_reg.devices[device_id]
device_id_mapping[device_entry.id] = (integration_domain, len(devices_info))
devices_info.append(
{
"entities": [],
"entry_type": device_entry.entry_type,
"has_configuration_url": device_entry.configuration_url is not None,
"hw_version": device_entry.hw_version,
"manufacturer": device_entry.manufacturer,
"model": device_entry.model,
"model_id": device_entry.model_id,
"sw_version": device_entry.sw_version,
"via_device": device_entry.via_device_id,
}
)
# Fill out via_device with new device ids
for integration_info in integrations_info.values():
@@ -445,10 +625,15 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
continue
device_info["via_device"] = device_id_mapping.get(device_info["via_device"])
ent_reg = er.async_get(hass)
# Fill out information about entities
for integration_domain, integration_input in integration_inputs.items():
integration_config = integration_configs.get(
integration_domain, DEFAULT_ANALYTICS_CONFIG
)
if integration_config.remove:
continue
for entity_entry in ent_reg.entities.values():
integration_domain = entity_entry.platform
integration_info = integrations_info.setdefault(
integration_domain, {"devices": [], "entities": []}
)
@@ -456,35 +641,52 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
devices_info = integration_info["devices"]
entities_info = integration_info["entities"]
entity_state = hass.states.get(entity_entry.entity_id)
for entity_id in integration_input[1]:
entity_config = DEFAULT_ENTITY_ANALYTICS_CONFIG
if integration_config.entities is not None:
entity_config = integration_config.entities.get(
entity_id, entity_config
)
entity_info = {
# LIMITATION: `assumed_state` can be overridden by users;
# we should replace it with the original value in the future.
# It is also not present, if entity is not in the state machine,
# which can happen for disabled entities.
"assumed_state": entity_state.attributes.get(ATTR_ASSUMED_STATE, False)
if entity_state is not None
else None,
"capabilities": entity_entry.capabilities,
"domain": entity_entry.domain,
"entity_category": entity_entry.entity_category,
"has_entity_name": entity_entry.has_entity_name,
"original_device_class": entity_entry.original_device_class,
# LIMITATION: `unit_of_measurement` can be overridden by users;
# we should replace it with the original value in the future.
"unit_of_measurement": entity_entry.unit_of_measurement,
}
if entity_config.remove:
continue
if (
((device_id := entity_entry.device_id) is not None)
and ((new_device_id := device_id_mapping.get(device_id)) is not None)
and (new_device_id[0] == integration_domain)
):
device_info = devices_info[new_device_id[1]]
device_info["entities"].append(entity_info)
else:
entities_info.append(entity_info)
entity_entry = ent_reg.entities[entity_id]
entity_state = hass.states.get(entity_entry.entity_id)
entity_info = {
# LIMITATION: `assumed_state` can be overridden by users;
# we should replace it with the original value in the future.
# It is also not present, if entity is not in the state machine,
# which can happen for disabled entities.
"assumed_state": entity_state.attributes.get(ATTR_ASSUMED_STATE, False)
if entity_state is not None
else None,
"capabilities": entity_config.capabilities
if entity_config.capabilities is not UNDEFINED
else entity_entry.capabilities,
"domain": entity_entry.domain,
"entity_category": entity_entry.entity_category,
"has_entity_name": entity_entry.has_entity_name,
"modified_by_integration": ["capabilities"]
if entity_config.capabilities is not UNDEFINED
else None,
"original_device_class": entity_entry.original_device_class,
# LIMITATION: `unit_of_measurement` can be overridden by users;
# we should replace it with the original value in the future.
"unit_of_measurement": entity_entry.unit_of_measurement,
}
if (
((device_id_ := entity_entry.device_id) is not None)
and ((new_device_id := device_id_mapping.get(device_id_)) is not None)
and (new_device_id[0] == integration_domain)
):
device_info = devices_info[new_device_id[1]]
device_info["entities"].append(entity_info)
else:
entities_info.append(entity_info)
integrations = {
domain: integration
@@ -2,7 +2,7 @@
"domain": "analytics",
"name": "Analytics",
"after_dependencies": ["energy", "hassio", "recorder"],
"codeowners": ["@home-assistant/core", "@ludeeus"],
"codeowners": ["@home-assistant/core"],
"dependencies": ["api", "websocket_api", "http"],
"documentation": "https://www.home-assistant.io/integrations/analytics",
"integration_type": "system",
+4 -1
View File
@@ -467,7 +467,10 @@ async def async_setup_entry(
# periodical (or manual) self test since last daemon restart. It might not be available
# when we set up the integration, and we do not know if it would ever be available. Here we
# add it anyway and mark it as unknown initially.
for resource in available_resources | {LAST_S_TEST}:
#
# We also sort the resources to ensure the order of entities created is deterministic since
# "APCMODEL" and "MODEL" resources map to the same "Model" name.
for resource in sorted(available_resources | {LAST_S_TEST}):
if resource not in SENSORS:
_LOGGER.warning("Invalid resource from APCUPSd: %s", resource.upper())
continue
@@ -109,7 +109,7 @@ class AssistPipelineSelect(SelectEntity, restore_state.RestoreEntity):
)
state = await self.async_get_last_state()
if state is not None and state.state in self.options:
if (state is not None) and (state.state in self.options):
self._attr_current_option = state.state
if self.registry_entry and (device_id := self.registry_entry.device_id):
@@ -119,7 +119,7 @@ class AssistPipelineSelect(SelectEntity, restore_state.RestoreEntity):
def cleanup() -> None:
"""Clean up registered device."""
pipeline_data.pipeline_devices.pop(device_id)
pipeline_data.pipeline_devices.pop(device_id, None)
self.async_on_remove(cleanup)
@@ -120,6 +120,7 @@ class AsusWrtBridge(ABC):
def __init__(self, host: str) -> None:
"""Initialize Bridge."""
self._configuration_url = f"http://{host}"
self._host = host
self._firmware: str | None = None
self._label_mac: str | None = None
@@ -127,6 +128,11 @@ class AsusWrtBridge(ABC):
self._model_id: str | None = None
self._serial_number: str | None = None
@property
def configuration_url(self) -> str:
"""Return configuration URL."""
return self._configuration_url
@property
def host(self) -> str:
"""Return hostname."""
@@ -371,6 +377,7 @@ class AsusWrtHttpBridge(AsusWrtBridge):
# get main router properties
if mac := _identity.mac:
self._label_mac = format_mac(mac)
self._configuration_url = self._api.webpanel
self._firmware = str(_identity.firmware)
self._model = _identity.model
self._model_id = _identity.product_id
+1 -1
View File
@@ -388,13 +388,13 @@ class AsusWrtRouter:
def device_info(self) -> DeviceInfo:
"""Return the device information."""
info = DeviceInfo(
configuration_url=self._api.configuration_url,
identifiers={(DOMAIN, self._entry.unique_id or "AsusWRT")},
name=self.host,
model=self._api.model or "Asus Router",
model_id=self._api.model_id,
serial_number=self._api.serial_number,
manufacturer="Asus",
configuration_url=f"http://{self.host}",
)
if self._api.firmware:
info["sw_version"] = self._api.firmware
+13 -19
View File
@@ -2,13 +2,12 @@
from __future__ import annotations
from collections.abc import Callable, Coroutine
import logging
from typing import Any
from aiohttp import ClientResponseError
from yalexs.activity import ActivityType, ActivityTypes
from yalexs.lock import Lock, LockStatus
from yalexs.activity import ActivityType
from yalexs.lock import Lock, LockOperation, LockStatus
from yalexs.util import get_latest_activity, update_lock_detail_from_activity
from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity, LockEntityFeature
@@ -50,30 +49,25 @@ class AugustLock(AugustEntity, RestoreEntity, LockEntity):
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the device."""
if self._data.push_updates_connected:
await self._data.async_lock_async(self._device_id, self._hyper_bridge)
return
await self._call_lock_operation(self._data.async_lock)
await self._perform_lock_operation(LockOperation.LOCK)
async def async_open(self, **kwargs: Any) -> None:
"""Open/unlatch the device."""
if self._data.push_updates_connected:
await self._data.async_unlatch_async(self._device_id, self._hyper_bridge)
return
await self._call_lock_operation(self._data.async_unlatch)
await self._perform_lock_operation(LockOperation.OPEN)
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the device."""
if self._data.push_updates_connected:
await self._data.async_unlock_async(self._device_id, self._hyper_bridge)
return
await self._call_lock_operation(self._data.async_unlock)
await self._perform_lock_operation(LockOperation.UNLOCK)
async def _call_lock_operation(
self, lock_operation: Callable[[str], Coroutine[Any, Any, list[ActivityTypes]]]
) -> None:
async def _perform_lock_operation(self, operation: LockOperation) -> None:
"""Perform a lock operation."""
try:
activities = await lock_operation(self._device_id)
activities = await self._data.async_operate_lock(
self._device_id,
operation,
self._data.push_updates_connected,
self._hyper_bridge,
)
except ClientResponseError as err:
if err.status == LOCK_JAMMED_ERR:
self._detail.lock_status = LockStatus.JAMMED
@@ -29,5 +29,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.0.1", "yalexs-ble==3.1.2"]
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.1.2"]
}
+15 -4
View File
@@ -92,7 +92,11 @@ from homeassistant.components.http.ban import (
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.network import is_cloud_connection
from homeassistant.helpers.network import (
NoURLAvailableError,
get_url,
is_cloud_connection,
)
from homeassistant.util.network import is_local
from . import indieauth
@@ -125,11 +129,18 @@ class WellKnownOAuthInfoView(HomeAssistantView):
async def get(self, request: web.Request) -> web.Response:
"""Return the well known OAuth2 authorization info."""
hass = request.app[KEY_HASS]
# Some applications require absolute urls, so we prefer using the
# current requests url if possible, with fallback to a relative url.
try:
url_prefix = get_url(hass, require_current_request=True)
except NoURLAvailableError:
url_prefix = ""
return self.json(
{
"authorization_endpoint": "/auth/authorize",
"token_endpoint": "/auth/token",
"revocation_endpoint": "/auth/revoke",
"authorization_endpoint": f"{url_prefix}/auth/authorize",
"token_endpoint": f"{url_prefix}/auth/token",
"revocation_endpoint": f"{url_prefix}/auth/revoke",
"response_types_supported": ["code"],
"service_documentation": (
"https://developers.home-assistant.io/docs/auth_api"
@@ -0,0 +1,24 @@
"""Analytics platform."""
from homeassistant.components.analytics import (
AnalyticsInput,
AnalyticsModifications,
EntityAnalyticsModifications,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
async def async_modify_analytics(
hass: HomeAssistant, analytics_input: AnalyticsInput
) -> AnalyticsModifications:
"""Modify the analytics."""
ent_reg = er.async_get(hass)
entities: dict[str, EntityAnalyticsModifications] = {}
for entity_id in analytics_input.entity_ids:
entity_entry = ent_reg.entities[entity_id]
if entity_entry.capabilities is not None:
entities[entity_id] = EntityAnalyticsModifications(capabilities=None)
return AnalyticsModifications(entities=entities)
-1
View File
@@ -26,7 +26,6 @@ EXCLUDE_FROM_BACKUP = [
"tmp_backups/*.tar",
"OZW_Log.txt",
"tts/*",
"ai_task/*",
]
EXCLUDE_DATABASE_FROM_BACKUP = [
+3 -2
View File
@@ -8,7 +8,7 @@ import threading
from typing import IO, cast
from aiohttp import BodyPartReader
from aiohttp.hdrs import CONTENT_DISPOSITION
from aiohttp.hdrs import CONTENT_DISPOSITION, CONTENT_TYPE
from aiohttp.web import FileResponse, Request, Response, StreamResponse
from multidict import istr
@@ -76,7 +76,8 @@ class DownloadBackupView(HomeAssistantView):
return Response(status=HTTPStatus.NOT_FOUND)
headers = {
CONTENT_DISPOSITION: f"attachment; filename={slugify(backup.name)}.tar"
CONTENT_DISPOSITION: f"attachment; filename={slugify(backup.name)}.tar",
CONTENT_TYPE: "application/x-tar",
}
try:
+3 -3
View File
@@ -14,15 +14,15 @@
},
"automatic_backup_failed_addons": {
"title": "Not all add-ons could be included in automatic backup",
"description": "Add-ons {failed_addons} could not be included in automatic backup. Please check the supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured."
"description": "Add-ons {failed_addons} could not be included in automatic backup. Please check the Supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured."
},
"automatic_backup_failed_agents_addons_folders": {
"title": "Automatic backup was created with errors",
"description": "The automatic backup was created with errors:\n* Locations which the backup could not be uploaded to: {failed_agents}\n* Add-ons which could not be backed up: {failed_addons}\n* Folders which could not be backed up: {failed_folders}\n\nPlease check the core and supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured."
"description": "The automatic backup was created with errors:\n* Locations which the backup could not be uploaded to: {failed_agents}\n* Add-ons which could not be backed up: {failed_addons}\n* Folders which could not be backed up: {failed_folders}\n\nPlease check the Core and Supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured."
},
"automatic_backup_failed_folders": {
"title": "Not all folders could be included in automatic backup",
"description": "Folders {failed_folders} could not be included in automatic backup. Please check the supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured."
"description": "Folders {failed_folders} could not be included in automatic backup. Please check the Supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured."
}
},
"services": {
@@ -13,20 +13,30 @@ from bluecurrent_api.exceptions import (
RequestLimitReached,
WebsocketError,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_API_TOKEN, CONF_DEVICE_ID, Platform
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
ServiceValidationError,
)
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
from .const import (
BCU_APP,
CHARGEPOINT_SETTINGS,
CHARGEPOINT_STATUS,
CHARGING_CARD_ID,
DOMAIN,
EVSE_ID,
LOGGER,
PLUG_AND_CHARGE,
SERVICE_START_CHARGE_SESSION,
VALUE,
)
@@ -34,6 +44,7 @@ type BlueCurrentConfigEntry = ConfigEntry[Connector]
PLATFORMS = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH]
CHARGE_POINTS = "CHARGE_POINTS"
CHARGE_CARDS = "CHARGE_CARDS"
DATA = "data"
DELAY = 5
@@ -41,6 +52,16 @@ GRID = "GRID"
OBJECT = "object"
VALUE_TYPES = [CHARGEPOINT_STATUS, CHARGEPOINT_SETTINGS]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
SERVICE_START_CHARGE_SESSION_SCHEMA = vol.Schema(
{
vol.Required(CONF_DEVICE_ID): cv.string,
# When no charging card is provided, use no charging card (BCU_APP = no charging card).
vol.Optional(CHARGING_CARD_ID, default=BCU_APP): cv.string,
}
)
async def async_setup_entry(
hass: HomeAssistant, config_entry: BlueCurrentConfigEntry
@@ -67,6 +88,66 @@ async def async_setup_entry(
return True
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Blue Current."""
async def start_charge_session(service_call: ServiceCall) -> None:
"""Start a charge session with the provided device and charge card ID."""
# When no charge card is provided, use the default charge card set in the config flow.
charging_card_id = service_call.data[CHARGING_CARD_ID]
device_id = service_call.data[CONF_DEVICE_ID]
# Get the device based on the given device ID.
device = dr.async_get(hass).devices.get(device_id)
if device is None:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="invalid_device_id"
)
blue_current_config_entry: ConfigEntry | None = None
for config_entry_id in device.config_entries:
config_entry = hass.config_entries.async_get_entry(config_entry_id)
if not config_entry or config_entry.domain != DOMAIN:
# Not the blue_current config entry.
continue
if config_entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="config_entry_not_loaded"
)
blue_current_config_entry = config_entry
break
if not blue_current_config_entry:
# The device is not connected to a valid blue_current config entry.
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="no_config_entry"
)
connector = blue_current_config_entry.runtime_data
# Get the evse_id from the identifier of the device.
evse_id = next(
identifier[1]
for identifier in device.identifiers
if identifier[0] == DOMAIN
)
await connector.client.start_session(evse_id, charging_card_id)
hass.services.async_register(
DOMAIN,
SERVICE_START_CHARGE_SESSION,
start_charge_session,
SERVICE_START_CHARGE_SESSION_SCHEMA,
)
return True
async def async_unload_entry(
hass: HomeAssistant, config_entry: BlueCurrentConfigEntry
) -> bool:
@@ -87,6 +168,7 @@ class Connector:
self.client = client
self.charge_points: dict[str, dict] = {}
self.grid: dict[str, Any] = {}
self.charge_cards: dict[str, dict[str, Any]] = {}
async def on_data(self, message: dict) -> None:
"""Handle received data."""
@@ -8,6 +8,12 @@ LOGGER = logging.getLogger(__package__)
EVSE_ID = "evse_id"
MODEL_TYPE = "model_type"
CARD = "card"
UID = "uid"
BCU_APP = "BCU-APP"
WITHOUT_CHARGING_CARD = "without_charging_card"
CHARGING_CARD_ID = "charging_card_id"
SERVICE_START_CHARGE_SESSION = "start_charge_session"
PLUG_AND_CHARGE = "plug_and_charge"
VALUE = "value"
PERMISSION = "permission"
@@ -42,5 +42,10 @@
"default": "mdi:lock"
}
}
},
"services": {
"start_charge_session": {
"service": "mdi:play"
}
}
}
@@ -0,0 +1,12 @@
start_charge_session:
fields:
device_id:
selector:
device:
integration: blue_current
required: true
charging_card_id:
selector:
text:
required: false
@@ -22,6 +22,16 @@
"wrong_account": "Wrong account: Please authenticate with the API token for {email}."
}
},
"options": {
"step": {
"init": {
"data": {
"card": "Card"
},
"description": "Select the default charging card you want to use"
}
}
},
"entity": {
"sensor": {
"activity": {
@@ -136,5 +146,39 @@
"name": "Block charge point"
}
}
},
"selector": {
"select_charging_card": {
"options": {
"without_charging_card": "Without charging card"
}
}
},
"services": {
"start_charge_session": {
"name": "Start charge session",
"description": "Starts a new charge session on a specified charge point.",
"fields": {
"charging_card_id": {
"name": "Charging card ID",
"description": "Optional charging card ID that will be used to start a charge session. When not provided, no charging card will be used."
},
"device_id": {
"name": "Device ID",
"description": "The ID of the Blue Current charge point."
}
}
}
},
"exceptions": {
"invalid_device_id": {
"message": "Invalid device ID given."
},
"config_entry_not_loaded": {
"message": "Config entry not loaded."
},
"no_config_entry": {
"message": "Device has not a valid blue_current config entry."
}
}
}
+7 -3
View File
@@ -10,6 +10,7 @@ from asyncio import Future
from collections.abc import Callable, Iterable
from typing import TYPE_CHECKING, cast
from bleak import BleakScanner
from habluetooth import (
BaseHaScanner,
BluetoothScannerDevice,
@@ -38,13 +39,16 @@ def _get_manager(hass: HomeAssistant) -> HomeAssistantBluetoothManager:
@hass_callback
def async_get_scanner(hass: HomeAssistant) -> HaBleakScannerWrapper:
"""Return a HaBleakScannerWrapper.
def async_get_scanner(hass: HomeAssistant) -> BleakScanner:
"""Return a HaBleakScannerWrapper cast to BleakScanner.
This is a wrapper around our BleakScanner singleton that allows
multiple integrations to share the same BleakScanner.
The wrapper is cast to BleakScanner for type compatibility with
libraries expecting a BleakScanner instance.
"""
return HaBleakScannerWrapper()
return cast(BleakScanner, HaBleakScannerWrapper())
@hass_callback
@@ -205,6 +205,7 @@ class BringActivityCoordinator(BringBaseCoordinator[dict[str, BringActivityData]
async def _async_update_data(self) -> dict[str, BringActivityData]:
"""Fetch activity data from bring."""
self.lists = self.coordinator.lists
list_dict: dict[str, BringActivityData] = {}
for lst in self.lists:
+3 -2
View File
@@ -43,7 +43,7 @@ async def async_setup_entry(
)
lists_added |= new_lists
coordinator.activity.async_add_listener(add_entities)
coordinator.data.async_add_listener(add_entities)
add_entities()
@@ -67,7 +67,8 @@ class BringEventEntity(BringBaseEntity, EventEntity):
def _async_handle_event(self) -> None:
"""Handle the activity event."""
bring_list = self.coordinator.data[self._list_uuid]
if (bring_list := self.coordinator.data.get(self._list_uuid)) is None:
return
last_event_triggered = self.state
if bring_list.activity.timeline and (
last_event_triggered is None
@@ -20,5 +20,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bthome",
"iot_class": "local_push",
"requirements": ["bthome-ble==3.13.1"]
"requirements": ["bthome-ble==3.14.2"]
}
+10
View File
@@ -25,6 +25,7 @@ from homeassistant.const import (
DEGREE,
LIGHT_LUX,
PERCENTAGE,
REVOLUTIONS_PER_MINUTE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
UnitOfConductivity,
@@ -269,6 +270,15 @@ SENSOR_DESCRIPTIONS = {
native_unit_of_measurement=DEGREE,
state_class=SensorStateClass.MEASUREMENT,
),
# Rotational speed (rpm)
(
BTHomeExtendedSensorDeviceClass.ROTATIONAL_SPEED,
Units.REVOLUTIONS_PER_MINUTE,
): SensorEntityDescription(
key=f"{BTHomeExtendedSensorDeviceClass.ROTATIONAL_SPEED}_{Units.REVOLUTIONS_PER_MINUTE}",
native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
state_class=SensorStateClass.MEASUREMENT,
),
# Signal Strength (RSSI) (dB)
(
BTHomeSensorDeviceClass.SIGNAL_STRENGTH,
+109
View File
@@ -37,6 +37,10 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.loader import (
async_get_custom_components,
async_get_loaded_integration,
)
from homeassistant.util.location import async_detect_location_info
from .alexa_config import entity_supported as entity_supported_by_alexa
@@ -431,6 +435,79 @@ class DownloadSupportPackageView(HomeAssistantView):
url = "/api/cloud/support_package"
name = "api:cloud:support_package"
async def _get_integration_info(self, hass: HomeAssistant) -> dict[str, Any]:
"""Collect information about active and custom integrations."""
# Get loaded components from hass.config.components
loaded_components = hass.config.components.copy()
# Get custom integrations
custom_domains = set()
with suppress(Exception):
custom_domains = set(await async_get_custom_components(hass))
# Separate built-in and custom integrations
builtin_integrations = []
custom_integrations = []
for domain in sorted(loaded_components):
try:
integration = async_get_loaded_integration(hass, domain)
except Exception: # noqa: BLE001
# Broad exception catch for robustness in support package
# generation. If we can't get integration info,
# just add the domain
if domain in custom_domains:
custom_integrations.append(
{
"domain": domain,
"name": "Unknown",
"version": "Unknown",
"documentation": "Unknown",
}
)
else:
builtin_integrations.append(
{
"domain": domain,
"name": "Unknown",
}
)
else:
if domain in custom_domains:
# This is a custom integration
# include version and documentation link
version = (
str(integration.version) if integration.version else "Unknown"
)
if not (documentation := integration.documentation):
documentation = "Unknown"
custom_integrations.append(
{
"domain": domain,
"name": integration.name,
"version": version,
"documentation": documentation,
}
)
else:
# This is a built-in integration.
# No version needed, as it is always the same as the
# Home Assistant version
builtin_integrations.append(
{
"domain": domain,
"name": integration.name,
}
)
return {
"builtin_count": len(builtin_integrations),
"builtin_integrations": builtin_integrations,
"custom_count": len(custom_integrations),
"custom_integrations": custom_integrations,
}
async def _generate_markdown(
self,
hass: HomeAssistant,
@@ -453,6 +530,38 @@ class DownloadSupportPackageView(HomeAssistantView):
markdown = "## System Information\n\n"
markdown += get_domain_table_markdown(hass_info)
# Add integration information
try:
integration_info = await self._get_integration_info(hass)
except Exception: # noqa: BLE001
# Broad exception catch for robustness in support package generation
# If there's any error getting integration info, just note it
markdown += "## Active integrations\n\n"
markdown += "Unable to collect integration information\n\n"
else:
markdown += "## Active Integrations\n\n"
markdown += f"Built-in integrations: {integration_info['builtin_count']}\n"
markdown += f"Custom integrations: {integration_info['custom_count']}\n\n"
# Built-in integrations
if integration_info["builtin_integrations"]:
markdown += "<details><summary>Built-in integrations</summary>\n\n"
markdown += "Domain | Name\n"
markdown += "--- | ---\n"
for integration in integration_info["builtin_integrations"]:
markdown += f"{integration['domain']} | {integration['name']}\n"
markdown += "\n</details>\n\n"
# Custom integrations
if integration_info["custom_integrations"]:
markdown += "<details><summary>Custom integrations</summary>\n\n"
markdown += "Domain | Name | Version | Documentation\n"
markdown += "--- | --- | --- | ---\n"
for integration in integration_info["custom_integrations"]:
doc_url = integration.get("documentation") or "N/A"
markdown += f"{integration['domain']} | {integration['name']} | {integration['version']} | {doc_url}\n"
markdown += "\n</details>\n\n"
for domain, domain_info in domains_info.items():
domain_info_md = get_domain_table_markdown(domain_info)
markdown += (
@@ -25,7 +25,11 @@ async def async_subscription_info(cloud: Cloud[CloudClient]) -> SubscriptionInfo
return await cloud.payments.subscription_info()
except PaymentsApiError as exception:
_LOGGER.error("Failed to fetch subscription information - %s", exception)
except TimeoutError:
_LOGGER.error(
"A timeout of %s was reached while trying to fetch subscription information",
REQUEST_TIMEOUT,
)
return None
@@ -2,7 +2,7 @@
from abc import abstractmethod
from datetime import timedelta
from typing import TypeVar
from typing import Any, TypeVar
from aiocomelit.api import (
AlarmDataObject,
@@ -13,7 +13,16 @@ from aiocomelit.api import (
ComelitVedoAreaObject,
ComelitVedoZoneObject,
)
from aiocomelit.const import BRIDGE, VEDO
from aiocomelit.const import (
BRIDGE,
CLIMATE,
COVER,
IRRIGATION,
LIGHT,
OTHER,
SCENARIO,
VEDO,
)
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
from aiohttp import ClientSession
@@ -111,6 +120,32 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[T]):
async def _async_update_system_data(self) -> T:
"""Class method for updating data."""
async def _async_remove_stale_devices(
self,
previous_list: dict[int, Any],
current_list: dict[int, Any],
dev_type: str,
) -> None:
"""Remove stale devices."""
device_registry = dr.async_get(self.hass)
for i in previous_list:
if i not in current_list:
_LOGGER.debug(
"Detected change in %s devices: index %s removed",
dev_type,
i,
)
identifier = f"{self.config_entry.entry_id}-{dev_type}-{i}"
device = device_registry.async_get_device(
identifiers={(DOMAIN, identifier)}
)
if device:
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
class ComelitSerialBridge(
ComelitBaseCoordinator[dict[str, dict[int, ComelitSerialBridgeObject]]]
@@ -137,7 +172,15 @@ class ComelitSerialBridge(
self,
) -> dict[str, dict[int, ComelitSerialBridgeObject]]:
"""Specific method for updating data."""
return await self.api.get_all_devices()
data = await self.api.get_all_devices()
if self.data:
for dev_type in (CLIMATE, COVER, LIGHT, IRRIGATION, OTHER, SCENARIO):
await self._async_remove_stale_devices(
self.data[dev_type], data[dev_type], dev_type
)
return data
class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
@@ -163,4 +206,14 @@ class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
self,
) -> AlarmDataObject:
"""Specific method for updating data."""
return await self.api.get_all_areas_and_zones()
data = await self.api.get_all_areas_and_zones()
if self.data:
for obj_type in ("alarm_areas", "alarm_zones"):
await self._async_remove_stale_devices(
self.data[obj_type],
data[obj_type],
"area" if obj_type == "alarm_areas" else "zone",
)
return data
@@ -72,9 +72,7 @@ rules:
repair-issues:
status: exempt
comment: no known use cases for repair issues or flows, yet
stale-devices:
status: todo
comment: missing implementation
stale-devices: done
# Platinum
async-dependency: done
@@ -0,0 +1,45 @@
"""The Compit integration."""
from compit_inext_api import CannotConnect, CompitApiConnector, InvalidAuth
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .coordinator import CompitConfigEntry, CompitDataUpdateCoordinator
PLATFORMS = [
Platform.CLIMATE,
]
async def async_setup_entry(hass: HomeAssistant, entry: CompitConfigEntry) -> bool:
"""Set up Compit from a config entry."""
session = async_get_clientsession(hass)
connector = CompitApiConnector(session)
try:
connected = await connector.init(
entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD], hass.config.language
)
except CannotConnect as e:
raise ConfigEntryNotReady(f"Error while connecting to Compit: {e}") from e
except InvalidAuth as e:
raise ConfigEntryAuthFailed(
f"Invalid credentials for {entry.data[CONF_EMAIL]}"
) from e
if not connected:
raise ConfigEntryAuthFailed("Authentication API error")
coordinator = CompitDataUpdateCoordinator(hass, entry, connector)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: CompitConfigEntry) -> bool:
"""Unload an entry for the Compit integration."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+265
View File
@@ -0,0 +1,265 @@
"""Module contains the CompitClimate class for controlling climate entities."""
import logging
from typing import Any
from compit_inext_api import Param, Parameter
from compit_inext_api.consts import (
CompitFanMode,
CompitHVACMode,
CompitParameter,
CompitPresetMode,
)
from propcache.api import cached_property
from homeassistant.components.climate import (
FAN_AUTO,
FAN_HIGH,
FAN_LOW,
FAN_MEDIUM,
FAN_OFF,
PRESET_AWAY,
PRESET_ECO,
PRESET_HOME,
PRESET_NONE,
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER_NAME
from .coordinator import CompitConfigEntry, CompitDataUpdateCoordinator
_LOGGER: logging.Logger = logging.getLogger(__name__)
# Device class for climate devices in Compit system
CLIMATE_DEVICE_CLASS = 10
PARALLEL_UPDATES = 0
COMPIT_MODE_MAP = {
CompitHVACMode.COOL: HVACMode.COOL,
CompitHVACMode.HEAT: HVACMode.HEAT,
CompitHVACMode.OFF: HVACMode.OFF,
}
COMPIT_FANSPEED_MAP = {
CompitFanMode.OFF: FAN_OFF,
CompitFanMode.AUTO: FAN_AUTO,
CompitFanMode.LOW: FAN_LOW,
CompitFanMode.MEDIUM: FAN_MEDIUM,
CompitFanMode.HIGH: FAN_HIGH,
CompitFanMode.HOLIDAY: FAN_AUTO,
}
COMPIT_PRESET_MAP = {
CompitPresetMode.AUTO: PRESET_HOME,
CompitPresetMode.HOLIDAY: PRESET_ECO,
CompitPresetMode.MANUAL: PRESET_NONE,
CompitPresetMode.AWAY: PRESET_AWAY,
}
HVAC_MODE_TO_COMPIT_MODE = {v: k for k, v in COMPIT_MODE_MAP.items()}
FAN_MODE_TO_COMPIT_FAN_MODE = {v: k for k, v in COMPIT_FANSPEED_MAP.items()}
PRESET_MODE_TO_COMPIT_PRESET_MODE = {v: k for k, v in COMPIT_PRESET_MAP.items()}
async def async_setup_entry(
hass: HomeAssistant,
entry: CompitConfigEntry,
async_add_devices: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the CompitClimate platform from a config entry."""
coordinator = entry.runtime_data
climate_entities = []
for device_id in coordinator.connector.all_devices:
device = coordinator.connector.all_devices[device_id]
if device.definition.device_class == CLIMATE_DEVICE_CLASS:
climate_entities.append(
CompitClimate(
coordinator,
device_id,
{
parameter.parameter_code: parameter
for parameter in device.definition.parameters
},
device.definition.name,
)
)
async_add_devices(climate_entities)
class CompitClimate(CoordinatorEntity[CompitDataUpdateCoordinator], ClimateEntity):
"""Representation of a Compit climate device."""
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_hvac_modes = [*COMPIT_MODE_MAP.values()]
_attr_name = None
_attr_has_entity_name = True
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.PRESET_MODE
)
def __init__(
self,
coordinator: CompitDataUpdateCoordinator,
device_id: int,
parameters: dict[str, Parameter],
device_name: str,
) -> None:
"""Initialize the climate device."""
super().__init__(coordinator)
self._attr_unique_id = f"{device_name}_{device_id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, str(device_id))},
name=device_name,
manufacturer=MANUFACTURER_NAME,
model=device_name,
)
self.parameters = parameters
self.device_id = device_id
self.available_presets: Parameter | None = self.parameters.get(
CompitParameter.PRESET_MODE.value
)
self.available_fan_modes: Parameter | None = self.parameters.get(
CompitParameter.FAN_MODE.value
)
@property
def available(self) -> bool:
"""Return if entity is available."""
return (
super().available
and self.device_id in self.coordinator.connector.all_devices
)
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
value = self.get_parameter_value(CompitParameter.CURRENT_TEMPERATURE)
if value is None:
return None
return float(value.value)
@property
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
value = self.get_parameter_value(CompitParameter.SET_TARGET_TEMPERATURE)
if value is None:
return None
return float(value.value)
@cached_property
def preset_modes(self) -> list[str] | None:
"""Return the available preset modes."""
if self.available_presets is None or self.available_presets.details is None:
return []
preset_modes = []
for item in self.available_presets.details:
if item is not None:
ha_preset = COMPIT_PRESET_MAP.get(CompitPresetMode(item.state))
if ha_preset and ha_preset not in preset_modes:
preset_modes.append(ha_preset)
return preset_modes
@cached_property
def fan_modes(self) -> list[str] | None:
"""Return the available fan modes."""
if self.available_fan_modes is None or self.available_fan_modes.details is None:
return []
fan_modes = []
for item in self.available_fan_modes.details:
if item is not None:
ha_fan_mode = COMPIT_FANSPEED_MAP.get(CompitFanMode(item.state))
if ha_fan_mode and ha_fan_mode not in fan_modes:
fan_modes.append(ha_fan_mode)
return fan_modes
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode."""
preset_mode = self.get_parameter_value(CompitParameter.PRESET_MODE)
if preset_mode:
compit_preset_mode = CompitPresetMode(preset_mode.value)
return COMPIT_PRESET_MAP.get(compit_preset_mode)
return None
@property
def fan_mode(self) -> str | None:
"""Return the current fan mode."""
fan_mode = self.get_parameter_value(CompitParameter.FAN_MODE)
if fan_mode:
compit_fan_mode = CompitFanMode(fan_mode.value)
return COMPIT_FANSPEED_MAP.get(compit_fan_mode)
return None
@property
def hvac_mode(self) -> HVACMode | None:
"""Return the current HVAC mode."""
hvac_mode = self.get_parameter_value(CompitParameter.HVAC_MODE)
if hvac_mode:
compit_hvac_mode = CompitHVACMode(hvac_mode.value)
return COMPIT_MODE_MAP.get(compit_hvac_mode)
return None
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
temp = kwargs.get(ATTR_TEMPERATURE)
if temp is None:
raise ServiceValidationError("Temperature argument missing")
await self.set_parameter_value(CompitParameter.SET_TARGET_TEMPERATURE, temp)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target HVAC mode."""
if not (mode := HVAC_MODE_TO_COMPIT_MODE.get(hvac_mode)):
raise ServiceValidationError(f"Invalid hvac mode {hvac_mode}")
await self.set_parameter_value(CompitParameter.HVAC_MODE, mode.value)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new target preset mode."""
compit_preset = PRESET_MODE_TO_COMPIT_PRESET_MODE.get(preset_mode)
if compit_preset is None:
raise ServiceValidationError(f"Invalid preset mode: {preset_mode}")
await self.set_parameter_value(CompitParameter.PRESET_MODE, compit_preset.value)
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode."""
compit_fan_mode = FAN_MODE_TO_COMPIT_FAN_MODE.get(fan_mode)
if compit_fan_mode is None:
raise ServiceValidationError(f"Invalid fan mode: {fan_mode}")
await self.set_parameter_value(CompitParameter.FAN_MODE, compit_fan_mode.value)
async def set_parameter_value(self, parameter: CompitParameter, value: int) -> None:
"""Call the API to set a parameter to a new value."""
await self.coordinator.connector.set_device_parameter(
self.device_id, parameter, value
)
self.async_write_ha_state()
def get_parameter_value(self, parameter: CompitParameter) -> Param | None:
"""Get the parameter value from the device state."""
return self.coordinator.connector.get_device_parameter(
self.device_id, parameter
)
@@ -0,0 +1,110 @@
"""Config flow for Compit integration."""
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
from compit_inext_api import CannotConnect, CompitApiConnector, InvalidAuth
import voluptuous as vol
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_EMAIL): str,
vol.Required(CONF_PASSWORD): str,
}
)
STEP_REAUTH_SCHEMA = vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
}
)
class CompitConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Compit."""
VERSION = 1
async def async_step_user(
self,
user_input: dict[str, Any] | None = None,
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
session = async_create_clientsession(self.hass)
api = CompitApiConnector(session)
success = False
try:
success = await api.init(
user_input[CONF_EMAIL],
user_input[CONF_PASSWORD],
self.hass.config.language,
)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
if not success:
# Api returned unexpected result but no exception
_LOGGER.error("Compit api returned unexpected result")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(user_input[CONF_EMAIL])
if self.source == SOURCE_REAUTH:
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data_updates=user_input
)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_EMAIL], data=user_input
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_reauth(self, data: Mapping[str, Any]) -> ConfigFlowResult:
"""Handle re-auth."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm re-authentication."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
reauth_entry_data = reauth_entry.data
if user_input:
# Reuse async_step_user with combined credentials
return await self.async_step_user(
{
CONF_EMAIL: reauth_entry_data[CONF_EMAIL],
CONF_PASSWORD: user_input[CONF_PASSWORD],
}
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=STEP_REAUTH_SCHEMA,
description_placeholders={CONF_EMAIL: reauth_entry_data[CONF_EMAIL]},
errors=errors,
)
+4
View File
@@ -0,0 +1,4 @@
"""Constants for the Compit integration."""
DOMAIN = "compit"
MANUFACTURER_NAME = "Compit"
@@ -0,0 +1,43 @@
"""Define an object to manage fetching Compit data."""
from datetime import timedelta
import logging
from compit_inext_api import CompitApiConnector, DeviceInstance
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN
SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER: logging.Logger = logging.getLogger(__name__)
type CompitConfigEntry = ConfigEntry[CompitDataUpdateCoordinator]
class CompitDataUpdateCoordinator(DataUpdateCoordinator[dict[int, DeviceInstance]]):
"""Class to manage fetching data from the API."""
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
connector: CompitApiConnector,
) -> None:
"""Initialize."""
self.connector = connector
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
config_entry=config_entry,
)
async def _async_update_data(self) -> dict[int, DeviceInstance]:
"""Update data via library."""
await self.connector.update_state(device_id=None) # Update all devices
return self.connector.all_devices
@@ -0,0 +1,12 @@
{
"domain": "compit",
"name": "Compit",
"codeowners": ["@Przemko92"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/compit",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["compit"],
"quality_scale": "bronze",
"requirements": ["compit-inext-api==0.3.1"]
}
@@ -0,0 +1,86 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
This integration does not provide additional actions.
appropriate-polling: done
brands: done
common-modules:
status: exempt
comment: |
This integration does not use any common modules.
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
This integration does not provide additional actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
Entities of this integration does not explicitly subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: |
This integration does not provide additional actions.
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: |
This integration does not have an options flow.
docs-installation-parameters: done
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: done
reauthentication-flow: done
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: |
This integration is a cloud service and does not support discovery.
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: done
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default:
status: exempt
comment: |
This integration does not have any entities that should disabled by default.
entity-translations: done
exception-translations: todo
icon-translations:
status: exempt
comment: |
There is no need for icon translations.
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
# Platinum
async-dependency: done
inject-websession: todo
strict-typing: done
@@ -0,0 +1,35 @@
{
"config": {
"step": {
"user": {
"description": "Please enter your https://inext.compit.pl/ credentials.",
"title": "Connect to Compit iNext",
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"email": "The email address of your inext.compit.pl account",
"password": "The password of your inext.compit.pl account"
}
},
"reauth_confirm": {
"description": "Please update your password for {email}",
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"password": "[%key:component::compit::config::step::user::data_description::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
}
}
@@ -50,14 +50,13 @@ from .const import (
ATTR_LANGUAGE,
ATTR_TEXT,
DATA_COMPONENT,
DATA_DEFAULT_ENTITY,
DOMAIN,
HOME_ASSISTANT_AGENT,
SERVICE_PROCESS,
SERVICE_RELOAD,
ConversationEntityFeature,
)
from .default_agent import DefaultAgent, async_setup_default_agent
from .default_agent import async_setup_default_agent
from .entity import ConversationEntity
from .http import async_setup as async_setup_conversation_http
from .models import AbstractConversationAgent, ConversationInput, ConversationResult
@@ -142,7 +141,7 @@ def async_unset_agent(
hass: HomeAssistant,
config_entry: ConfigEntry,
) -> None:
"""Set the agent to handle the conversations."""
"""Unset the agent to handle the conversations."""
get_agent_manager(hass).async_unset_agent(config_entry.entry_id)
@@ -241,10 +240,10 @@ async def async_handle_sentence_triggers(
Returns None if no match occurred.
"""
default_agent = async_get_agent(hass)
assert isinstance(default_agent, DefaultAgent)
agent = get_agent_manager(hass).default_agent
assert agent is not None
return await default_agent.async_handle_sentence_triggers(user_input)
return await agent.async_handle_sentence_triggers(user_input)
async def async_handle_intents(
@@ -257,12 +256,10 @@ async def async_handle_intents(
Returns None if no match occurred.
"""
default_agent = async_get_agent(hass)
assert isinstance(default_agent, DefaultAgent)
agent = get_agent_manager(hass).default_agent
assert agent is not None
return await default_agent.async_handle_intents(
user_input, intent_filter=intent_filter
)
return await agent.async_handle_intents(user_input, intent_filter=intent_filter)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@@ -298,9 +295,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def handle_reload(service: ServiceCall) -> None:
"""Reload intents."""
await hass.data[DATA_DEFAULT_ENTITY].async_reload(
language=service.data.get(ATTR_LANGUAGE)
)
agent = get_agent_manager(hass).default_agent
if agent is not None:
await agent.async_reload(language=service.data.get(ATTR_LANGUAGE))
hass.services.async_register(
DOMAIN,
@@ -4,15 +4,21 @@ from __future__ import annotations
import dataclasses
import logging
from typing import Any
from typing import TYPE_CHECKING, Any
import voluptuous as vol
from homeassistant.core import Context, HomeAssistant, async_get_hass, callback
from homeassistant.core import (
CALLBACK_TYPE,
Context,
HomeAssistant,
async_get_hass,
callback,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, intent, singleton
from .const import DATA_COMPONENT, DATA_DEFAULT_ENTITY, HOME_ASSISTANT_AGENT
from .const import DATA_COMPONENT, HOME_ASSISTANT_AGENT
from .entity import ConversationEntity
from .models import (
AbstractConversationAgent,
@@ -28,6 +34,10 @@ from .trace import (
_LOGGER = logging.getLogger(__name__)
if TYPE_CHECKING:
from .default_agent import DefaultAgent
from .trigger import TriggerDetails
@singleton.singleton("conversation_agent")
@callback
@@ -49,8 +59,10 @@ def async_get_agent(
hass: HomeAssistant, agent_id: str | None = None
) -> AbstractConversationAgent | ConversationEntity | None:
"""Get specified agent."""
manager = get_agent_manager(hass)
if agent_id is None or agent_id == HOME_ASSISTANT_AGENT:
return hass.data[DATA_DEFAULT_ENTITY]
return manager.default_agent
if "." in agent_id:
return hass.data[DATA_COMPONENT].get_entity(agent_id)
@@ -134,6 +146,8 @@ class AgentManager:
"""Initialize the conversation agents."""
self.hass = hass
self._agents: dict[str, AbstractConversationAgent] = {}
self.default_agent: DefaultAgent | None = None
self.triggers_details: list[TriggerDetails] = []
@callback
def async_get_agent(self, agent_id: str) -> AbstractConversationAgent | None:
@@ -182,3 +196,23 @@ class AgentManager:
def async_unset_agent(self, agent_id: str) -> None:
"""Unset the agent."""
self._agents.pop(agent_id, None)
async def async_setup_default_agent(self, agent: DefaultAgent) -> None:
"""Set up the default agent."""
agent.update_triggers(self.triggers_details)
self.default_agent = agent
def register_trigger(self, trigger_details: TriggerDetails) -> CALLBACK_TYPE:
"""Register a trigger."""
self.triggers_details.append(trigger_details)
if self.default_agent is not None:
self.default_agent.update_triggers(self.triggers_details)
@callback
def unregister_trigger() -> None:
"""Unregister the trigger."""
self.triggers_details.remove(trigger_details)
if self.default_agent is not None:
self.default_agent.update_triggers(self.triggers_details)
return unregister_trigger
@@ -10,11 +10,9 @@ from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from homeassistant.helpers.entity_component import EntityComponent
from .default_agent import DefaultAgent
from .entity import ConversationEntity
DOMAIN = "conversation"
DEFAULT_EXPOSED_ATTRIBUTES = {"device_class"}
HOME_ASSISTANT_AGENT = "conversation.home_assistant"
ATTR_TEXT = "text"
@@ -26,7 +24,6 @@ SERVICE_PROCESS = "process"
SERVICE_RELOAD = "reload"
DATA_COMPONENT: HassKey[EntityComponent[ConversationEntity]] = HassKey(DOMAIN)
DATA_DEFAULT_ENTITY: HassKey[DefaultAgent] = HassKey(f"{DOMAIN}_default_entity")
class ConversationEntityFeature(IntFlag):
@@ -4,13 +4,11 @@ from __future__ import annotations
import asyncio
from collections import OrderedDict
from collections.abc import Awaitable, Callable, Iterable
from collections.abc import Callable, Iterable
from dataclasses import dataclass
from enum import Enum, auto
import functools
import logging
from pathlib import Path
import re
import time
from typing import IO, Any, cast
@@ -53,6 +51,7 @@ from homeassistant.components.homeassistant.exposed_entities import (
async_should_expose,
)
from homeassistant.const import EVENT_STATE_CHANGED, MATCH_ALL
from homeassistant.core import Event, callback
from homeassistant.helpers import (
area_registry as ar,
device_registry as dr,
@@ -68,25 +67,22 @@ from homeassistant.helpers.event import async_track_state_added_domain
from homeassistant.util import language as language_util
from homeassistant.util.json import JsonObjectType, json_loads_object
from .agent_manager import get_agent_manager
from .chat_log import AssistantContent, ChatLog
from .const import (
DATA_DEFAULT_ENTITY,
DEFAULT_EXPOSED_ATTRIBUTES,
DOMAIN,
ConversationEntityFeature,
)
from .const import DOMAIN, ConversationEntityFeature
from .entity import ConversationEntity
from .models import ConversationInput, ConversationResult
from .trace import ConversationTraceEventType, async_conversation_trace_append
from .trigger import TriggerDetails
_LOGGER = logging.getLogger(__name__)
_DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that"
_ENTITY_REGISTRY_UPDATE_FIELDS = ["aliases", "name", "original_name"]
REGEX_TYPE = type(re.compile(""))
TRIGGER_CALLBACK_TYPE = Callable[
[ConversationInput, RecognizeResult], Awaitable[str | None]
]
_DEFAULT_EXPOSED_ATTRIBUTES = {"device_class"}
METADATA_CUSTOM_SENTENCE = "hass_custom_sentence"
METADATA_CUSTOM_FILE = "hass_custom_file"
METADATA_FUZZY_MATCH = "hass_fuzzy_match"
@@ -112,14 +108,6 @@ class LanguageIntents:
fuzzy_responses: FuzzyLanguageResponses | None = None
@dataclass(slots=True)
class TriggerData:
"""List of sentences and the callback for a trigger."""
sentences: list[str]
callback: TRIGGER_CALLBACK_TYPE
@dataclass(slots=True)
class SentenceTriggerResult:
"""Result when matching a sentence trigger in an automation."""
@@ -155,8 +143,8 @@ class IntentCacheKey:
language: str
"""Language of text."""
device_id: str | None
"""Device id from user input."""
satellite_id: str | None
"""Satellite id from user input."""
@dataclass(frozen=True)
@@ -209,9 +197,9 @@ async def async_setup_default_agent(
config_intents: dict[str, Any],
) -> None:
"""Set up entity registry listener for the default agent."""
entity = DefaultAgent(hass, config_intents)
await entity_component.async_add_entities([entity])
hass.data[DATA_DEFAULT_ENTITY] = entity
agent = DefaultAgent(hass, config_intents)
await entity_component.async_add_entities([agent])
await get_agent_manager(hass).async_setup_default_agent(agent)
@core.callback
def async_entity_state_listener(
@@ -242,21 +230,23 @@ class DefaultAgent(ConversationEntity):
"""Initialize the default agent."""
self.hass = hass
self._lang_intents: dict[str, LanguageIntents | object] = {}
self._load_intents_lock = asyncio.Lock()
# intent -> [sentences]
self._config_intents: dict[str, Any] = config_intents
# Sentences that will trigger a callback (skipping intent recognition)
self._triggers_details: list[TriggerDetails] = []
self._trigger_intents: Intents | None = None
# Slot lists for entities, areas, etc.
self._slot_lists: dict[str, SlotList] | None = None
self._unsub_clear_slot_list: list[Callable[[], None]] | None = None
# Used to filter slot lists before intent matching
self._exposed_names_trie: Trie | None = None
self._unexposed_names_trie: Trie | None = None
# Sentences that will trigger a callback (skipping intent recognition)
self.trigger_sentences: list[TriggerData] = []
self._trigger_intents: Intents | None = None
self._unsub_clear_slot_list: list[Callable[[], None]] | None = None
self._load_intents_lock = asyncio.Lock()
# LRU cache to avoid unnecessary intent matching
self._intent_cache = IntentCache(capacity=128)
@@ -445,9 +435,15 @@ class DefaultAgent(ConversationEntity):
}
for entity in result.entities_list
}
device_area = self._get_device_area(user_input.device_id)
if device_area:
slots["preferred_area_id"] = {"value": device_area.id}
satellite_id = user_input.satellite_id
device_id = user_input.device_id
satellite_area, device_id = self._get_satellite_area_and_device(
satellite_id, device_id
)
if satellite_area is not None:
slots["preferred_area_id"] = {"value": satellite_area.id}
async_conversation_trace_append(
ConversationTraceEventType.TOOL_CALL,
{
@@ -469,8 +465,8 @@ class DefaultAgent(ConversationEntity):
user_input.context,
language,
assistant=DOMAIN,
device_id=user_input.device_id,
satellite_id=user_input.satellite_id,
device_id=device_id,
satellite_id=satellite_id,
conversation_agent_id=user_input.agent_id,
)
except intent.MatchFailedError as match_error:
@@ -536,7 +532,9 @@ class DefaultAgent(ConversationEntity):
# Try cache first
cache_key = IntentCacheKey(
text=user_input.text, language=language, device_id=user_input.device_id
text=user_input.text,
language=language,
satellite_id=user_input.satellite_id,
)
cache_value = self._intent_cache.get(cache_key)
if cache_value is not None:
@@ -846,7 +844,7 @@ class DefaultAgent(ConversationEntity):
context = {"domain": state.domain}
if state.attributes:
# Include some attributes
for attr in DEFAULT_EXPOSED_ATTRIBUTES:
for attr in _DEFAULT_EXPOSED_ATTRIBUTES:
if attr not in state.attributes:
continue
context[attr] = state.attributes[attr]
@@ -1192,8 +1190,8 @@ class DefaultAgent(ConversationEntity):
fuzzy_responses=fuzzy_responses,
)
@core.callback
def _async_clear_slot_list(self, event: core.Event[Any] | None = None) -> None:
@callback
def _async_clear_slot_list(self, event: Event[Any] | None = None) -> None:
"""Clear slot lists when a registry has changed."""
# Two subscribers can be scheduled at same time
_LOGGER.debug("Clearing slot lists")
@@ -1306,28 +1304,40 @@ class DefaultAgent(ConversationEntity):
self, user_input: ConversationInput
) -> dict[str, Any] | None:
"""Return intent recognition context for user input."""
if not user_input.device_id:
satellite_area, _ = self._get_satellite_area_and_device(
user_input.satellite_id, user_input.device_id
)
if satellite_area is None:
return None
device_area = self._get_device_area(user_input.device_id)
if device_area is None:
return None
return {"area": {"value": satellite_area.name, "text": satellite_area.name}}
return {"area": {"value": device_area.name, "text": device_area.name}}
def _get_satellite_area_and_device(
self, satellite_id: str | None, device_id: str | None = None
) -> tuple[ar.AreaEntry | None, str | None]:
"""Return area entry and device id."""
hass = self.hass
def _get_device_area(self, device_id: str | None) -> ar.AreaEntry | None:
"""Return area object for given device identifier."""
if device_id is None:
return None
area_id: str | None = None
devices = dr.async_get(self.hass)
device = devices.async_get(device_id)
if (device is None) or (device.area_id is None):
return None
if (
satellite_id is not None
and (entity_entry := er.async_get(hass).async_get(satellite_id)) is not None
):
area_id = entity_entry.area_id
device_id = entity_entry.device_id
areas = ar.async_get(self.hass)
if (
area_id is None
and device_id is not None
and (device_entry := dr.async_get(hass).async_get(device_id)) is not None
):
area_id = device_entry.area_id
return areas.async_get_area(device.area_id)
if area_id is None:
return None, device_id
return ar.async_get(hass).async_get_area(area_id), device_id
def _get_error_text(
self,
@@ -1351,22 +1361,14 @@ class DefaultAgent(ConversationEntity):
return response_template.async_render(response_args)
@core.callback
def register_trigger(
self,
sentences: list[str],
callback: TRIGGER_CALLBACK_TYPE,
) -> core.CALLBACK_TYPE:
"""Register a list of sentences that will trigger a callback when recognized."""
trigger_data = TriggerData(sentences=sentences, callback=callback)
self.trigger_sentences.append(trigger_data)
@callback
def update_triggers(self, triggers_details: list[TriggerDetails]) -> None:
"""Update triggers."""
self._triggers_details = triggers_details
# Force rebuild on next use
self._trigger_intents = None
return functools.partial(self._unregister_trigger, trigger_data)
@core.callback
def _rebuild_trigger_intents(self) -> None:
"""Rebuild the HassIL intents object from the current trigger sentences."""
intents_dict = {
@@ -1375,8 +1377,8 @@ class DefaultAgent(ConversationEntity):
# Use trigger data index as a virtual intent name for HassIL.
# This works because the intents are rebuilt on every
# register/unregister.
str(trigger_id): {"data": [{"sentences": trigger_data.sentences}]}
for trigger_id, trigger_data in enumerate(self.trigger_sentences)
str(trigger_id): {"data": [{"sentences": trigger_details.sentences}]}
for trigger_id, trigger_details in enumerate(self._triggers_details)
},
}
@@ -1396,14 +1398,6 @@ class DefaultAgent(ConversationEntity):
_LOGGER.debug("Rebuilt trigger intents: %s", intents_dict)
@core.callback
def _unregister_trigger(self, trigger_data: TriggerData) -> None:
"""Unregister a set of trigger sentences."""
self.trigger_sentences.remove(trigger_data)
# Force rebuild on next use
self._trigger_intents = None
async def async_recognize_sentence_trigger(
self, user_input: ConversationInput
) -> SentenceTriggerResult | None:
@@ -1412,7 +1406,7 @@ class DefaultAgent(ConversationEntity):
Calls the registered callbacks if there's a match and returns a sentence
trigger result.
"""
if not self.trigger_sentences:
if not self._triggers_details:
# No triggers registered
return None
@@ -1457,7 +1451,7 @@ class DefaultAgent(ConversationEntity):
# Gather callback responses in parallel
trigger_callbacks = [
self.trigger_sentences[trigger_id].callback(user_input, trigger_result)
self._triggers_details[trigger_id].callback(user_input, trigger_result)
for trigger_id, trigger_result in result.matched_triggers.items()
]
@@ -25,7 +25,7 @@ from .agent_manager import (
async_get_agent,
get_agent_manager,
)
from .const import DATA_COMPONENT, DATA_DEFAULT_ENTITY
from .const import DATA_COMPONENT
from .default_agent import (
METADATA_CUSTOM_FILE,
METADATA_CUSTOM_SENTENCE,
@@ -169,11 +169,11 @@ async def websocket_list_sentences(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None:
"""List custom registered sentences."""
agent = hass.data[DATA_DEFAULT_ENTITY]
manager = get_agent_manager(hass)
sentences = []
for trigger_data in agent.trigger_sentences:
sentences.extend(trigger_data.sentences)
for trigger_details in manager.triggers_details:
sentences.extend(trigger_details.sentences)
connection.send_result(msg["id"], {"trigger_sentences": sentences})
@@ -191,7 +191,8 @@ async def websocket_hass_agent_debug(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None:
"""Return intents that would be matched by the default agent for a list of sentences."""
agent = hass.data[DATA_DEFAULT_ENTITY]
agent = get_agent_manager(hass).default_agent
assert agent is not None
# Return results for each sentence in the same order as the input.
result_dicts: list[dict[str, Any] | None] = []
@@ -1,4 +1,9 @@
{
"entity_component": {
"_": {
"default": "mdi:forum-outline"
}
},
"services": {
"process": {
"service": "mdi:message-processing"
@@ -4,7 +4,7 @@
"codeowners": ["@home-assistant/core", "@synesthesiam", "@arturpragacz"],
"dependencies": ["http", "intent"],
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.2.0", "home-assistant-intents==2025.9.3"]
}
@@ -2,6 +2,8 @@
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from hassil.recognize import RecognizeResult
@@ -15,14 +17,27 @@ import voluptuous as vol
from homeassistant.const import CONF_COMMAND, CONF_PLATFORM
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.script import ScriptRunResult
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import UNDEFINED, ConfigType
from .const import DATA_DEFAULT_ENTITY, DOMAIN
from .agent_manager import get_agent_manager
from .const import DOMAIN
from .models import ConversationInput
TRIGGER_CALLBACK_TYPE = Callable[
[ConversationInput, RecognizeResult], Awaitable[str | None]
]
@dataclass(slots=True)
class TriggerDetails:
"""List of sentences and the callback for a trigger."""
sentences: list[str]
callback: TRIGGER_CALLBACK_TYPE
def has_no_punctuation(value: list[str]) -> list[str]:
"""Validate result does not contain punctuation."""
@@ -70,6 +85,8 @@ async def async_attach_trigger(
trigger_data = trigger_info["trigger_data"]
sentences = config.get(CONF_COMMAND, [])
ent_reg = er.async_get(hass)
job = HassJob(action)
async def call_action(
@@ -91,6 +108,14 @@ async def async_attach_trigger(
for entity_name, entity in result.entities.items()
}
satellite_id = user_input.satellite_id
device_id = user_input.device_id
if (
satellite_id is not None
and (satellite_entry := ent_reg.async_get(satellite_id)) is not None
):
device_id = satellite_entry.device_id
trigger_input: dict[str, Any] = { # Satisfy type checker
**trigger_data,
"platform": DOMAIN,
@@ -99,8 +124,8 @@ async def async_attach_trigger(
"slots": { # direct access to values
entity_name: entity["value"] for entity_name, entity in details.items()
},
"device_id": user_input.device_id,
"satellite_id": user_input.satellite_id,
"device_id": device_id,
"satellite_id": satellite_id,
"user_input": user_input.as_dict(),
}
@@ -123,4 +148,6 @@ async def async_attach_trigger(
# two trigger copies for who will provide a response.
return None
return hass.data[DATA_DEFAULT_ENTITY].register_trigger(sentences, call_action)
return get_agent_manager(hass).register_trigger(
TriggerDetails(sentences=sentences, callback=call_action)
)
@@ -9,6 +9,7 @@
"conversation",
"dhcp",
"energy",
"file",
"go2rtc",
"history",
"homeassistant_alerts",
@@ -19,6 +20,7 @@
"ssdp",
"stream",
"sun",
"usage_prediction",
"usb",
"webhook",
"zeroconf"
+2
View File
@@ -43,3 +43,5 @@ class DelugeSensorType(enum.StrEnum):
UPLOAD_SPEED_SENSOR = "upload_speed"
PROTOCOL_TRAFFIC_UPLOAD_SPEED_SENSOR = "protocol_traffic_upload_speed"
PROTOCOL_TRAFFIC_DOWNLOAD_SPEED_SENSOR = "protocol_traffic_download_speed"
DOWNLOADING_COUNT_SENSOR = "downloading_count"
SEEDING_COUNT_SENSOR = "seeding_count"
+37 -8
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
from collections import Counter
from datetime import timedelta
from ssl import SSLError
from typing import Any
@@ -14,11 +15,22 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import LOGGER, DelugeGetSessionStatusKeys
from .const import LOGGER, DelugeGetSessionStatusKeys, DelugeSensorType
type DelugeConfigEntry = ConfigEntry[DelugeDataUpdateCoordinator]
def count_states(data: dict[str, Any]) -> dict[str, int]:
"""Count the states of the provided torrents."""
counts = Counter(torrent[b"state"].decode() for torrent in data.values())
return {
DelugeSensorType.DOWNLOADING_COUNT_SENSOR.value: counts.get("Downloading", 0),
DelugeSensorType.SEEDING_COUNT_SENSOR.value: counts.get("Seeding", 0),
}
class DelugeDataUpdateCoordinator(
DataUpdateCoordinator[dict[Platform, dict[str, Any]]]
):
@@ -39,19 +51,22 @@ class DelugeDataUpdateCoordinator(
)
self.api = api
async def _async_update_data(self) -> dict[Platform, dict[str, Any]]:
"""Get the latest data from Deluge and updates the state."""
def _get_deluge_data(self):
"""Get the latest data from Deluge."""
data = {}
try:
_data = await self.hass.async_add_executor_job(
self.api.call,
data["session_status"] = self.api.call(
"core.get_session_status",
[iter_member.value for iter_member in list(DelugeGetSessionStatusKeys)],
)
data[Platform.SENSOR] = {k.decode(): v for k, v in _data.items()}
data[Platform.SWITCH] = await self.hass.async_add_executor_job(
self.api.call, "core.get_torrents_status", {}, ["paused"]
data["torrents_status_state"] = self.api.call(
"core.get_torrents_status", {}, ["state"]
)
data["torrents_status_paused"] = self.api.call(
"core.get_torrents_status", {}, ["paused"]
)
except (
ConnectionRefusedError,
TimeoutError,
@@ -66,4 +81,18 @@ class DelugeDataUpdateCoordinator(
) from ex
LOGGER.error("Unknown error connecting to Deluge: %s", ex)
raise
return data
async def _async_update_data(self) -> dict[Platform, dict[str, Any]]:
"""Get the latest data from Deluge and updates the state."""
deluge_data = await self.hass.async_add_executor_job(self._get_deluge_data)
data = {}
data[Platform.SENSOR] = {
k.decode(): v for k, v in deluge_data["session_status"].items()
}
data[Platform.SENSOR].update(count_states(deluge_data["torrents_status_state"]))
data[Platform.SWITCH] = deluge_data["torrents_status_paused"]
return data
@@ -0,0 +1,12 @@
{
"entity": {
"sensor": {
"downloading_count": {
"default": "mdi:download"
},
"seeding_count": {
"default": "mdi:upload"
}
}
}
}
+12
View File
@@ -110,6 +110,18 @@ SENSOR_TYPES: tuple[DelugeSensorEntityDescription, ...] = (
data, DelugeSensorType.PROTOCOL_TRAFFIC_DOWNLOAD_SPEED_SENSOR.value
),
),
DelugeSensorEntityDescription(
key=DelugeSensorType.DOWNLOADING_COUNT_SENSOR.value,
translation_key=DelugeSensorType.DOWNLOADING_COUNT_SENSOR.value,
state_class=SensorStateClass.TOTAL,
value=lambda data: data[DelugeSensorType.DOWNLOADING_COUNT_SENSOR.value],
),
DelugeSensorEntityDescription(
key=DelugeSensorType.SEEDING_COUNT_SENSOR.value,
translation_key=DelugeSensorType.SEEDING_COUNT_SENSOR.value,
state_class=SensorStateClass.TOTAL,
value=lambda data: data[DelugeSensorType.SEEDING_COUNT_SENSOR.value],
),
)
@@ -36,6 +36,10 @@
"idle": "[%key:common::state::idle%]"
}
},
"downloading_count": {
"name": "Downloading count",
"unit_of_measurement": "torrents"
},
"download_speed": {
"name": "Download speed"
},
@@ -45,6 +49,10 @@
"protocol_traffic_upload_speed": {
"name": "Protocol traffic upload speed"
},
"seeding_count": {
"name": "Seeding count",
"unit_of_measurement": "[%key:component::deluge::entity::sensor::downloading_count::unit_of_measurement%]"
},
"upload_speed": {
"name": "Upload speed"
}
@@ -0,0 +1,23 @@
"""Diagnostics support for derivative."""
from __future__ import annotations
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
registry = er.async_get(hass)
entities = registry.entities.get_entries_for_config_entry_id(config_entry.entry_id)
return {
"config_entry": config_entry.as_dict(),
"entity": [entity.extended_dict for entity in entities],
}
+50 -3
View File
@@ -227,15 +227,28 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
weight = calculate_weight(start, end, current_time)
derivative = derivative + (value * Decimal(weight))
_LOGGER.debug(
"%s: Calculated new derivative as %f from %d segments",
self.entity_id,
derivative,
len(self._state_list),
)
return derivative
def _prune_state_list(self, current_time: datetime) -> None:
# filter out all derivatives older than `time_window` from our window list
old_len = len(self._state_list)
self._state_list = [
(time_start, time_end, state)
for time_start, time_end, state in self._state_list
if (current_time - time_end).total_seconds() < self._time_window
]
_LOGGER.debug(
"%s: Pruned %d elements from state list",
self.entity_id,
old_len - len(self._state_list),
)
def _handle_invalid_source_state(self, state: State | None) -> bool:
# Check the source state for unknown/unavailable condition. If unusable, write unknown/unavailable state and return false.
@@ -292,6 +305,10 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
) -> None:
"""Calculate derivative based on time and reschedule."""
_LOGGER.debug(
"%s: Recalculating derivative due to max_sub_interval time elapsed",
self.entity_id,
)
self._prune_state_list(now)
derivative = self._calc_derivative_from_state_list(now)
self._write_native_value(derivative)
@@ -300,6 +317,11 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
if derivative != 0:
schedule_max_sub_interval_exceeded(source_state)
_LOGGER.debug(
"%s: Scheduling max_sub_interval_callback in %s",
self.entity_id,
self._max_sub_interval,
)
self._cancel_max_sub_interval_exceeded_callback = async_call_later(
self.hass,
self._max_sub_interval,
@@ -309,6 +331,9 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
@callback
def on_state_reported(event: Event[EventStateReportedData]) -> None:
"""Handle constant sensor state."""
_LOGGER.debug(
"%s: New state reported event: %s", self.entity_id, event.data
)
self._cancel_max_sub_interval_exceeded_callback()
new_state = event.data["new_state"]
if not self._handle_invalid_source_state(new_state):
@@ -330,6 +355,7 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
@callback
def on_state_changed(event: Event[EventStateChangedData]) -> None:
"""Handle changed sensor state."""
_LOGGER.debug("%s: New state changed event: %s", self.entity_id, event.data)
self._cancel_max_sub_interval_exceeded_callback()
new_state = event.data["new_state"]
if not self._handle_invalid_source_state(new_state):
@@ -382,15 +408,32 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
/ Decimal(self._unit_prefix)
* Decimal(self._unit_time)
)
_LOGGER.debug(
"%s: Calculated new derivative segment as %f / %f / %f * %f = %f",
self.entity_id,
delta_value,
elapsed_time,
self._unit_prefix,
self._unit_time,
new_derivative,
)
except ValueError as err:
_LOGGER.warning("While calculating derivative: %s", err)
_LOGGER.warning(
"%s: While calculating derivative: %s", self.entity_id, err
)
except DecimalException as err:
_LOGGER.warning(
"Invalid state (%s > %s): %s", old_value, new_state.state, err
"%s: Invalid state (%s > %s): %s",
self.entity_id,
old_value,
new_state.state,
err,
)
except AssertionError as err:
_LOGGER.error("Could not calculate derivative: %s", err)
_LOGGER.error(
"%s: Could not calculate derivative: %s", self.entity_id, err
)
# For total inreasing sensors, the value is expected to continuously increase.
# A negative derivative for a total increasing sensor likely indicates the
@@ -400,6 +443,10 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
== SensorStateClass.TOTAL_INCREASING
and new_derivative < 0
):
_LOGGER.debug(
"%s: Dropping sample as source total_increasing sensor decreased",
self.entity_id,
)
return
# add latest derivative to the window list
@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/droplet",
"iot_class": "local_push",
"quality_scale": "bronze",
"requirements": ["pydroplet==2.3.2"],
"requirements": ["pydroplet==2.3.3"],
"zeroconf": ["_droplet._tcp.local."]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==13.7.0"]
"requirements": ["py-sucks==0.9.11", "deebot-client==14.0.0"]
}
+28 -1
View File
@@ -5,9 +5,11 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from deebot_client.capabilities import CapabilitySet
from deebot_client.capabilities import CapabilityNumber, CapabilitySet
from deebot_client.device import Device
from deebot_client.events import CleanCountEvent, CutDirectionEvent, VolumeEvent
from deebot_client.events.base import Event
from deebot_client.events.water_info import WaterCustomAmountEvent
from homeassistant.components.number import (
NumberEntity,
@@ -75,6 +77,19 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsNumberEntityDescription, ...] = (
native_step=1.0,
mode=NumberMode.BOX,
),
EcovacsNumberEntityDescription[WaterCustomAmountEvent](
capability_fn=lambda caps: (
caps.water.amount
if caps.water and isinstance(caps.water.amount, CapabilityNumber)
else None
),
value_fn=lambda e: e.value,
key="water_amount",
translation_key="water_amount",
entity_category=EntityCategory.CONFIG,
native_step=1.0,
mode=NumberMode.BOX,
),
)
@@ -100,6 +115,18 @@ class EcovacsNumberEntity[EventT: Event](
entity_description: EcovacsNumberEntityDescription
def __init__(
self,
device: Device,
capability: CapabilitySet[EventT, [int]],
entity_description: EcovacsNumberEntityDescription,
) -> None:
"""Initialize entity."""
super().__init__(device, capability, entity_description)
if isinstance(capability, CapabilityNumber):
self._attr_native_min_value = capability.min
self._attr_native_max_value = capability.max
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
await super().async_added_to_hass()
+5 -1
View File
@@ -33,7 +33,11 @@ class EcovacsSelectEntityDescription[EventT: Event](
ENTITY_DESCRIPTIONS: tuple[EcovacsSelectEntityDescription, ...] = (
EcovacsSelectEntityDescription[WaterAmountEvent](
capability_fn=lambda caps: caps.water.amount if caps.water else None,
capability_fn=lambda caps: (
caps.water.amount
if caps.water and isinstance(caps.water.amount, CapabilitySetTypes)
else None
),
current_option_fn=lambda e: get_name_key(e.value),
options_fn=lambda water: [get_name_key(amount) for amount in water.types],
key="water_amount",
@@ -102,6 +102,9 @@
},
"volume": {
"name": "Volume"
},
"water_amount": {
"name": "Water flow level"
}
},
"sensor": {
@@ -152,8 +155,10 @@
"station_state": {
"name": "Station state",
"state": {
"drying_mop": "Drying mop",
"idle": "[%key:common::state::idle%]",
"emptying_dustbin": "Emptying dustbin"
"emptying_dustbin": "Emptying dustbin",
"washing_mop": "Washing mop"
}
},
"stats_area": {
@@ -174,7 +179,7 @@
},
"select": {
"water_amount": {
"name": "Water flow level",
"name": "[%key:component::ecovacs::entity::number::water_amount::name%]",
"state": {
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
-5
View File
@@ -7,8 +7,6 @@ import random
import string
from typing import TYPE_CHECKING
from deebot_client.events.station import State
from homeassistant.core import HomeAssistant, callback
from homeassistant.util import slugify
@@ -49,9 +47,6 @@ def get_supported_entities(
@callback
def get_name_key(enum: Enum) -> str:
"""Return the lower case name of the enum."""
if enum is State.EMPTYING:
# Will be fixed in the next major release of deebot-client
return "emptying_dustbin"
return enum.name.lower()
@@ -152,24 +152,28 @@ ECOWITT_SENSORS_MAPPING: Final = {
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
),
EcoWittSensorTypes.RAIN_COUNT_INCHES: SensorEntityDescription(
key="RAIN_COUNT_INCHES",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=2,
),
EcoWittSensorTypes.RAIN_RATE_MM: SensorEntityDescription(
key="RAIN_RATE_MM",
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
suggested_display_precision=1,
),
EcoWittSensorTypes.RAIN_RATE_INCHES: SensorEntityDescription(
key="RAIN_RATE_INCHES",
native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_HOUR,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
suggested_display_precision=2,
),
EcoWittSensorTypes.LIGHTNING_DISTANCE_KM: SensorEntityDescription(
key="LIGHTNING_DISTANCE_KM",
@@ -230,6 +234,17 @@ ECOWITT_SENSORS_MAPPING: Final = {
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.DISTANCE_MM: SensorEntityDescription(
key="DISTANCE_MM",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.MILLIMETERS,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.HEAT_COUNT: SensorEntityDescription(
key="HEAT_COUNT",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
),
EcoWittSensorTypes.PM1: SensorEntityDescription(
key="PM1",
device_class=SensorDeviceClass.PM1,
@@ -238,6 +253,7 @@ ECOWITT_SENSORS_MAPPING: Final = {
),
EcoWittSensorTypes.PM4: SensorEntityDescription(
key="PM4",
device_class=SensorDeviceClass.PM4,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
+85 -3
View File
@@ -120,6 +120,14 @@ def _make_url_from_data(data: dict[str, str]) -> str:
return f"{protocol}{address}"
def _get_protocol_from_url(url: str) -> str:
"""Get protocol from URL. Returns the configured protocol from URL or the default secure protocol."""
return next(
(k for k, v in PROTOCOL_MAP.items() if url.startswith(v)),
DEFAULT_SECURE_PROTOCOL,
)
def _placeholders_from_device(device: ElkSystem) -> dict[str, str]:
return {
"mac_address": _short_mac(device.mac_address),
@@ -205,6 +213,78 @@ class Elkm1ConfigFlow(ConfigFlow, domain=DOMAIN):
)
return await self.async_step_discovered_connection()
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of the integration."""
errors: dict[str, str] = {}
reconfigure_entry = self._get_reconfigure_entry()
existing_data = reconfigure_entry.data
if user_input is not None:
validate_input_data = dict(user_input)
validate_input_data[CONF_PREFIX] = existing_data.get(CONF_PREFIX, "")
try:
info = await validate_input(
validate_input_data, reconfigure_entry.unique_id
)
except TimeoutError:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors[CONF_PASSWORD] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception during reconfiguration")
errors["base"] = "unknown"
else:
# Discover the device at the provided address to obtain its MAC (unique_id)
device = await async_discover_device(
self.hass, validate_input_data[CONF_ADDRESS]
)
if device is not None and device.mac_address:
await self.async_set_unique_id(dr.format_mac(device.mac_address))
self._abort_if_unique_id_mismatch() # aborts if user tried to switch devices
else:
# If we cannot confirm identity, keep existing behavior (don't block reconfigure)
await self.async_set_unique_id(reconfigure_entry.unique_id)
return self.async_update_reload_and_abort(
reconfigure_entry,
data_updates={
**reconfigure_entry.data,
CONF_HOST: info[CONF_HOST],
CONF_USERNAME: validate_input_data[CONF_USERNAME],
CONF_PASSWORD: validate_input_data[CONF_PASSWORD],
CONF_PREFIX: info[CONF_PREFIX],
},
reason="reconfigure_successful",
)
return self.async_show_form(
step_id="reconfigure",
data_schema=vol.Schema(
{
vol.Optional(
CONF_USERNAME,
default=existing_data.get(CONF_USERNAME, ""),
): str,
vol.Optional(
CONF_PASSWORD,
default="",
): str,
vol.Required(
CONF_ADDRESS,
default=hostname_from_url(existing_data[CONF_HOST]),
): str,
vol.Required(
CONF_PROTOCOL,
default=_get_protocol_from_url(existing_data[CONF_HOST]),
): vol.In(ALL_PROTOCOLS),
}
),
errors=errors,
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -249,12 +329,14 @@ class Elkm1ConfigFlow(ConfigFlow, domain=DOMAIN):
try:
info = await validate_input(user_input, self.unique_id)
except TimeoutError:
except TimeoutError as ex:
_LOGGER.debug("Connection timed out: %s", ex)
return {"base": "cannot_connect"}, None
except InvalidAuth:
except InvalidAuth as ex:
_LOGGER.debug("Invalid auth for %s: %s", user_input.get(CONF_HOST), ex)
return {CONF_PASSWORD: "invalid_auth"}, None
except Exception:
_LOGGER.exception("Unexpected exception")
_LOGGER.exception("Unexpected error validating input")
return {"base": "unknown"}, None
if importing:
+22 -10
View File
@@ -17,8 +17,8 @@
"address": "The IP address or domain or serial port if connecting via serial.",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"prefix": "A unique prefix (leave blank if you only have one ElkM1).",
"temperature_unit": "The temperature unit ElkM1 uses."
"prefix": "A unique prefix (leave blank if you only have one Elk-M1).",
"temperature_unit": "The temperature unit Elk-M1 uses."
}
},
"discovered_connection": {
@@ -30,6 +30,16 @@
"password": "[%key:common::config_flow::data::password%]",
"temperature_unit": "[%key:component::elkm1::config::step::manual_connection::data::temperature_unit%]"
}
},
"reconfigure": {
"title": "Reconfigure Elk-M1 Control",
"description": "[%key:component::elkm1::config::step::manual_connection::description%]",
"data": {
"protocol": "[%key:component::elkm1::config::step::manual_connection::data::protocol%]",
"address": "[%key:component::elkm1::config::step::manual_connection::data::address%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
@@ -42,8 +52,10 @@
"unknown": "[%key:common::config_flow::error::unknown%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"already_configured": "An ElkM1 with this prefix is already configured",
"address_already_configured": "An ElkM1 with this address is already configured"
"already_configured": "An Elk-M1 with this prefix is already configured",
"address_already_configured": "An Elk-M1 with this address is already configured",
"reconfigure_successful": "Successfully reconfigured Elk-M1 integration",
"unique_id_mismatch": "Reconfigure should be used for the same device not a new one"
}
},
"services": {
@@ -69,7 +81,7 @@
},
"alarm_arm_home_instant": {
"name": "Alarm arm home instant",
"description": "Arms the ElkM1 in home instant mode.",
"description": "Arms the Elk-M1 in home instant mode.",
"fields": {
"code": {
"name": "Code",
@@ -79,7 +91,7 @@
},
"alarm_arm_night_instant": {
"name": "Alarm arm night instant",
"description": "Arms the ElkM1 in night instant mode.",
"description": "Arms the Elk-M1 in night instant mode.",
"fields": {
"code": {
"name": "Code",
@@ -89,7 +101,7 @@
},
"alarm_arm_vacation": {
"name": "Alarm arm vacation",
"description": "Arms the ElkM1 in vacation mode.",
"description": "Arms the Elk-M1 in vacation mode.",
"fields": {
"code": {
"name": "Code",
@@ -99,7 +111,7 @@
},
"alarm_display_message": {
"name": "Alarm display message",
"description": "Displays a message on all of the ElkM1 keypads for an area.",
"description": "Displays a message on all of the Elk-M1 keypads for an area.",
"fields": {
"clear": {
"name": "Clear",
@@ -135,7 +147,7 @@
},
"speak_phrase": {
"name": "Speak phrase",
"description": "Speaks a phrase. See list of phrases in ElkM1 ASCII Protocol documentation.",
"description": "Speaks a phrase. See list of phrases in Elk-M1 ASCII Protocol documentation.",
"fields": {
"number": {
"name": "Phrase number",
@@ -149,7 +161,7 @@
},
"speak_word": {
"name": "Speak word",
"description": "Speaks a word. See list of words in ElkM1 ASCII Protocol documentation.",
"description": "Speaks a word. See list of words in Elk-M1 ASCII Protocol documentation.",
"fields": {
"number": {
"name": "Word number",
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/emoncms",
"iot_class": "local_polling",
"requirements": ["pyemoncms==0.1.2"]
"requirements": ["pyemoncms==0.1.3"]
}
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/emoncms_history",
"iot_class": "local_polling",
"quality_scale": "legacy",
"requirements": ["pyemoncms==0.1.2"]
"requirements": ["pyemoncms==0.1.3"]
}
@@ -52,7 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: Eq3ConfigEntry) -> bool:
f"[{eq3_config.mac_address}] Device could not be found"
)
thermostat = Thermostat(mac_address=device) # type: ignore[arg-type]
thermostat = Thermostat(device)
entry.runtime_data = Eq3ConfigEntryData(
eq3_config=eq3_config, thermostat=thermostat
@@ -22,5 +22,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["eq3btsmart"],
"requirements": ["eq3btsmart==2.1.0", "bleak-esphome==3.3.0"]
"requirements": ["eq3btsmart==2.3.0"]
}
@@ -51,6 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> b
client_info=CLIENT_INFO,
zeroconf_instance=zeroconf_instance,
noise_psk=noise_psk,
timezone=hass.config.time_zone,
)
domain_data = DomainData.get(hass)
+40 -22
View File
@@ -138,6 +138,16 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
return await self._async_authenticate_or_add()
if error is None and entry_data.get(CONF_NOISE_PSK):
# Device was configured with encryption but now connects without it.
# Check if it's the same device before offering to remove encryption.
if self._reauth_entry.unique_id and self._device_mac:
expected_mac = format_mac(self._reauth_entry.unique_id)
actual_mac = format_mac(self._device_mac)
if expected_mac != actual_mac:
# Different device at the same IP - do not offer to remove encryption
return self._async_abort_wrong_device(
self._reauth_entry, expected_mac, actual_mac
)
return await self.async_step_reauth_encryption_removed_confirm()
return await self.async_step_reauth_confirm()
@@ -508,6 +518,28 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
CONF_DEVICE_NAME: self._device_name,
}
@callback
def _async_abort_wrong_device(
self, entry: ConfigEntry, expected_mac: str, actual_mac: str
) -> ConfigFlowResult:
"""Abort flow because a different device was found at the IP address."""
assert self._host is not None
assert self._device_name is not None
if self.source == SOURCE_RECONFIGURE:
reason = "reconfigure_unique_id_changed"
else:
reason = "reauth_unique_id_changed"
return self.async_abort(
reason=reason,
description_placeholders={
"name": entry.data.get(CONF_DEVICE_NAME, entry.title),
"host": self._host,
"expected_mac": expected_mac,
"unexpected_mac": actual_mac,
"unexpected_device_name": self._device_name,
},
)
async def _async_validated_connection(self) -> ConfigFlowResult:
"""Handle validated connection."""
if self.source == SOURCE_RECONFIGURE:
@@ -539,17 +571,10 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
# Reauth was triggered a while ago, and since than
# a new device resides at the same IP address.
assert self._device_name is not None
return self.async_abort(
reason="reauth_unique_id_changed",
description_placeholders={
"name": self._reauth_entry.data.get(
CONF_DEVICE_NAME, self._reauth_entry.title
),
"host": self._host,
"expected_mac": format_mac(self._reauth_entry.unique_id),
"unexpected_mac": format_mac(self.unique_id),
"unexpected_device_name": self._device_name,
},
return self._async_abort_wrong_device(
self._reauth_entry,
format_mac(self._reauth_entry.unique_id),
format_mac(self.unique_id),
)
async def _async_reconfig_validated_connection(self) -> ConfigFlowResult:
@@ -589,17 +614,10 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
if self._reconfig_entry.data.get(CONF_DEVICE_NAME) == self._device_name:
self._entry_with_name_conflict = self._reconfig_entry
return await self.async_step_name_conflict()
return self.async_abort(
reason="reconfigure_unique_id_changed",
description_placeholders={
"name": self._reconfig_entry.data.get(
CONF_DEVICE_NAME, self._reconfig_entry.title
),
"host": self._host,
"expected_mac": format_mac(self._reconfig_entry.unique_id),
"unexpected_mac": format_mac(self.unique_id),
"unexpected_device_name": self._device_name,
},
return self._async_abort_wrong_device(
self._reconfig_entry,
format_mac(self._reconfig_entry.unique_id),
format_mac(self.unique_id),
)
async def async_step_encryption_key(
+27 -2
View File
@@ -49,11 +49,13 @@ from aioesphomeapi import (
from aioesphomeapi.model import ButtonInfo
from bleak_esphome.backend.device import ESPHomeBluetoothDevice
from homeassistant import config_entries
from homeassistant.components.assist_satellite import AssistSatelliteConfiguration
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import discovery_flow, entity_registry as er
from homeassistant.helpers.service_info.esphome import ESPHomeServiceInfo
from homeassistant.helpers.storage import Store
from .const import DOMAIN
@@ -468,7 +470,7 @@ class RuntimeEntryData:
@callback
def async_on_connect(
self, device_info: DeviceInfo, api_version: APIVersion
self, hass: HomeAssistant, device_info: DeviceInfo, api_version: APIVersion
) -> None:
"""Call when the entry has been connected."""
self.available = True
@@ -484,6 +486,29 @@ class RuntimeEntryData:
# be marked as unavailable or not.
self.expected_disconnect = True
if not device_info.zwave_proxy_feature_flags:
return
assert self.client.connected_address
discovery_flow.async_create_flow(
hass,
"zwave_js",
{"source": config_entries.SOURCE_ESPHOME},
ESPHomeServiceInfo(
name=device_info.name,
zwave_home_id=device_info.zwave_home_id or None,
ip_address=self.client.connected_address,
port=self.client.port,
noise_psk=self.client.noise_psk,
),
discovery_key=discovery_flow.DiscoveryKey(
domain=DOMAIN,
key=device_info.mac_address,
version=1,
),
)
@callback
def async_register_assist_satellite_config_updated_callback(
self,
+1 -1
View File
@@ -505,7 +505,7 @@ class ESPHomeManager:
api_version = cli.api_version
assert api_version is not None, "API version must be set"
entry_data.async_on_connect(device_info, api_version)
entry_data.async_on_connect(hass, device_info, api_version)
await self._handle_dynamic_encryption_key(device_info)
@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==40.1.0",
"aioesphomeapi==41.9.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.3.0"
],
@@ -194,6 +194,21 @@ class EsphomeAssistSatelliteWakeWordSelect(
self._attr_options = [NO_WAKE_WORD, *sorted(self._wake_words)]
option = self._attr_current_option
if (
(self._wake_word_index == 0)
and (len(config.active_wake_words) == 1)
and (option in (None, NO_WAKE_WORD))
):
option = next(
(
wake_word
for wake_word, wake_word_id in self._wake_words.items()
if wake_word_id == config.active_wake_words[0]
),
None,
)
if (
(option is None)
or ((wake_word_id := self._wake_words.get(option)) is None)
@@ -12,7 +12,7 @@
"mqtt_missing_mac": "Missing MAC address in MQTT properties.",
"mqtt_missing_api": "Missing API port in MQTT properties.",
"mqtt_missing_ip": "Missing IP address in MQTT properties.",
"mqtt_missing_payload": "Missing MQTT Payload.",
"mqtt_missing_payload": "Missing MQTT payload.",
"name_conflict_migrated": "The configuration for `{name}` has been migrated to a new device with MAC address `{mac}` from `{existing_mac}`.",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"reauth_unique_id_changed": "**Re-authentication of `{name}` was aborted** because the address `{host}` points to a different device: `{unexpected_device_name}` (MAC: `{unexpected_mac}`) instead of the expected one (MAC: `{expected_mac}`).",
@@ -91,7 +91,7 @@
"subscribe_logs": "Subscribe to logs from the device."
},
"data_description": {
"allow_service_calls": "When enabled, ESPHome devices can perform Home Assistant actions, such as calling services or sending events. Only enable this if you trust the device.",
"allow_service_calls": "When enabled, ESPHome devices can perform Home Assistant actions or send events. Only enable this if you trust the device.",
"subscribe_logs": "When enabled, the device will send logs to Home Assistant and you can view them in the logs panel."
}
}
@@ -154,7 +154,7 @@
"description": "To improve Bluetooth reliability and performance, we highly recommend updating {name} with ESPHome {version} or later. When updating the device from ESPHome earlier than 2022.12.0, it is recommended to use a serial cable instead of an over-the-air update to take advantage of the new partition scheme."
},
"api_password_deprecated": {
"title": "API Password deprecated on {name}",
"title": "API password deprecated on {name}",
"description": "The API password for ESPHome is deprecated and the use of an API encryption key is recommended instead.\n\nRemove the API password and add an encryption key to your ESPHome device to resolve this issue."
},
"service_calls_not_allowed": {
@@ -193,10 +193,10 @@
"message": "Error communicating with the device {device_name}: {error}"
},
"error_compiling": {
"message": "Error compiling {configuration}; Try again in ESPHome dashboard for more information."
"message": "Error compiling {configuration}. Try again in ESPHome dashboard for more information."
},
"error_uploading": {
"message": "Error during OTA (Over-The-Air) of {configuration}; Try again in ESPHome dashboard for more information."
"message": "Error during OTA (Over-The-Air) update of {configuration}. Try again in ESPHome dashboard for more information."
},
"ota_in_progress": {
"message": "An OTA (Over-The-Air) update is already in progress for {configuration}."
+3 -3
View File
@@ -162,12 +162,12 @@ def setup_service_functions(
It appears that all TCC-compatible systems support the same three zones modes.
"""
@verify_domain_control(hass, DOMAIN)
@verify_domain_control(DOMAIN)
async def force_refresh(call: ServiceCall) -> None:
"""Obtain the latest state data via the vendor's RESTful API."""
await coordinator.async_refresh()
@verify_domain_control(hass, DOMAIN)
@verify_domain_control(DOMAIN)
async def set_system_mode(call: ServiceCall) -> None:
"""Set the system mode."""
assert coordinator.tcs is not None # mypy
@@ -179,7 +179,7 @@ def setup_service_functions(
}
async_dispatcher_send(hass, DOMAIN, payload)
@verify_domain_control(hass, DOMAIN)
@verify_domain_control(DOMAIN)
async def set_zone_override(call: ServiceCall) -> None:
"""Set the zone override (setpoint)."""
entity_id = call.data[ATTR_ENTITY_ID]
+8 -38
View File
@@ -66,26 +66,6 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = {
key="last_alarm_type_name",
translation_key="last_alarm_type_name",
),
"Record_Mode": SensorEntityDescription(
key="Record_Mode",
translation_key="record_mode",
entity_registry_enabled_default=False,
),
"battery_camera_work_mode": SensorEntityDescription(
key="battery_camera_work_mode",
translation_key="battery_camera_work_mode",
entity_registry_enabled_default=False,
),
"powerStatus": SensorEntityDescription(
key="powerStatus",
translation_key="power_status",
entity_registry_enabled_default=False,
),
"OnlineStatus": SensorEntityDescription(
key="OnlineStatus",
translation_key="online_status",
entity_registry_enabled_default=False,
),
}
@@ -96,26 +76,16 @@ async def async_setup_entry(
) -> None:
"""Set up EZVIZ sensors based on a config entry."""
coordinator = entry.runtime_data
entities: list[EzvizSensor] = []
for camera, sensors in coordinator.data.items():
entities.extend(
async_add_entities(
[
EzvizSensor(coordinator, camera, sensor)
for sensor, value in sensors.items()
if sensor in SENSOR_TYPES and value is not None
)
optionals = sensors.get("optionals", {})
entities.extend(
EzvizSensor(coordinator, camera, optional_key)
for optional_key in ("powerStatus", "OnlineStatus")
if optional_key in optionals
)
if "mode" in optionals.get("Record_Mode", {}):
entities.append(EzvizSensor(coordinator, camera, "mode"))
async_add_entities(entities)
for camera in coordinator.data
for sensor, value in coordinator.data[camera].items()
if sensor in SENSOR_TYPES
if value is not None
]
)
class EzvizSensor(EzvizEntity, SensorEntity):
@@ -147,18 +147,6 @@
},
"last_alarm_type_name": {
"name": "Last alarm type name"
},
"record_mode": {
"name": "Record mode"
},
"battery_camera_work_mode": {
"name": "Battery work mode"
},
"power_status": {
"name": "Power status"
},
"online_status": {
"name": "Online status"
}
},
"switch": {
+11
View File
@@ -7,11 +7,22 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_FILE_PATH, CONF_NAME, CONF_PLATFORM, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
from .services import async_register_services
PLATFORMS = [Platform.NOTIFY, Platform.SENSOR]
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the file component."""
async_register_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a file component entry."""
+4
View File
@@ -6,3 +6,7 @@ CONF_TIMESTAMP = "timestamp"
DEFAULT_NAME = "File"
FILE_ICON = "mdi:file"
SERVICE_READ_FILE = "read_file"
ATTR_FILE_NAME = "file_name"
ATTR_FILE_ENCODING = "file_encoding"
+7
View File
@@ -0,0 +1,7 @@
{
"services": {
"read_file": {
"service": "mdi:file"
}
}
}
+88
View File
@@ -0,0 +1,88 @@
"""File Service calls."""
from collections.abc import Callable
import json
import voluptuous as vol
import yaml
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
from .const import ATTR_FILE_ENCODING, ATTR_FILE_NAME, DOMAIN, SERVICE_READ_FILE
def async_register_services(hass: HomeAssistant) -> None:
"""Register services for File integration."""
if not hass.services.has_service(DOMAIN, SERVICE_READ_FILE):
hass.services.async_register(
DOMAIN,
SERVICE_READ_FILE,
read_file,
schema=vol.Schema(
{
vol.Required(ATTR_FILE_NAME): cv.string,
vol.Required(ATTR_FILE_ENCODING): cv.string,
}
),
supports_response=SupportsResponse.ONLY,
)
ENCODING_LOADERS: dict[str, tuple[Callable, type[Exception]]] = {
"json": (json.loads, json.JSONDecodeError),
"yaml": (yaml.safe_load, yaml.YAMLError),
}
def read_file(call: ServiceCall) -> dict:
"""Handle read_file service call."""
file_name = call.data[ATTR_FILE_NAME]
file_encoding = call.data[ATTR_FILE_ENCODING].lower()
if not call.hass.config.is_allowed_path(file_name):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_access_to_path",
translation_placeholders={"filename": file_name},
)
if file_encoding not in ENCODING_LOADERS:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="unsupported_file_encoding",
translation_placeholders={
"filename": file_name,
"encoding": file_encoding,
},
)
try:
with open(file_name, encoding="utf-8") as file:
file_content = file.read()
except FileNotFoundError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="file_not_found",
translation_placeholders={"filename": file_name},
) from err
except OSError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="file_read_error",
translation_placeholders={"filename": file_name},
) from err
loader, error_type = ENCODING_LOADERS[file_encoding]
try:
data = loader(file_content)
except error_type as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="file_decoding",
translation_placeholders={"filename": file_name, "encoding": file_encoding},
) from err
return {"data": data}
@@ -0,0 +1,14 @@
# Describes the format for available file services
read_file:
fields:
file_name:
example: "www/my_file.json"
selector:
text:
file_encoding:
example: "JSON"
selector:
select:
options:
- "JSON"
- "YAML"
@@ -64,6 +64,37 @@
},
"write_access_failed": {
"message": "Write access to {filename} failed: {exc}."
},
"no_access_to_path": {
"message": "Cannot read {filename}, no access to path; `allowlist_external_dirs` may need to be adjusted in `configuration.yaml`"
},
"unsupported_file_encoding": {
"message": "Cannot read {filename}, unsupported file encoding {encoding}."
},
"file_decoding": {
"message": "Cannot read file {filename} as {encoding}."
},
"file_not_found": {
"message": "File {filename} not found."
},
"file_read_error": {
"message": "Error reading {filename}."
}
},
"services": {
"read_file": {
"name": "Read file",
"description": "Reads a file and returns the contents.",
"fields": {
"file_name": {
"name": "File name",
"description": "Name of the file to read."
},
"file_encoding": {
"name": "File encoding",
"description": "Encoding of the file (JSON, YAML.)"
}
}
}
}
}
+7 -7
View File
@@ -14,13 +14,7 @@ from homeassistant.components.climate import (
HVACAction,
HVACMode,
)
from homeassistant.components.modbus import (
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_REGISTER_INPUT,
DEFAULT_HUB,
ModbusHub,
get_hub,
)
from homeassistant.components.modbus import ModbusHub, get_hub
from homeassistant.const import (
ATTR_TEMPERATURE,
CONF_NAME,
@@ -33,7 +27,13 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
# These constants are not offered by modbus, because modbus do not have
# an official API.
CALL_TYPE_REGISTER_HOLDING = "holding"
CALL_TYPE_REGISTER_INPUT = "input"
CALL_TYPE_WRITE_REGISTER = "write_register"
DEFAULT_HUB = "modbus_hub"
CONF_HUB = "hub"
PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend(
+4 -3
View File
@@ -31,11 +31,12 @@ SERVICE_SCHEMA_SET_GUEST_WIFI_PW = vol.Schema(
async def _async_set_guest_wifi_password(service_call: ServiceCall) -> None:
"""Call Fritz set guest wifi password service."""
hass = service_call.hass
target_entry_ids = await async_extract_config_entry_ids(hass, service_call)
target_entry_ids = await async_extract_config_entry_ids(service_call)
target_entries: list[FritzConfigEntry] = [
loaded_entry
for loaded_entry in hass.config_entries.async_loaded_entries(DOMAIN)
for loaded_entry in service_call.hass.config_entries.async_loaded_entries(
DOMAIN
)
if loaded_entry.entry_id in target_entry_ids
]
@@ -124,7 +124,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: GeniusHubConfigEntry) ->
def setup_service_functions(hass: HomeAssistant, broker):
"""Set up the service functions."""
@verify_domain_control(hass, DOMAIN)
@verify_domain_control(DOMAIN)
async def set_zone_mode(call: ServiceCall) -> None:
"""Set the system mode."""
entity_id = call.data[ATTR_ENTITY_ID]
@@ -0,0 +1,39 @@
"""Sensor entities for Geocaching."""
from typing import cast
from geocachingapi.models import GeocachingCache
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import GeocachingDataUpdateCoordinator
# Base class for all platforms
class GeocachingBaseEntity(CoordinatorEntity[GeocachingDataUpdateCoordinator]):
"""Base class for Geocaching sensors."""
_attr_has_entity_name = True
# Base class for cache entities
class GeocachingCacheEntity(GeocachingBaseEntity):
"""Base class for Geocaching cache entities."""
def __init__(
self, coordinator: GeocachingDataUpdateCoordinator, cache: GeocachingCache
) -> None:
"""Initialize the Geocaching cache entity."""
super().__init__(coordinator)
self.cache = cache
# A device can have multiple entities, and for a cache which requires multiple entities we want to group them together.
# Therefore, we create a device for each cache, which holds all related entities.
self._attr_device_info = DeviceInfo(
name=f"Geocache {cache.name}",
identifiers={(DOMAIN, cast(str, cache.reference_code))},
entry_type=DeviceEntryType.SERVICE,
manufacturer=cache.owner.username,
)

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