Compare commits

...

447 Commits

Author SHA1 Message Date
Ludovic BOUÉ bada82fc6e Bump python-roborock to 5.5.1 2026-04-06 16:25:53 +02:00
Erwin Douna 6b2a4df6e0 Bump pyportainer 1.0.36 (#167319) 2026-04-03 21:09:05 +02:00
g4bri3lDev 2ac3979f83 Add opendisplay encryption support (#167251) 2026-04-03 20:11:10 +02:00
Raj Laud 2fb44bce5d Fix victron ble reauth flow title (#167307)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-03 19:48:41 +02:00
MarkGodwin d187e61274 Update to tplink-omada-client 1.5.7 (#167313) 2026-04-03 19:46:53 +02:00
Erwin Douna 7f79da2f75 Add prune volumes button to Portainer (#167314) 2026-04-03 19:45:55 +02:00
Ludovic BOUÉ 4871344138 Add Hisense AC (0x138C/0x0101) to Matter dry and fan mode device lists (#167282)
Co-authored-by: Ludovic BOUÉ <132135057+lboue@users.noreply.github.com>
2026-04-03 19:41:41 +02:00
Ludovic BOUÉ b0ba740024 Matter Pir unoccupied to occupied delay (#162435)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-03 18:44:29 +02:00
epenet b6f49d2063 Refactor None handling in renault diagnostics (#167295) 2026-04-03 18:01:36 +02:00
Franck Nijhof c8e2a2b520 Extract type casting template functions into a type cast Jinja2 extension (#167280) 2026-04-03 18:00:58 +02:00
epenet 27365a4457 Refactor None handling renault device_tracker (#167298) 2026-04-03 17:54:51 +02:00
epenet 4efcb5a700 Use PEP-695 syntax in Renault sensors (#167301) 2026-04-03 17:54:07 +02:00
Denis Shulyaka 4ffc4b8f71 Allow users to overwrite content type for AI task attachments (#167302) 2026-04-03 17:45:00 +02:00
Pete Sage e8fa61ae63 Sonos alarm switch entities may not be created when speaker offline initially (#167303) 2026-04-03 17:42:09 +02:00
Simone Chemelli c6c469cc7a Migrate image unique_id for Fritz (#167209) 2026-04-03 16:39:35 +02:00
Richard Polzer d51afe20e0 Clarify ekeybionyx config flow oauth2 implementation handling (#167169)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-04-03 15:45:06 +02:00
epenet 5dac5ef099 Add missing availability check in device_tracker _async_write_ha_state (#167297) 2026-04-03 15:44:23 +02:00
Pete Sage a1b93b418b Bump soco to 0.30.15 (#167299) 2026-04-03 15:38:59 +02:00
dependabot[bot] 22a96583a9 Bump codecov/codecov-action from 5.5.3 to 6.0.0 (#167267)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 15:22:12 +02:00
Norbert Rittel 01ffa6a676 Improve Recorder action naming consistency (#167244) 2026-04-03 15:21:58 +02:00
epenet abf37849fb Remove unnecessary attribute from Renault sensor entity descriptions (#167268) 2026-04-03 15:21:36 +02:00
Ludovic BOUÉ 72cd7ed178 Fix to allow Matter Fan percent setting to be null when FanMode is Auto (#167279)
Co-authored-by: Ludovic BOUÉ <132135057+lboue@users.noreply.github.com>
2026-04-03 15:21:14 +02:00
Erwin Douna 6305ea8cf2 Bump pyportainer 1.0.35 (#167288) 2026-04-03 15:20:49 +02:00
Ludovic BOUÉ 815c30b213 Fix Matter water heater off mode (#167286)
Co-authored-by: Ludovic BOUÉ <132135057+lboue@users.noreply.github.com>
2026-04-03 15:20:17 +02:00
Joost Lekkerkerker 68cc6df3e0 Make sure we take all Zinvolt battery units in account (#167294) 2026-04-03 15:13:17 +02:00
Norbert Rittel 7f700c891a Improve Media player action naming consistency (#167274)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-03 15:11:35 +02:00
Joost Lekkerkerker e33727a75a Bump Zinvolt to 0.4.1 (#167296) 2026-04-03 15:09:33 +02:00
Bram Kragten 374a050636 Update frontend to 20260325.6 (#167285) 2026-04-03 14:29:17 +02:00
Wendelin 90045e5539 Fix zwave_js subscribe_rebuild_routes_progress initial event (#167178) 2026-04-03 14:18:37 +02:00
Joost Lekkerkerker e30c379979 Bump zinvolt to 0.4.0 (#167276) 2026-04-03 14:11:20 +02:00
Norbert Rittel 86d80c96d7 Improve Assist satellite action naming consistency (#167278) 2026-04-03 12:36:54 +02:00
Jan Bouwhuis 1bbecec991 Fix tuya energy sensor units (#160392) 2026-04-03 11:49:40 +02:00
dotlambda 3970a27369 Bump psutil to 7.2.2 (#167263) 2026-04-03 11:38:19 +02:00
Kevin O'Brien 745107c192 Fix Proxmox VE storage usage percentage crash on missing used_fraction (#167136)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 10:41:09 +02:00
Norbert Rittel 24530d13af Fix spelling of "cannot" in dwd_weather_warnings error string (#167138) 2026-04-03 10:04:32 +02:00
epenet 7529b9252c Remove unnecessary None checks in Renault numbers and binary sensors (#167271) 2026-04-03 10:00:55 +02:00
g4bri3lDev 92a1c4568d Bump py-opendisplay version to 5.9.0 (#167250) 2026-04-03 10:00:51 +02:00
Hai-Nam Nguyen c6527c9f6d Bump hyponcloud to 0.9.3 (#167273) 2026-04-03 10:00:35 +02:00
Andrew Jackson 05b7fa9602 Remove Transmission port forward sensor (#167269) 2026-04-03 09:49:38 +02:00
Joakim Plate 375bd55ae6 Update arcam to 1.8.3 (#167249) 2026-04-02 23:39:00 +02:00
LTek fbd0cb8666 Fix Ring snapshots (#164337)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-04-02 23:37:14 +02:00
Max R 7e061262ad Add pre-commit hook for copilot instructions (#167219) 2026-04-02 22:35:53 +01:00
G Johansson 0e9c17fb16 Bump holiday library to 0.93 (#167217) 2026-04-02 23:32:36 +02:00
G Johansson 940de5ea84 Fix SMHI (#167212) 2026-04-02 23:28:44 +02:00
Marc Mueller bf4773d9bc Fix asyncio loop scopes for pytest fixtures (#166758) 2026-04-02 22:25:17 +02:00
Abílio Costa 0bedcc55ce Set codeowners for agent configurations (#167222) 2026-04-02 19:45:09 +02:00
32u-nd 313f97fc47 Add missing mHz docstrings (#167226) 2026-04-02 19:18:57 +02:00
epenet b7d32e0650 Adjust git commit guidelines for AI agents (#167184) 2026-04-02 17:20:17 +01:00
Pete Sage b9afb2a861 Fix Sonos reporting wrong state when media title is whitespace (#167223) 2026-04-02 17:02:14 +01:00
32u-nd d3a01d4c80 Add millihertz (mHz) to UnitOfFrequency (#167087)
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-04-02 17:27:40 +02:00
Steve Easley 05bd2d05f5 Bump pykaleidescape to v1.1.5 (#167203) 2026-04-02 15:59:01 +02:00
Joost Lekkerkerker 23469d8950 Remove not implemented supported feature from Wiim (#167205) 2026-04-02 15:58:07 +02:00
Erwin Douna 26f677dcd1 Portainer refactor async_setup (#166544) 2026-04-02 15:55:50 +02:00
Stefan Agner 484d9b0cbe Fix test_receive_backup test error when run in isolation (#167204) 2026-04-02 15:36:24 +02:00
tzagim 03ed46aa07 Bump python-telegram-bot to 22.7 (#167062) 2026-04-02 15:14:58 +02:00
Joost Lekkerkerker e5f4000ac2 Add manufacturer to Ecowitt device (#167199) 2026-04-02 14:56:13 +02:00
Norbert Rittel 406598dbfa Fix agreement mismatch and spelling of "cannot" in nmbs (#167137) 2026-04-02 14:47:42 +02:00
epenet 7f23a35155 Migrate qnap to use runtime_data (#167198)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:47:09 +02:00
epenet 0e521eda2e Migrate qbittorrent to use runtime_data (#167196)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:46:28 +02:00
Kurt Chrisford 5a72dc8eca Add exception translations to Actron Air (#167159) 2026-04-02 14:45:42 +02:00
epenet b11292385f Use runtime_data in ovo_energy (#167141)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:45:19 +02:00
epenet 2179a5405a Migrate progettihwsw to use runtime_data (#167157)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:45:08 +02:00
epenet 78f5989cd6 Migrate permobil to use runtime_data (#167170)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:44:35 +02:00
Norbert Rittel cca44c675c Fix spelling of "cannot" in climate exception string (#167139) 2026-04-02 14:41:26 +02:00
Ariel Ebersberger 0ebe65c25b Fix hydrawise crashes when controllers/zones are added (#166708) 2026-04-02 14:32:57 +02:00
dependabot[bot] f7ee95c4b9 Bump sigstore/cosign-installer from 4.1.0 to 4.1.1 (#167156)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-02 14:11:33 +02:00
Erik Montnemery d78c05ab62 Propagate the in_zones attribute from device trackers in person entities (#167192) 2026-04-02 14:09:57 +02:00
epenet 9f41e3341f Migrate peco to use runtime_data (#167147)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:48:58 +02:00
epenet 8ab3d482b9 Migrate prusalink to use runtime_data (#167164)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:48:06 +02:00
epenet a485c3d410 Migrate prosegur to use runtime_data (#167161)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:00:06 +02:00
Erik Montnemery 38b27d624a Add new state attribute in_zones to device_tracker (#166573) 2026-04-02 12:24:35 +02:00
Manu f437d65d3c Remove deprecated LANnouncer integration (#166838) 2026-04-02 12:06:47 +02:00
Erik Montnemery 5ba0764a87 Fix propagation of GPS accuracy in person entity (#167174) 2026-04-02 11:58:48 +02:00
epenet 69fd6532cc Migrate openhome to use runtime_data (#167183)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:51:31 +02:00
epenet ee8bd9f016 Migrate picnic to use runtime_data (#167151)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:48:49 +02:00
epenet de5a2d47a5 Migrate pushbullet to use runtime_data (#167166)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:48:39 +02:00
epenet 54b2e0285c Migrate panasonic_viera to use runtime_data (#167171)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:48:36 +02:00
epenet a0e118d411 Migrate mutesync to use runtime_data (#167180)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-02 11:47:51 +02:00
epenet 07c33233ee Migrate nibe_heatpump to use runtime_data (#167181)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:47:30 +02:00
rrooggiieerr 962cac902b Add Config Flow to Pico TTS (#163114)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Manu <4445816+tr4nt0r@users.noreply.github.com>
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-04-02 11:41:47 +02:00
epenet 9ff5c9863f Migrate openexchangerates to use runtime_data (#167182)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:40:10 +02:00
epenet b60e396241 Migrate pvoutput to use runtime_data (#167167)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:03:16 +02:00
Mike Degatano 6e567ced92 Wrap hassio import in is_hassio check in get_system_info helper (#167111) 2026-04-02 09:05:09 +02:00
Raphael Hehl e1c1e9a8b2 Bump unifi-discovery to version 1.3.0 (#167106) 2026-04-02 00:11:13 +02:00
Norbert Rittel 25b66be84d Fix spelling of "cannot" in two user-facing strings of reolink (#167085) 2026-04-01 22:37:21 +02:00
Jon Culver 4d6a278137 Add Off mode support for water_heater entities in HomeKit (#166836)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 22:29:13 +02:00
Denis Shulyaka 7a77b071a2 Add coordinator to Anthropic for availability check (#164615)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-04-01 22:20:56 +02:00
Norbert Rittel 279c9e71df Improve google_sheets action naming consistency (#167107) 2026-04-01 21:57:55 +02:00
DeerMaximum 2881916c91 Replace NINA attributes with sensors (#161882) 2026-04-01 21:53:28 +02:00
Norbert Rittel f09602363c Improve system_log action naming consistency (#167104) 2026-04-01 21:42:59 +02:00
Norbert Rittel 79b37bff0b Improve shelly action naming consistency (#167102) 2026-04-01 22:15:19 +03:00
Tom 7c549870b5 Add firmware update to Ubiquiti airOS (#166913) 2026-04-01 20:48:06 +02:00
Abílio Costa e50b7f41aa Simplify claude's integrations skill (#166903) 2026-04-01 20:41:35 +02:00
Kevin O'Brien efc8053027 Fix Proxmox VE backup status sensor false positive due to case mismatch (#167069)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 20:32:37 +02:00
Brett Adams d104a1126f Fix Tesla Fleet charge current scope handling (#166919) 2026-04-01 20:26:18 +02:00
Joost Lekkerkerker a573ef4b1c Use subentry helper in WAQI (#167061) 2026-04-01 20:20:33 +02:00
Joost Lekkerkerker 83e8c3fc19 Revert "Pull out Dropbox integration" (#166995) 2026-04-01 20:19:16 +02:00
Abílio Costa cd0ed42941 Make the Claude's GH reviewer skill a subagent (#167065) 2026-04-01 20:16:34 +02:00
Manu 2beca6b322 Fix websocket calling async_release_notes in update component although unavailable (#167067) 2026-04-01 20:10:39 +02:00
Andres Ruiz 0fc62c3150 Add support for energy statistics in waterfurnace integration (#166707)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-04-01 20:03:15 +02:00
johanzander 7daaf3de6a growatt_server: implement reconfiguration flow (Gold) (#165961)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 19:56:53 +02:00
Abílio Costa 6470cbeada Add --draft flag to raise-pull-request agent PR creation command (#167068)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 19:53:33 +02:00
Joost Lekkerkerker 983bade8c5 Bump pySmartThings to 3.7.3 (#167075) 2026-04-01 19:52:05 +02:00
Franck Nijhof 9d27b9290c Merge branch 'master' into dev 2026-04-01 17:50:14 +00:00
Norbert Rittel d9acf64904 Fix one misspelled occurrence of "cannot" in shelly (#167093) 2026-04-01 19:49:38 +02:00
Norbert Rittel cc1114de63 Fix spelling of "cannot" in local_file error string (#167089) 2026-04-01 19:48:54 +02:00
Bram Kragten bff97254d7 Fix select condition state selector (#167064) 2026-04-01 19:41:46 +02:00
Norbert Rittel 6355adc6de Fix spelling of "cannot" in rehlko exception string (#167092) 2026-04-01 19:29:49 +02:00
Norbert Rittel 879d9176bd Fix spelling of "cannot" in azure_storage exception string (#167088) 2026-04-01 19:26:59 +02:00
Norbert Rittel a3badd0a83 Spelling fixes in user-facing strings of wiz (#167091) 2026-04-01 19:24:01 +02:00
Simone Chemelli 73da736ebb Patch the correct socket method in SNMP (#167081) 2026-04-01 18:55:53 +02:00
Norbert Rittel c077538015 Fix spelling of "cannot" in pooldose exception string (#167079) 2026-04-01 18:46:34 +02:00
Norbert Rittel 33bcd710fc Fix spelling of "Cannot reheat …" in kitchen_sink (#167082) 2026-04-01 18:39:46 +02:00
Niracler 6cf264dc18 Mark entity-disabled-by-default as exempt in sunricher_dali (#166861) 2026-04-01 18:17:24 +02:00
Mike O'Driscoll d50d6db1bd Add battery sensors to Casper Glow (#166801) 2026-04-01 18:15:38 +02:00
g4bri3lDev d680c72c7c Add sensor platform for OpenDisplay (#164998)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-04-01 18:01:26 +02:00
Zoltán Farkasdi 49a8c73f72 netatmo: NDB test addition and camera fix (#165375) 2026-04-01 17:47:33 +02:00
Brett Adams dc00fcaf60 Fix Tesla Fleet OAuth scope refresh during reauth (#166920) 2026-04-01 17:47:12 +02:00
Artem Khvastunov b056723b98 Add multi-plane support for Forecast.Solar integration (#160058)
Co-authored-by: Junie <noreply@jb.gg>
Co-authored-by: Junie <junie@jetbrains.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-01 17:42:17 +02:00
CLN 1b4286381d Bump aiounifi to 90 (#166918) 2026-04-01 15:43:20 +01:00
Daniel Jolly 524c2129eb Fix ToDo List Intents item casing (#160177)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
2026-04-01 16:28:59 +02:00
Brett Adams 8fe09e1837 Add Claude Code agent for PR creation (#160759)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-01 15:12:08 +01:00
Marc Mueller 99a186fad7 Fix lingering tasks in condition and trigger tests (#166967) 2026-04-01 16:10:14 +02:00
Retha Runolfsson c1a9f293a7 Add fan speed percentage control to SwitchBot Air Purifier (#166953) 2026-04-01 16:08:40 +02:00
Norbert Rittel 783e2f0a00 Fix spelling of "Shut down" button label in proxmoxve (#167059) 2026-04-01 15:56:04 +02:00
Joost Lekkerkerker 36045c4bd3 Add ConfigEntry method to get subentries by type (#167055)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-04-01 15:18:03 +02:00
Erwin Douna 18cd488622 Hassfest requirements.py optimization (#166514) 2026-04-01 15:00:54 +02:00
epenet ef66446a0d Add entity descriptions to Tuya camera/fan/vacuum (#167056) 2026-04-01 14:46:13 +02:00
Simone Chemelli 4870bb749c 100% coverage of services for Alexa Devices (#165826) 2026-04-01 14:44:15 +02:00
Simone Chemelli 2e2ad0aaec Fix patching for DNS queries in Obihai (#166790) 2026-04-01 14:41:39 +02:00
epenet fa7576dc5a Simplify PLATFORMS patching in Tuya test (#167054) 2026-04-01 14:38:53 +02:00
epenet c6ec90c871 Move OVO Energy DataUpdateCoordinator to separate module (#167048)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:09:29 +02:00
Norbert Rittel c2065f1f14 Fix switch_failed_off exception wording in honeywell (#166987) 2026-04-01 14:06:15 +02:00
Keith Roehrenbeck 5197722733 Add keyboard text input services to Apple TV integration (#165638)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-01 14:05:20 +02:00
David Bonnes 49d63892d1 Validate set_system_mode params in code instead of by schema for Evohome (#165925)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-01 13:59:51 +02:00
Simone Chemelli 713054f9f8 Bump aioamazondevices to 13.3.2 (#167052) 2026-04-01 13:55:23 +02:00
Brett Adams 0b67644b97 Migrate Tessie setup and coordinator to tesla_fleet_api (#167018) 2026-04-01 13:53:08 +02:00
epenet f2001db68c Use runtime_data in octoprint integration (#167028)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 12:00:40 +02:00
Bram Kragten df08d989f2 Update frontend to 20260325.5 (#167050) 2026-04-01 11:59:50 +02:00
epenet d5c7a04751 Use runtime_data in openuv integration (#167029)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-01 11:34:10 +02:00
epenet 3369dfece1 Use runtime_data in ondilo_ico integration (#167039)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:33:57 +02:00
epenet 7268571587 Use runtime_data in opengarage integration (#167040)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:33:55 +02:00
epenet 2341f8dd5a Use runtime_data in osoenergy integration (#167042)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:33:37 +02:00
epenet dd1722b5d6 Use runtime_data in ourgroceries integration (#167043)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:29:50 +02:00
epenet c8667addd8 Use runtime_data in opensky integration (#167041)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:04:22 +02:00
Stefan Agner 3b9a9ca6cb Store received backup in temp backup dir only (#166982) 2026-04-01 09:54:01 +02:00
smarthome-10 52050711a3 Rename component to integration in Pushsafer (#166893)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-01 09:52:32 +02:00
smarthome-10 17abdd02d3 Rename component to integration in Ubiquiti mFi mPort (#166988) 2026-04-01 09:52:11 +02:00
smarthome-10 996f9fdca2 Rename component to integration in Hikvision (#167030) 2026-04-01 09:50:47 +02:00
smarthome-10 a434a0ab90 Rename component to integration in Kodi (#167031)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-01 09:50:29 +02:00
smarthome-10 7bff0e2f3f Rename component to integration in Hyperion (#167032) 2026-04-01 09:50:08 +02:00
smarthome-10 9cf6911b7f Rename component to integration in Radio Thermostat (#167033) 2026-04-01 09:49:34 +02:00
smarthome-10 b0201e893e Rename component to integration in TEMPer (#167034) 2026-04-01 09:49:00 +02:00
smarthome-10 df74d76ff2 Rename component to integration in Aruba (#167035) 2026-04-01 09:48:03 +02:00
epenet 6dc391e169 Use runtime_data in obihai integration (#167037)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-01 09:47:03 +02:00
epenet c7cf78952e Use runtime_data in omnilogic integration (#167038)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-01 09:34:05 +02:00
Oluwatobi Mustapha 2591cf2b3d Migrate google_mail OAuth token refresh exception handling (#165371)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-04-01 09:27:39 +02:00
Brett Adams 2b1c93724f Use Tesla Fleet API for Tessie config flow validation (#167021) 2026-04-01 08:29:07 +02:00
Joost Lekkerkerker 899b776e54 Add BEGA brand (#166992) 2026-04-01 08:27:06 +02:00
Paulus Schoutsen 423b694a0d Bump serialx to 1.1.1 (#167023)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2026-04-01 08:19:34 +02:00
TheJulianJES 01324a84a8 Bump ZHA to 1.1.1 (#167025) 2026-04-01 06:02:21 +02:00
Marc Mueller b63ea35959 Update requests to 2.33.1 (#167014) 2026-04-01 00:32:00 +02:00
J. Nick Koston bb345dfd09 Bump aiohttp to 3.13.5 (#167015) 2026-03-31 12:25:09 -10:00
smarthome-10 c05c2b7f70 Rename component to integration in Start.ca (#166989) 2026-03-31 23:30:29 +02:00
Leon Grave 3d07ec8696 Add freshr reconfiguration flow (#166907) 2026-03-31 23:27:33 +02:00
Marc Mueller 3b396814ae Update mypy to 1.20.0 (#167000) 2026-03-31 23:27:18 +02:00
smarthome-10 b2047c1aca Rename component to integration in SNMP (#166994) 2026-03-31 23:26:11 +02:00
smarthome-10 2b0cff2c93 Rename component to integration in DNS IP (#166993) 2026-03-31 23:24:21 +02:00
smarthome-10 fa7af34678 Rename component to integration in EBox (#166996) 2026-03-31 23:24:19 +02:00
smarthome-10 7563ea6217 Rename component to integration in Bbox (#166998) 2026-03-31 23:24:17 +02:00
smarthome-10 08726af215 Rename component to integration in EBox (#166996) 2026-03-31 23:22:51 +02:00
smarthome-10 4fa1d6b0a1 Rename component to integration in Actiontec (#167004) 2026-03-31 23:22:25 +02:00
smarthome-10 3c86f1eee8 Rename component to integration in Fido (#166997) 2026-03-31 23:22:05 +02:00
smarthome-10 3a63f9fbb1 Rename component to integration in Tomato (#167002) 2026-03-31 23:20:52 +02:00
smarthome-10 7b5408d20c Rename component to integration in Denon Network Receivers (#167006) 2026-03-31 23:19:14 +02:00
potelux 058e8ba455 Add reload service to shell_command (#166557)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-31 23:16:54 +02:00
smarthome-10 bba3c0e6bb Rename component to integration in Denon AVR (#167008) 2026-03-31 23:13:44 +02:00
smarthome-10 a266976c33 Rename component to integration in Edimax (#167011) 2026-03-31 23:12:48 +02:00
smarthome-10 f29c051c73 Rename component to integration in BlinkStick (#167009) 2026-03-31 23:11:02 +02:00
smarthome-10 8842b4840e Rename component to integration in Glances (#167012) 2026-03-31 23:09:00 +02:00
potelux 586d2ceff6 Add reload service to shell_command (#166557)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-31 23:07:26 +02:00
epenet 69a2284a00 Migrate nightscout to use runtime_data (#166927)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 22:48:29 +02:00
Manu 19761a25da Improve strings in HTML5 integration (#166985) 2026-03-31 22:45:32 +02:00
dontinelli e4328fe34d Bump solarlog_cli to 0.7.1 (#166990) 2026-03-31 22:26:03 +02:00
Jackson_57 e91b49e7cd Bump led-ble to 1.1.8 (#166999) 2026-03-31 22:21:39 +02:00
Brett Adams 7d145cd3b8 Add command compatibility scaffold for Tessie migration (#166458) 2026-03-31 21:52:09 +02:00
Denis Shulyaka 962d5386c7 Add diagnostics to Anthropic integration (#166739) 2026-03-31 21:35:09 +02:00
Joost Lekkerkerker 3ba985f771 Pull out Dropbox integration (#166986) 2026-03-31 20:40:04 +02:00
Ariel Ebersberger ef6718c242 Add skeleton with repair issue to bmw integration (#166983)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-31 20:31:45 +02:00
Denis Shulyaka 02bcae00cf Document supported features for Anthropic integration (#166818) 2026-03-31 21:27:12 +03:00
Norbert Rittel d6cd1dffa4 Fix grammar of input_shutdown_failure error in victron_ble (#166972) 2026-03-31 20:00:37 +02:00
Joost Lekkerkerker fc32f0dbd3 Make sure we can fetch player stats in Chess.com (#166980) 2026-03-31 19:59:28 +02:00
Manu cda1974e40 Add html5.dismiss_message action to HTML5 integration (#166909) 2026-03-31 19:02:58 +02:00
epenet 5425e82fb4 Migrate nuheat to use runtime_data (#166937)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:55:47 +01:00
Claw Explorer 84f36b0d4d Migrate tilt_ble to use runtime_data (#166663) 2026-03-31 18:33:48 +02:00
Bram Kragten 0807525e1b Update frontend to 20260325.4 (#166970) 2026-03-31 18:26:51 +02:00
Erik Montnemery 73a86b8606 Remove redundant field descriptions from triggers and conditions (#166955) 2026-03-31 17:46:07 +02:00
Erik Montnemery b8652e70e5 Remove calendar and todo from unconditionally loaded integrations (#166951)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
2026-03-31 17:39:42 +02:00
Marc Mueller a3f3b0bed4 Fix lingering tasks in update_coordinator test (#166968) 2026-03-31 16:21:26 +02:00
prpr19xx daaa68ce22 London Underground integration: Add Tram and IFS Cloud Cable Car status (#166712) 2026-03-31 16:01:21 +02:00
Branden Cash 9ada10e0cf Bump srpenergy to 1.3.8 (#166926) 2026-03-31 15:48:48 +02:00
Andrew Jackson 35287c381b Bump aiomealie to 1.2.3 (#166942) 2026-03-31 15:42:14 +02:00
bkobus-bbx 2ff84b633c Add myself to blebox codeowners (#166966) 2026-03-31 15:38:39 +02:00
Andrew Jackson c09d91765f Bump aiomealie to 1.2.3 (#166942) 2026-03-31 15:36:23 +02:00
Manu ac6ddf32c8 Fix StopIteration error in ista EcoTrend coordinator (#166929) 2026-03-31 15:35:17 +02:00
Paul Bottein f15d9e5956 Fix Shutdown grammar in Synology DSM strings (#166946) 2026-03-31 15:32:07 +02:00
Paul Bottein f95601a2e7 Fix "Shutdown" grammar in Roborock strings (#166948)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:26:21 +02:00
Snuffy2 0aef0cc121 Add integration_type to opnsense (#166965) 2026-03-31 15:19:35 +02:00
Marc Mueller d1bfd94d33 Shutdown debouncer in tests (#166958) 2026-03-31 14:51:55 +02:00
Marc Mueller 8a9c0f4fde Fix lingering tasks in nest tests (#166959)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-31 14:49:54 +02:00
epenet 3596771af1 Migrate nzbget to use runtime_data (#166947) 2026-03-31 14:10:57 +02:00
epenet 7b9b457f15 Migrate nuki to use runtime_data (#166943) 2026-03-31 13:55:19 +02:00
Simone Chemelli cb8597d62f Improve SNMP tests and avoid dns lookups (#166604) 2026-03-31 12:54:40 +01:00
Marc Mueller c82cfaf633 Cancel brands rotate_token on shutdown (#166957) 2026-03-31 13:51:42 +02:00
Erik Montnemery 80802c9997 Update hassfest conditions, services and triggers plugins to not require field descriptions (#166954)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-31 12:29:05 +01:00
Franck Nijhof 971579f021 Improve datetime action naming consistency (#166530) 2026-03-31 12:01:32 +01:00
Franck Nijhof af6b8d4f66 Improve date action naming consistency (#166529) 2026-03-31 12:01:20 +01:00
Andreas Jakl e9a61963f2 Prevent invalid phase count state in nrgkick (#166575) 2026-03-31 11:55:04 +01:00
Erik Montnemery b350712f9e Add last_non_buffering_state media_player state attribute (#166941) 2026-03-31 12:22:13 +02:00
Alex Barcelo 51785f10c1 Adjust Thread network diagnostics prefixes to include double colon (#166520)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-31 12:19:07 +02:00
Artur Pragacz 24e0627b41 Register condition platform upon use (#166939) 2026-03-31 11:53:36 +02:00
Artur Pragacz 6c453c8b49 Register trigger platform upon use (#166911) 2026-03-31 11:49:38 +02:00
TheJulianJES 904a2d1b4d Remove invalid Matter HeatingCoolingUnit device type (#166828) 2026-03-31 11:37:49 +02:00
epenet f3b64dcbe0 Migrate nobo_hub to use runtime_data (#166934)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 11:16:19 +02:00
Franck Nijhof 0edc2cbbab Improve time action naming consistency (#166532) 2026-03-31 11:16:18 +02:00
epenet 751f06eb58 Migrate nmap_tracker to use runtime_data (#166932)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 11:11:12 +02:00
epenet 9bfac71bd7 Migrate netatmo to use runtime_data (#166925)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 11:10:50 +02:00
pedroterzero 9499476940 Add water_full fault sensor for D825A dehumidifier (#166847) 2026-03-31 11:10:39 +02:00
epenet eda1eb2e35 Migrate notion to use runtime_data (#166936)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 11:05:18 +02:00
Erik Montnemery 075e179972 Make field description optional for non config flows (#166892) 2026-03-31 09:59:23 +01:00
Robert Resch 99e8066607 Use async download for translations (#166940) 2026-03-31 10:10:41 +02:00
epenet 7ce32f0668 Remove unused hass.data[DOMAIN] in nfandroidtv (#166931)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 09:27:49 +02:00
Artur Pragacz dc5547d7b6 Unprefix entity name for template function (#166899) 2026-03-31 09:08:21 +02:00
Artur Pragacz de98bc7dcf Unprefix entity name for entity ID generation (#166900) 2026-03-31 09:05:39 +02:00
dependabot[bot] a71d48085a Bump j178/prek-action from 2.0.0 to 2.0.1 (#166924)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 08:45:54 +02:00
Brett Adams 9e20a13936 Fix Tesla Fleet startup scopes after OAuth refresh (#166922) 2026-03-31 07:53:15 +02:00
Mike Degatano e164e65217 Use aiohasupervisor for all Supervisor service calls (#166558) 2026-03-31 07:35:41 +02:00
Manu 07998de35e Bump aiontfy to 0.8.4 (#166917) 2026-03-31 01:05:37 +02:00
smarthome-10 5253dc11dc Rename component to integration in Linksys Smart Wi-Fi (#166885) 2026-03-30 22:42:00 +01:00
smarthome-10 3f9022cd53 Rename component to integration in Arris TG2492LG (#166883) 2026-03-30 23:23:27 +02:00
Leon Grave 073f498c75 Add freshr diagnostics (#166912) 2026-03-30 23:16:00 +02:00
reneboer c5b24e9470 Update datetime selector in Renault ac_start action (#166860) 2026-03-30 22:34:51 +02:00
smarthome-10 c12b7bfd18 Rename component to integration in Bitcoin (#166882) 2026-03-30 20:41:26 +01:00
smarthome-10 1c2f583587 Rename component to integration in FortiOS (#166887)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-30 20:33:06 +01:00
Raj Laud 58a376e68b Bump victron-ble-ha-parser (#166906) 2026-03-30 20:23:22 +01:00
Jan Bouwhuis 78b251e7cb Add clean segment support to MQTT vacuum entities (#166794) 2026-03-30 21:20:17 +02:00
Abílio Costa a2c65b9126 Remove checkout requirement from PR review skill (#166902) 2026-03-30 19:12:59 +01:00
Denis Shulyaka 5e443681c3 Add troubleshooting documentation for Anthropic integration (#166766) 2026-03-30 20:10:49 +02:00
smarthome-10 13756863f1 Rename component to integration in Fail2Ban (#166901) 2026-03-30 20:08:56 +02:00
Raphael Hehl fd54e45aeb Add dynamic device support for UniFi Access door platforms (#166793) 2026-03-30 19:51:05 +02:00
Manu 52af74c3b6 Add entity action html5.send_message to HTML5 integration (#166349)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-30 19:49:59 +02:00
Denis Shulyaka dc111a475e Add support for web search dynamic filtering for Anthropic (#164116) 2026-03-30 19:40:56 +02:00
Chase 14cb42349a OpenRouter: Add WebSearch Support (#164293)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-30 19:40:02 +02:00
Raphael Hehl c42b50418e Add stale device removal support to UniFi Access (#166792)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-03-30 19:19:20 +02:00
AlCalzone 501b4e6efb Convert Z-Wave Opening state to separate Open/Closed and Tilted sensors (#166635)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-30 19:17:05 +02:00
smarthome-10 ca2099b165 Rename component to integration in Panasonic Blu-Ray (#166890)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-30 18:13:17 +02:00
smarthome-10 69b55c295d Rename component to integration in OhmConnect (#166881) 2026-03-30 17:47:38 +02:00
smarthome-10 13709b1c90 Rename component to integration in Sky Hub (#166888) 2026-03-30 17:45:18 +02:00
smarthome-10 2c013777db Rename component to integration in Opple (#166891) 2026-03-30 17:43:56 +02:00
Raphael Hehl 91099ea489 Update UniFi Access quality scale: mark fulfilled Gold rules (#166789)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-30 17:19:07 +02:00
Michal Čihař 70cea66e5b Skip unavailable sensors in LaCrosse View (#166859) 2026-03-30 17:03:21 +02:00
Taylor Wilsdon e78bb97e84 Support vacation mode in Econet (#166659) 2026-03-30 16:58:11 +02:00
Robert Svensson 732b170190 Introduce per-source DataUpdateCoordinator for UniFi polling data sources (#166806) 2026-03-30 16:48:18 +02:00
Raphael Hehl 0a05993a4e Unifi Access add reconfiguration flow and refactor validation logic (#166812)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-03-30 16:44:12 +02:00
Abílio Costa 42c3610685 Add counter purpose-specific condition (#166879) 2026-03-30 16:41:08 +02:00
Raphael Hehl 4ad73da7ec Add strict typing to UniFi Access integration (#166787) 2026-03-30 16:36:07 +02:00
hanwg 0d14bdab24 Fix webhook leak for Telegram bot (#166776) 2026-03-30 16:29:28 +02:00
Denis Shulyaka 157362f225 Fix OpenAI image generation with reasoning (#166827) 2026-03-30 16:27:39 +02:00
Manu 1aa380fdfa Add tr4nt0r as codeowner to html5 integration (#166771) 2026-03-30 10:25:10 -04:00
Jan Bouwhuis 9348948afa Add attribute group_entities to the list of blocked MQTT entity attributes (#165360) 2026-03-30 16:21:02 +02:00
Jan Bouwhuis 14b9915914 Add repair flow when MQTT YAML config is present but the broker is not set up correctly (#165090)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-30 16:16:31 +02:00
smarthome-10 607462028b Rename component to integration in Thomson (#166880) 2026-03-30 16:08:03 +02:00
epenet 8c07348a3d Migrate neato to use runtime_data (#166854)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 16:03:43 +02:00
epenet cda52af178 Migrate motioneye to use runtime_data (#166848)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 15:56:08 +02:00
Tom Matheussen d1ccda18f7 Skip unchanged connection check on reconfigure flow for Satel Integra (#166695)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-30 15:52:11 +02:00
Franck Nijhof 9fb0b69f0a Improve text action naming consistency (#166523)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2026-03-30 15:42:31 +02:00
Paul Bottein f0848edea9 Use translation key and icons.json for Synology DSM button entities (#166862) 2026-03-30 15:23:49 +02:00
Mike O'Driscoll 5be12a213d Bump pycasperglow to 1.2.0 (#166791) 2026-03-30 15:03:40 +02:00
mettolen 20b284d0e9 Fix Huum exception translations (#166778)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-30 14:55:45 +02:00
Lorenzo Gasparini 49c3376c95 Bump fing_agent_api to 1.1.0 (#166855) 2026-03-30 14:33:00 +02:00
Joost Lekkerkerker 174b5f5593 Get list of analytics insights integrations from next environment (#166867) 2026-03-30 14:29:25 +02:00
epenet b38e41a34a Refactor Tuya device diagnostics (#166846) 2026-03-30 14:01:18 +02:00
epenet b6350478a5 Migrate meteo_france to use runtime_data (#166852)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 13:48:01 +02:00
Erik Montnemery b75af6d84a Mark Entity.async_write_ha_state as final (#166627) 2026-03-30 13:21:45 +02:00
Ariel Ebersberger 194485d863 Fix shelly tests - mock async_unload_entry (#166851) 2026-03-30 13:19:52 +02:00
Raphael Hehl d6458bc574 Add diagnostics support to UniFi Access integration (#166819)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-30 12:39:38 +02:00
Mike O'Driscoll 434f1dca2c Add diagnostics to Casper Glow (#166807) 2026-03-30 12:38:28 +02:00
Florian c6ad6da6ae Clamp surepetcare battery percentage to 0-100 (#166824)
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-30 12:34:38 +02:00
epenet be3d65538d Use runtime_data in motion_blinds integration (#166849)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 12:32:27 +02:00
Michael 297e9e265a Add valve.opened and valve.closed triggers (#165160) 2026-03-30 12:06:43 +02:00
Simone Chemelli 119dfbddea Update quality scale for Fritz (#166853) 2026-03-30 11:32:16 +02:00
Jeef 970925141e Bump weatherflow4py to 1.5.2 (#166773) 2026-03-30 10:54:17 +02:00
Matthias Alphart 51131beaec Update knx-frontend to 2026.3.28.223133 (#166764) 2026-03-30 10:44:16 +02:00
Manu c509226d17 Remove unused string from HTML5 integration (#166826) 2026-03-30 09:03:37 +02:00
epenet 067a9a0c25 Bump tuya-device-handlers to 0.0.16 (#166844) 2026-03-30 08:51:50 +02:00
pedroterzero d10197d535 Add fixture for Tuya D825A dehumidifier (#166822) 2026-03-30 07:12:57 +02:00
Raman Varabets 8978d197ca Allow Matter thermostats with null LocalTemperature (#162973)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2026-03-30 04:41:11 +02:00
Mika afc73fdcfd Bump aiosolaredge to 1.0.2 (#166763) 2026-03-30 04:07:01 +02:00
Jeff Terrace 31a24446a8 Rename onvif event module to event_manager (#166830) 2026-03-29 14:05:05 -10:00
Jeff Terrace e80caaa7cd Remove hunterjm@ as an owner of onvif (#166823) 2026-03-29 18:08:53 -04:00
mletenay 2b3a504a05 Update goodwe library to 0.4.10 (#166809) 2026-03-29 20:39:36 +02:00
Artur Pragacz a93229bd32 Cancel wait_for_started task in Onkyo (#166762) 2026-03-29 18:17:01 +02:00
DevHugo 99306a75d3 Bump youtubeaio to 2.1.2 (#166767) 2026-03-29 08:14:51 +02:00
Manu 3a761116e4 Bump aiontfy to 0.8.3 (#166770) 2026-03-29 08:14:19 +02:00
Manu a6ec59d6a5 Bump habiticalib to 0.4.7 (#166772) 2026-03-29 08:13:31 +02:00
Jan Bouwhuis ca51123115 Revert mqtt vacuum segments support (#166761) 2026-03-28 21:59:36 +01:00
J. Nick Koston cfc58bd415 Bump aiohttp to 3.13.4 (#166756) 2026-03-28 21:22:30 +01:00
jtjart a18f3cba32 Add config flow to pjlink (#166073)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-28 19:58:00 +01:00
Denis Shulyaka 6218741602 Document use cases for Anthropic integration (#166752) 2026-03-28 19:44:54 +01:00
Mike O'Driscoll 2285db5bb1 Casper Glow - Add Select Options (#166553)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-28 17:48:22 +01:00
Manu 738b85c17d Add event platform to HTML5 integration (#166577) 2026-03-28 17:39:21 +01:00
Erwin Douna b7bb185d50 Add new OAuth exceptions to Neato (#166584)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-28 17:27:09 +01:00
mettolen f4544cf952 Fix Huum test coverage and upgrade to silver (#166548)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-28 17:26:18 +01:00
crash0verride11 beab473dcc Correct Musiccast sound mode name (#166644)
Co-authored-by: crash0verride11 <3526616+crash0verride11@users.noreply.github.com>
Co-authored-by: jtjart <80978647+jtjart@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-28 17:23:57 +01:00
Anis Kadri 96891228c9 Add select platform to UniFi Access integration (#166096)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-03-28 17:19:49 +01:00
Will Moss a4a36b5cbd Handle Oauth2 ImplementationUnavailableError in microbees (#166654)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 17:18:05 +01:00
David Knowles 4a0a400e22 Bump pydrawise to 2026.3.0 (#166750) 2026-03-28 17:12:24 +01:00
Andrew Jackson fbe4195ae0 Add event entity to Transmission (#166686) 2026-03-28 17:06:11 +01:00
Will Moss 116fa57903 Handle Oauth2 ImplementationUnavailableError in monzo (#166653)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 16:56:39 +01:00
Will Moss 2399da93db Handle Oauth2 ImplementationUnavailableError in google_assistant_sdk (#166649)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 16:55:55 +01:00
Will Moss 3850bb0e57 Handle Oauth2 ImplementationUnavailableError in google_mail (#166650)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 16:55:24 +01:00
Will Moss f45c84b2a8 Handle Oauth2 ImplementationUnavailableError in iotty (#166652)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 16:54:00 +01:00
Will Moss a2e60f84da Handle Oauth2 ImplementationUnavailableError in google_sheets (#166651)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 16:53:47 +01:00
Will Moss 3757289c73 Handle Oauth2 ImplementationUnavailableError in geocaching (#166648)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 16:53:20 +01:00
Will Moss 09067a18b7 Handle Oauth2 ImplementationUnavailableError in husqvarna_automower (#166633)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 16:52:42 +01:00
Will Moss 6eb834946b Handle Oauth2 ImplementationUnavailableError in lyric (#166655)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 16:51:48 +01:00
Will Moss 0e1663f259 Handle Oauth2 ImplementationUnavailableError in gentex_homelink (#166646)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 16:51:09 +01:00
Will Moss 0ba3a94a3b Handle Oauth2 ImplementationUnavailableError in google_tasks (#166657)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 16:50:01 +01:00
Martin Hjelmare 3562a3800f Improve energyid config flow tests (#166749) 2026-03-28 16:46:49 +01:00
Michael de0efa1639 Bump aioimmich to 0.12.1 (#166746) 2026-03-28 15:50:26 +01:00
Mattie 818cf41c22 Bump python-qube-heatpump to 1.8.0 (#166713) 2026-03-28 15:49:24 +01:00
Denis Shulyaka 25bfb16936 Exception translations for Anthropic integration (#166723) 2026-03-28 15:40:03 +01:00
Raman Gupta 75782e6f17 Remove dispatcher pattern and use options properties in Vizio (#164711)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 15:38:59 +01:00
Åke Strandberg 3e5c291338 Add missing code for miele washing machine (#166731) 2026-03-28 15:27:06 +01:00
Louis Christ 30163fa2e7 Bump pyblu to 2.0.6 (#166738) 2026-03-28 15:26:35 +01:00
Steve Easley 16231d8d36 Bump kaleidescape dependency to 1.1.4 (#166744) 2026-03-28 15:21:26 +01:00
Ludovic BOUÉ 0c0d6595d6 Add Matter range hood fixture (#166743)
Co-authored-by: Ludovic BOUÉ <132135057+lboue@users.noreply.github.com>
2026-03-28 15:20:51 +01:00
Martin Hjelmare a443060faa Improve comelit type handling (#166740) 2026-03-28 15:20:23 +01:00
Noah Husby 9807722077 Bump aiorussound to 4.9.1 (#166718) 2026-03-28 11:15:29 +01:00
TimL 12b485b17e Add Remote platform to SMLIGHT Integration (#166728) 2026-03-28 07:50:36 +01:00
Joakim Plate 45def46a45 Bump gardena bluetooth to 2.3.0 (#166719) 2026-03-28 00:57:27 +01:00
Martin Hjelmare 685b921fe7 Update switchbot_cloud snapshots (#166720) 2026-03-27 18:54:55 -04:00
Paul Bottein b813aa213f Update frontend to 20260325.2 (#166717) 2026-03-27 22:45:11 +01:00
Ludovic BOUÉ 79ec3ff484 Add Matter Thermostat presets feature (#160885)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
Co-authored-by: Ludovic BOUÉ <132135057+lboue@users.noreply.github.com>
2026-03-27 22:39:15 +01:00
reneboer 63ba49ce4c Add start_charge action to renault (#166701)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2026-03-27 22:31:48 +01:00
Samuel Xiao 85c7bf1dff Add new Weather Station sensors to Switchbot Cloud (#165257) 2026-03-27 19:14:13 +00:00
Artur Pragacz 894e9bab0a Use legacy naming for entities (#166696) 2026-03-27 19:45:39 +01:00
Will Moss b39c83efd2 Handle Oauth2 ImplementationUnavailableError in google (#166647)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 18:12:55 +00:00
DeerMaximum e855b92b82 Introduce a base entity for NINA (#166637) 2026-03-27 17:19:30 +01:00
Norbert Rittel 30ee28a0d3 Improve timer action naming consistency (#166682) 2026-03-27 15:43:51 +00:00
Åke Strandberg 78f6b934bb Add missing miele program_id code (#166685) 2026-03-27 16:14:35 +01:00
Erik Montnemery fbef3b27bd Add timer conditions (#166641)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-27 15:39:10 +01:00
Abílio Costa 646f56d015 Reduce code duplication in todo triggers (#166640)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-27 14:35:29 +00:00
Åke Strandberg f82d21886a Add missing miele oven codes (#166690) 2026-03-27 15:02:04 +01:00
Erik Montnemery f5054d41e1 Add calendar conditions (#166643) 2026-03-27 14:15:41 +01:00
Allen Porter 53f64bff49 Add client_id_metadata_document_supported to the OAuth Authorization Server Metadata (#166220) 2026-03-27 08:51:24 -04:00
Abílio Costa 65cb9b8528 Update idasen-ha to 2.6.5 (#166645) 2026-03-27 11:05:58 +00:00
Will Moss ecd16d759a Handle Oauth2 ImplementationUnavailableError in smappee (#166660)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 11:27:58 +01:00
LG-ThinQ-Integration 8498e2a715 Bump thinqconnect to 1.0.11 (#166668)
Co-authored-by: YunseonPark-LGE <yunseon.park@lge.com>
2026-03-27 11:24:04 +01:00
Erik Montnemery 4fa4ba5ad0 Add select conditions (#166612) 2026-03-27 10:48:20 +01:00
Erik Montnemery a953b697ce Add valve conditions (#166634) 2026-03-27 10:22:31 +01:00
Artur Pragacz c543743245 Wait for device registry in entity registry loading (#166636) 2026-03-27 09:51:50 +01:00
Simone Chemelli 5b76fab646 Bump aioamazondevices to 13.3.1 (#166658) 2026-03-27 08:51:39 +01:00
Simone Chemelli 6153705b61 Improve Obihai tests and avoid dns lookups (#166510) 2026-03-27 08:50:26 +01:00
Erik Montnemery 8632420b8f Add weather support to humidity conditions (#166599) 2026-03-27 07:48:14 +01:00
Erik Montnemery 4f89715453 Fix override of state write in calendar base entity (#166625) 2026-03-27 07:40:28 +01:00
Ariel Ebersberger 8ca8c2191f Modernize demo/remote to async (#166624) 2026-03-27 07:08:58 +01:00
Will Moss cb43950ccf Use error introduced in #154579 in mcp integration (#166661)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 20:45:23 -07:00
Will Moss ddfef18183 Use error introduced in #154579 in google_photos integration (#166656)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-03-26 20:45:04 -07:00
Will Moss ac65ba7d20 Use error introduced in #154579 in fitbit integration (#166632)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 20:43:23 -07:00
Erik Montnemery d76272d74a Fix override of state write in camera base entity (#166626) 2026-03-26 22:00:25 +01:00
Erik Montnemery 8e5daeb7dd Fix override of state write in fritzbox (#166629) 2026-03-26 21:56:23 +01:00
Will Moss 5d7abae490 Handle Oauth2 ImplementationUnavailableError in aladdin_connect (#166631)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 20:37:47 +00:00
Bram Kragten f875c77af0 Update frontend to 20260325.1 (#166614) 2026-03-26 20:43:39 +01:00
Erik Montnemery c00a68383c Fix override of state write in radarr (#166630) 2026-03-26 20:39:43 +01:00
Erik Montnemery 5544157d5e Fix override of state write in dlna_dmr (#166628) 2026-03-26 20:29:49 +01:00
Ariel Ebersberger 70aa58913d Modernize demo/switch to async (#166619) 2026-03-26 19:52:37 +01:00
Jamie Magee cc363e4ebd Remove tplink_lte integration (#166615) 2026-03-26 18:47:39 +00:00
Daniel Nicoara 8d28b399b0 Add Matter radon sensor support (#166298)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2026-03-26 19:46:58 +01:00
Alessio Magliarella fe76fe5408 Bump ttn_client from 1.2.3 to 1.3.0 (#166613) 2026-03-26 17:35:48 +00:00
Erik Montnemery a7de418213 Add light.is_brightness condition (#166601) 2026-03-26 17:58:44 +01:00
Andres Ruiz e359a8952b Add support for unloading the waterfurnace config (#166555) 2026-03-26 17:34:52 +01:00
Tom 0a9d4ef138 Verify Proxmox permissions when creating snapshots (#166547) 2026-03-26 17:21:30 +01:00
Andres Ruiz 5620cfbfd8 Add support for unloading the waterfurnace config (#166555) 2026-03-26 17:16:38 +01:00
Erik Montnemery fb65cf48c9 Add condition humidifier.is_mode (#166610) 2026-03-26 17:14:11 +01:00
Norbert Rittel 7fd7b2c203 Make siren conditions consistent with new wording (#166600) 2026-03-26 17:06:40 +01:00
Erik Montnemery 69e691f042 Add input_boolean support to switch conditions (#166602) 2026-03-26 16:51:51 +01:00
Erik Montnemery f690e6de6a Restore support for number entities as limits in moisture conditions and triggers (#166608) 2026-03-26 16:42:51 +01:00
Erik Montnemery ee3c2e6f80 Restore support for number entities as limits in battery conditions and triggers (#166607) 2026-03-26 16:35:59 +01:00
Erik Montnemery 5ffe301384 Add climate.is_hvac_mode condition (#166570) 2026-03-26 16:24:27 +01:00
Erik Montnemery e5ad6092d1 Remove number entity support from illuminance triggers and conditions (#166595) 2026-03-26 16:08:28 +01:00
Erik Montnemery bd79958d10 Remove number entity support from power triggers and conditions (#166597) 2026-03-26 16:05:21 +01:00
hanwg fe485f853f Add missing translations for Telegram bot (#166581)
Co-authored-by: Robert Resch <robert@resch.dev>
2026-03-26 16:03:21 +01:00
Robert Resch 3c67c6087a Create IntegrationType enum (#166598) 2026-03-26 15:53:57 +01:00
Erwin Douna cb7f9b5f49 Google Assistant SDK add new OAuth exceptions (#166587) 2026-03-26 15:53:12 +01:00
Erik Montnemery 2547563e8c Remove number entity support from humidity triggers and conditions (#166594) 2026-03-26 15:49:40 +01:00
Erwin Douna 213b370693 Add new OAuth exceptions to Netatmo (#166585) 2026-03-26 15:43:13 +01:00
Robin Thoni 2c9ecb394d Bump sfrbox-api to 0.1.1 (#166605) 2026-03-26 15:24:22 +01:00
Simone Chemelli 51a5f5793f Improve Nuki tests and avoid dns lookups (#166506) 2026-03-26 15:12:17 +01:00
Erik Montnemery 33f11f2263 Remove number entity support from battery triggers and conditions (#166593) 2026-03-26 14:46:39 +01:00
Erik Montnemery 45069b623c Remove number entity support from moisture triggers and conditions (#166596) 2026-03-26 14:40:56 +01:00
Abílio Costa 5defb4dbff Add todo to experimental triggers (#166591) 2026-03-26 14:36:16 +01:00
Ronald van der Meer bc7c3f0617 Bump pooldose 0.9.0 (#166589) 2026-03-26 14:32:52 +01:00
Devin Slick 704c0d1eb0 Bump lojack-api to 0.7.2 (#166560)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:15:04 +01:00
John Meyers 6c864a1725 Update rainmachine solar radiation to reflect it is per day, not per … (#166040) 2026-03-26 14:11:12 +01:00
reneboer 299c6556bb Bump renault-api to 0.5.7 (#166586) 2026-03-26 13:16:50 +01:00
Erik Montnemery f0fc98cb66 Remove class NumericalDomainSpec (#166588) 2026-03-26 13:13:07 +01:00
Ariel Ebersberger cd63d14e6f Add battery triggers (#166258) 2026-03-26 11:51:49 +01:00
Simone Chemelli 30dfd23da8 Improve MySensors tests and avoid dns lookups (#166509) 2026-03-26 11:51:45 +01:00
AlCalzone d39ef523b8 Revert: Create repair issue for legacy Z-Wave Door state sensors that are still in use (#166583) 2026-03-26 11:45:34 +01:00
Erik Montnemery b6c2fbb8c0 Adjust some trigger and condition schemas (#166568) 2026-03-26 11:32:39 +01:00
tronikos 758d5469aa Add Google Drive backup upload progress (#166549) 2026-03-26 10:31:07 +01:00
Keilin Bickar ea99f88d10 Bump sense-energy to 0.14.0 (#166550) 2026-03-26 10:27:02 +01:00
Keilin Bickar 0a8f76864c Bump asyncsleepiq to 1.7.1 (#166552) 2026-03-26 10:25:29 +01:00
Erik Montnemery ad522d723c Add trigger humidifier.mode_changed (#166241)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2026-03-26 10:03:49 +01:00
dependabot[bot] 0f41a311c8 Bump dawidd6/action-download-artifact from 16 to 19 (#166564) 2026-03-26 07:45:40 +01:00
Fabian Munkes 412a9a050e Bump music-assistant-client to 1.3.4 (#166567) 2026-03-26 07:45:05 +01:00
dependabot[bot] d5efc3abd5 Bump actions/cache from 5.0.3 to 5.0.4 (#166563) 2026-03-26 07:41:07 +01:00
dependabot[bot] a205623d52 Bump codecov/codecov-action from 5.5.2 to 5.5.3 (#166562) 2026-03-26 07:38:31 +01:00
dependabot[bot] 8208eecf8c Bump j178/prek-action from 1.1.1 to 2.0.0 (#166561) 2026-03-26 07:37:25 +01:00
Erik Montnemery f84398eb9c Speed up trigger tests (#166522) 2026-03-26 00:51:14 +01:00
Franck Nijhof aca5adb673 Improve conversation action naming consistency (#166542) 2026-03-26 00:34:22 +01:00
Franck Nijhof f361d01b8b Improve dashboard action naming consistency (#166539) 2026-03-26 00:34:08 +01:00
Franck Nijhof d2cef2d26e Improve cloud action naming consistency (#166516) 2026-03-26 00:33:48 +01:00
Abílio Costa 90524e53ec Revert "Instruct copilot to place main comment in collapsible section" (#166543) 2026-03-25 22:15:21 +00:00
Franck Nijhof 668d220400 Improve script action naming consistency (#166517) 2026-03-25 22:14:19 +00:00
Franck Nijhof 9e28db0535 Improve valve action naming consistency (#166521) 2026-03-25 22:13:56 +00:00
Franck Nijhof c5807463fd Improve humidifier action naming consistency (#166524) 2026-03-25 22:13:12 +00:00
Franck Nijhof f72a9e52f5 Improve counter action naming consistency (#166526) 2026-03-25 22:11:16 +00:00
Franck Nijhof 619582bd03 Improve image action naming consistency (#166527) 2026-03-25 22:10:50 +00:00
Franck Nijhof bcc02d7adc Improve automation action naming consistency (#166525) 2026-03-25 22:08:49 +00:00
Franck Nijhof a9083d5362 Improve weather action naming consistency (#166540) 2026-03-25 22:08:29 +00:00
Franck Nijhof dd89fa0f5b Improve device tracker action naming consistency (#166534) 2026-03-25 22:04:37 +00:00
Franck Nijhof 88d0bd5a1d Improve group action naming consistency (#166537) 2026-03-25 22:03:15 +00:00
Franck Nijhof a045c2907f Improve logger action naming consistency (#166538) 2026-03-25 22:02:16 +00:00
Franck Nijhof bcca7655f8 Improve water heater action naming consistency (#166535)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-25 22:01:53 +00:00
Jordan Harvey 269ef5f824 Bump pyanglianwater to 3.1.2 (#166531) 2026-03-25 22:33:24 +01:00
Erik Montnemery c80a9aab71 Add trigger water_heater.operation_mode_changed (#166450) 2026-03-25 21:54:34 +01:00
balloob-travel 33180a658a Validate port ranges in URL validator (#166059)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-25 21:44:30 +01:00
Erik Montnemery c5955ada1a Use NumericThresholdSelector in numeric conditions (#166507) 2026-03-25 20:57:12 +01:00
Abílio Costa fd7d936a0d Instruct copilot to place main comment in collapsible section (#166503) 2026-03-25 20:45:39 +01:00
Franck Nijhof 84cd137bae Bump version to 2026.5.0dev0 (#166512) 2026-03-25 20:24:07 +01:00
johanzander 3a77a638d5 growatt_server: use human-readable labels in exception messages (#166024)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2026-03-25 20:00:47 +01:00
Christian Lackas 599f4f01d0 Add HmIP-FLC support to HomematicIP Cloud (#165827) 2026-03-25 19:58:18 +01:00
Joakim Plate bd298e92d0 Rework patching and handling of client runner in arcam (#165747) 2026-03-25 19:55:59 +01:00
Leon Grave fabbfd93df Add dynamic devices to freshr (#165942) 2026-03-25 19:49:08 +01:00
Simone Chemelli 1ecbc44368 Improve KNX tests and avoid dns lookups (#166508) 2026-03-25 19:47:57 +01:00
953 changed files with 33245 additions and 18267 deletions
@@ -1,18 +1,11 @@
---
name: github-pr-reviewer
description: Review a GitHub pull request and provide feedback comments. Use when the user says "review the current PR" or asks to review a specific PR.
description: Reviews GitHub pull requests and provides feedback comments.
disallowedTools: Write, Edit
---
# Review GitHub Pull Request
## Preparation:
- Check if the local commit matches the last one in the PR. If not, checkout the PR locally using 'gh pr checkout'.
- CRITICAL: If 'gh pr checkout' fails for ANY reason, you MUST immediately STOP.
- Do NOT attempt any workarounds.
- Do NOT proceed with the review.
- ALERT about the failure and WAIT for instructions.
- This is a hard requirement - no exceptions.
## Follow these steps:
1. Use 'gh pr view' to get the PR details and description.
2. Use 'gh pr diff' to see all the changes in the PR.
+229
View File
@@ -0,0 +1,229 @@
---
name: raise-pull-request
description: |
Use this agent when creating a pull request for the Home Assistant core repository after completing implementation work. This agent automates the PR creation process including running tests, formatting checks, and proper checkbox handling.
model: inherit
color: green
tools: Read, Bash, Grep, Glob
---
You are an expert at creating pull requests for the Home Assistant core repository. You will automate the PR creation process with proper verification, formatting, testing, and checkbox handling.
**Execute each step in order. Do not skip steps.**
## Step 1: Gather Information
Run these commands in parallel to analyze the changes:
```bash
# Get current branch and remote
git branch --show-current
git remote -v | grep push
# Determine the best available dev reference
if git rev-parse --verify --quiet upstream/dev >/dev/null; then
BASE_REF="upstream/dev"
elif git rev-parse --verify --quiet origin/dev >/dev/null; then
BASE_REF="origin/dev"
elif git rev-parse --verify --quiet dev >/dev/null; then
BASE_REF="dev"
else
echo "Could not find upstream/dev, origin/dev, or local dev"
exit 1
fi
BASE_SHA="$(git merge-base "$BASE_REF" HEAD)"
echo "BASE_REF=$BASE_REF"
echo "BASE_SHA=$BASE_SHA"
# Get commit info for this branch vs dev
git log "${BASE_SHA}..HEAD" --oneline
# Check what files changed
git diff "${BASE_SHA}..HEAD" --name-only
# Check if test files were added/modified
git diff "${BASE_SHA}..HEAD" --name-only | grep -E "^tests/.*\.py$" || echo "NO_TESTS_CHANGED"
# Check if manifest.json changed
git diff "${BASE_SHA}..HEAD" --name-only | grep "manifest.json" || echo "NO_MANIFEST_CHANGED"
```
From the file paths, extract the **integration domain** from `homeassistant/components/{integration}/` or `tests/components/{integration}/`.
**Track results:**
- `BASE_REF`: the dev reference used for comparison
- `BASE_SHA`: the merge-base commit used for diff-based checks
- `TESTS_CHANGED`: true if test files were added or modified
- `MANIFEST_CHANGED`: true if manifest.json was modified
**If no suitable dev reference is available, STOP and tell the user to fetch `upstream/dev`, `origin/dev`, or a local `dev` branch before continuing.**
## Step 2: Run Code Quality Checks
Run `prek` to perform code quality checks (formatting, linting, hassfest, etc.) on the files changed since `BASE_SHA`:
```bash
prek run --from-ref "$BASE_SHA" --to-ref HEAD
```
**Track results:**
- `PREK_PASSED`: true if `prek run` exits with code 0
**If `prek` fails or is not available, STOP and report the failure to the user. Do not proceed with PR creation. If the failure appears to be an environment setup issue (e.g., missing tools, command not found, venv not activated), also point the user to https://developers.home-assistant.io/docs/development_environment.**
## Step 3: Stage Any Changes from Checks
If `prek` made any formatting or generated file changes, stage and commit them as a separate commit:
```bash
git status --porcelain
# If changes exist:
git add -A
git commit -m "Apply prek formatting and generated file updates"
```
## Step 4: Run Tests
Run pytest for the specific integration:
```bash
pytest tests/components/{integration} \
--timeout=60 \
--durations-min=1 \
--durations=0 \
-q
```
**Track results:**
- `TESTS_PASSED`: true if pytest exits with code 0
**If tests fail, STOP and report the failures to the user. Do not proceed with PR creation.**
## Step 5: Identify PR Metadata
Write a release-note-style PR title summarizing the change. The title becomes the release notes entry, so it should be a complete sentence fragment describing what changed in imperative mood.
**PR Title Examples by Type:**
| Type | Example titles |
|------|----------------|
| Bugfix | `Fix Hikvision NVR binary sensors not being detected` |
| | `Fix JSON serialization of time objects in anthropic tool results` |
| | `Fix config flow bug in Tesla Fleet` |
| Dependency | `Bump eheimdigital to 1.5.0` |
| | `Bump python-otbr-api to 2.7.1` |
| New feature | `Add asyncio-level timeout to Backblaze B2 uploads` |
| | `Add Nettleie optimization option` |
| Code quality | `Add exception translations to Teslemetry` |
| | `Improve test coverage of Tesla Fleet` |
| | `Refactor adguard tests to use proper fixtures for mocking` |
| | `Simplify entity init in Proxmox` |
## Step 6: Verify Development Checklist
Check each item from the [development checklist](https://developers.home-assistant.io/docs/development_checklist/):
| Item | How to verify |
|------|---------------|
| External libraries on PyPI | Check manifest.json requirements - all should be PyPI packages |
| Dependencies in requirements_all.txt | Only if dependency declarations changed (the `requirements` field in `manifest.json` or `requirements_all.txt`), run `python -m script.gen_requirements_all` |
| Codeowners updated | If this is a new integration, ensure its `manifest.json` includes a `codeowners` field with one or more GitHub usernames |
| No commented out code | Visually scan the diff for blocks of commented-out code |
**Track results:**
- `NO_COMMENTED_CODE`: true if no blocks of commented-out code found in the diff
- `DEPENDENCIES_CHANGED`: true if the diff changes the `requirements` field in `manifest.json` or changes `requirements_all.txt`
- `REQUIREMENTS_UPDATED`: true if `DEPENDENCIES_CHANGED` is true and requirements_all.txt was regenerated successfully; not applicable if `DEPENDENCIES_CHANGED` is false
- `CHECKLIST_PASSED`: true if all items above pass
## Step 7: Determine Type of Change
Select exactly ONE based on the changes. Mark the selected type with `[x]` and all others with `[ ]` (space):
| Type | Condition |
|------|-----------|
| Dependency upgrade | Only manifest.json/requirements changes |
| Bugfix | Fixes broken behavior, no new features |
| New integration | New folder in components/ |
| New feature | Adds capability to existing integration |
| Deprecation | Adds deprecation warnings for future breaking change |
| Breaking change | Removes or changes existing functionality |
| Code quality | Only refactoring or test additions, no functional change |
**Track results:**
- `CHANGE_TYPE`: the selected type (e.g., "Bugfix", "New feature", "Code quality", etc.)
**Important:** All seven type options must remain in the PR body. Only the selected type gets `[x]`, all others get `[ ]`.
## Step 8: Determine Checkbox States
Based on the verification steps above, determine checkbox states:
| Checkbox | Condition to tick |
|----------|-------------------|
| The code change is tested and works locally | Leave unchecked for the contributor to verify manually (this refers to manual testing, not unit tests) |
| Local tests pass | Tick only if `TESTS_PASSED` is true |
| I understand the code I am submitting and can explain how it works | Leave unchecked for the contributor to review and set manually |
| There is no commented out code | Tick only if `NO_COMMENTED_CODE` is true |
| Development checklist | Tick only if `CHECKLIST_PASSED` is true |
| Perfect PR recommendations | Tick only if the PR affects a single integration or closely related modules, represents one primary type of change, and has a clear, self-contained scope |
| Formatted using Ruff | Tick only if `PREK_PASSED` is true |
| Tests have been added | Tick only if `TESTS_CHANGED` is true AND the changes exercise new or changed functionality (not only cosmetic test changes) |
| Documentation added/updated | Tick if documentation PR created (or not applicable) |
| Manifest file fields filled out | Tick if `PREK_PASSED` is true (or not applicable) |
| Dependencies in requirements_all.txt | Tick only if `DEPENDENCIES_CHANGED` is false, or if `DEPENDENCIES_CHANGED` is true and `REQUIREMENTS_UPDATED` is true |
| Dependency changelog linked | Tick if dependency changelog linked in PR description (or not applicable) |
| Any generated code has been carefully reviewed | Leave unchecked for the contributor to review and set manually |
## Step 9: Breaking Change Section
**If `CHANGE_TYPE` is NOT "Breaking change" or "Deprecation": REMOVE the entire "## Breaking change" section from the PR body (including the heading).**
If `CHANGE_TYPE` IS "Breaking change" or "Deprecation", keep the `## Breaking change` section and describe:
- What breaks
- How users can fix it
- Why it was necessary
## Step 10: Push Branch and Create PR
```bash
# Get branch name and GitHub username
BRANCH=$(git branch --show-current)
PUSH_REMOTE=$(git config "branch.$BRANCH.remote" 2>/dev/null || git remote | head -1)
GITHUB_USER=$(gh api user --jq .login 2>/dev/null || git remote get-url "$PUSH_REMOTE" | sed -E 's#.*[:/]([^/]+)/([^/]+)(\.git)?$#\1#')
# Create PR (gh pr create pushes the branch automatically)
gh pr create --repo home-assistant/core --base dev \
--head "$GITHUB_USER:$BRANCH" \
--draft \
--title "TITLE_HERE" \
--body "$(cat <<'EOF'
BODY_HERE
EOF
)"
```
### PR Body Template
Read the PR template from `.github/PULL_REQUEST_TEMPLATE.md` and use it as the basis for the PR body. **Do not hardcode the template — always read it from the file to stay in sync with upstream changes.**
Use any HTML comments (`<!-- ... -->`) in the template as guidance to understand what to fill in. For the final PR body sent to GitHub, keep the template text intact — do not delete any text from the template unless it explicitly instructs removal (e.g., the breaking change section when not applicable). Then fill in the sections:
1. **Breaking change section**: If the type is NOT "Breaking change" or "Deprecation", remove the entire `## Breaking change` section (heading and body). Otherwise, describe what breaks, how users can fix it, and why.
2. **Proposed change section**: Fill in a description of the change extracted from commit messages.
3. **Type of change**: Check exactly ONE checkbox matching the determined type from Step 7. Leave all others unchecked.
4. **Additional information**: Fill in any related issue numbers if known.
5. **Checklist**: Check boxes based on the conditions in Step 8. Leave manual-verification boxes unchecked for the contributor.
**Important:** Preserve all template structure, options, and link references exactly as they appear in the file — only modify checkbox states and fill in content sections.
## Step 11: Report Result
Provide the user with:
1. **PR URL** - The created pull request link
2. **Verification Summary** - Which checks passed/failed
3. **Unchecked Items** - List any checkboxes left unchecked and why
4. **User Action Required** - Remind user to:
- Review and set manual-verification checkboxes ("I understand the code..." and "Any generated code...") as applicable
- Consider reviewing two other open PRs
- Add any related issue numbers if applicable
+10 -756
View File
@@ -3,54 +3,27 @@ name: Home Assistant Integration knowledge
description: Everything you need to know to build, test and review Home Assistant Integrations. If you're looking at an integration, you must use this as your primary reference.
---
### File Locations
## File Locations
- **Integration code**: `./homeassistant/components/<integration_domain>/`
- **Integration tests**: `./tests/components/<integration_domain>/`
## Integration Templates
## General guidelines
### Standard Integration Structure
```
homeassistant/components/my_integration/
├── __init__.py # Entry point with async_setup_entry
├── manifest.json # Integration metadata and dependencies
├── const.py # Domain and constants
├── config_flow.py # UI configuration flow
├── coordinator.py # Data update coordinator (if needed)
├── entity.py # Base entity class (if shared patterns)
├── sensor.py # Sensor platform
├── strings.json # User-facing text and translations
├── services.yaml # Service definitions (if applicable)
└── quality_scale.yaml # Quality scale rule status
```
- When looking for examples, prefer integrations with the platinum or gold quality scale level first.
- Polling intervals are NOT user-configurable. Never add scan_interval, update_interval, or polling frequency options to config flows or config entries.
- Do NOT allow users to set config entry names in config flows. Names are automatically generated or can be customized later in UI. Exception: helper integrations may allow custom names.
An integration can have platforms as needed (e.g., `sensor.py`, `switch.py`, etc.). The following platforms have extra guidelines:
The following platforms have extra guidelines:
- **Diagnostics**: [`platform-diagnostics.md`](platform-diagnostics.md) for diagnostic data collection
- **Repairs**: [`platform-repairs.md`](platform-repairs.md) for user-actionable repair issues
### Minimal Integration Checklist
- [ ] `manifest.json` with required fields (domain, name, codeowners, etc.)
- [ ] `__init__.py` with `async_setup_entry` and `async_unload_entry`
- [ ] `config_flow.py` with UI configuration support
- [ ] `const.py` with `DOMAIN` constant
- [ ] `strings.json` with at least config flow text
- [ ] Platform files (`sensor.py`, etc.) as needed
- [ ] `quality_scale.yaml` with rule status tracking
## Integration Quality Scale
Home Assistant uses an Integration Quality Scale to ensure code quality and consistency. The quality level determines which rules apply:
- When validating the quality scale rules, check them at https://developers.home-assistant.io/docs/core/integration-quality-scale/rules
- When implementing or reviewing an integration, always consider the quality scale rules, since they promote best practices.
### Quality Scale Levels
- **Bronze**: Basic requirements (ALL Bronze rules are mandatory)
- **Silver**: Enhanced functionality
- **Gold**: Advanced features
- **Platinum**: Highest quality standards
### Quality Scale Progression
- **Bronze → Silver**: Add entity unavailability, parallel updates, auth flows
- **Silver → Gold**: Add device management, diagnostics, translations
- **Gold → Platinum**: Add strict typing, async dependencies, websession injection
Template scale file: `./script/scaffold/templates/integration/integration/quality_scale.yaml`
### How Rules Apply
1. **Check `manifest.json`**: Look for `"quality_scale"` key to determine integration level
@@ -61,726 +34,7 @@ Home Assistant uses an Integration Quality Scale to ensure code quality and cons
- `exempt`: Rule doesn't apply (with reason in comment)
- `todo`: Rule needs implementation
### Example `quality_scale.yaml` Structure
```yaml
rules:
# Bronze (mandatory)
config-flow: done
entity-unique-id: done
action-setup:
status: exempt
comment: Integration does not register custom actions.
# Silver (if targeting Silver+)
entity-unavailable: done
parallel-updates: done
# Gold (if targeting Gold+)
devices: done
diagnostics: done
# Platinum (if targeting Platinum)
strict-typing: done
```
**When Reviewing/Creating Code**: Always check the integration's quality scale level and exemption status before applying rules.
## Code Organization
### Core Locations
- Shared constants: `homeassistant/const.py` (use these instead of hardcoding)
- Integration structure:
- `homeassistant/components/{domain}/const.py` - Constants
- `homeassistant/components/{domain}/models.py` - Data models
- `homeassistant/components/{domain}/coordinator.py` - Update coordinator
- `homeassistant/components/{domain}/config_flow.py` - Configuration flow
- `homeassistant/components/{domain}/{platform}.py` - Platform implementations
### Common Modules
- **coordinator.py**: Centralize data fetching logic
```python
class MyCoordinator(DataUpdateCoordinator[MyData]):
def __init__(self, hass: HomeAssistant, client: MyClient, config_entry: ConfigEntry) -> None:
super().__init__(
hass,
logger=LOGGER,
name=DOMAIN,
update_interval=timedelta(minutes=1),
config_entry=config_entry, # ✅ Pass config_entry - it's accepted and recommended
)
```
- **entity.py**: Base entity definitions to reduce duplication
```python
class MyEntity(CoordinatorEntity[MyCoordinator]):
_attr_has_entity_name = True
```
### Runtime Data Storage
- **Use ConfigEntry.runtime_data**: Store non-persistent runtime data
```python
type MyIntegrationConfigEntry = ConfigEntry[MyClient]
async def async_setup_entry(hass: HomeAssistant, entry: MyIntegrationConfigEntry) -> bool:
client = MyClient(entry.data[CONF_HOST])
entry.runtime_data = client
```
### Manifest Requirements
- **Required Fields**: `domain`, `name`, `codeowners`, `integration_type`, `documentation`, `requirements`
- **Integration Types**: `device`, `hub`, `service`, `system`, `helper`
- **IoT Class**: Always specify connectivity method (e.g., `cloud_polling`, `local_polling`, `local_push`)
- **Discovery Methods**: Add when applicable: `zeroconf`, `dhcp`, `bluetooth`, `ssdp`, `usb`
- **Dependencies**: Include platform dependencies (e.g., `application_credentials`, `bluetooth_adapters`)
### Config Flow Patterns
- **Version Control**: Always set `VERSION = 1` and `MINOR_VERSION = 1`
- **Unique ID Management**:
```python
await self.async_set_unique_id(device_unique_id)
self._abort_if_unique_id_configured()
```
- **Error Handling**: Define errors in `strings.json` under `config.error`
- **Step Methods**: Use standard naming (`async_step_user`, `async_step_discovery`, etc.)
### Integration Ownership
- **manifest.json**: Add GitHub usernames to `codeowners`:
```json
{
"domain": "my_integration",
"name": "My Integration",
"codeowners": ["@me"]
}
```
### Async Dependencies (Platinum)
- **Requirement**: All dependencies must use asyncio
- Ensures efficient task handling without thread context switching
### WebSession Injection (Platinum)
- **Pass WebSession**: Support passing web sessions to dependencies
```python
async def async_setup_entry(hass: HomeAssistant, entry: MyConfigEntry) -> bool:
"""Set up integration from config entry."""
client = MyClient(entry.data[CONF_HOST], async_get_clientsession(hass))
```
- For cookies: Use `async_create_clientsession` (aiohttp) or `create_async_httpx_client` (httpx)
### Data Update Coordinator
- **Standard Pattern**: Use for efficient data management
```python
class MyCoordinator(DataUpdateCoordinator):
def __init__(self, hass: HomeAssistant, client: MyClient, config_entry: ConfigEntry) -> None:
super().__init__(
hass,
logger=LOGGER,
name=DOMAIN,
update_interval=timedelta(minutes=5),
config_entry=config_entry, # ✅ Pass config_entry - it's accepted and recommended
)
self.client = client
async def _async_update_data(self):
try:
return await self.client.fetch_data()
except ApiError as err:
raise UpdateFailed(f"API communication error: {err}")
```
- **Error Types**: Use `UpdateFailed` for API errors, `ConfigEntryAuthFailed` for auth issues
- **Config Entry**: Always pass `config_entry` parameter to coordinator - it's accepted and recommended
## Integration Guidelines
### Configuration Flow
- **UI Setup Required**: All integrations must support configuration via UI
- **Manifest**: Set `"config_flow": true` in `manifest.json`
- **Data Storage**:
- Connection-critical config: Store in `ConfigEntry.data`
- Non-critical settings: Store in `ConfigEntry.options`
- **Validation**: Always validate user input before creating entries
- **Config Entry Naming**:
- ❌ Do NOT allow users to set config entry names in config flows
- Names are automatically generated or can be customized later in UI
- ✅ Exception: Helper integrations MAY allow custom names in config flow
- **Connection Testing**: Test device/service connection during config flow:
```python
try:
await client.get_data()
except MyException:
errors["base"] = "cannot_connect"
```
- **Duplicate Prevention**: Prevent duplicate configurations:
```python
# Using unique ID
await self.async_set_unique_id(identifier)
self._abort_if_unique_id_configured()
# Using unique data
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
```
### Reauthentication Support
- **Required Method**: Implement `async_step_reauth` in config flow
- **Credential Updates**: Allow users to update credentials without re-adding
- **Validation**: Verify account matches existing unique ID:
```python
await self.async_set_unique_id(user_id)
self._abort_if_unique_id_mismatch(reason="wrong_account")
return self.async_update_reload_and_abort(
self._get_reauth_entry(),
data_updates={CONF_API_TOKEN: user_input[CONF_API_TOKEN]}
)
```
### Reconfiguration Flow
- **Purpose**: Allow configuration updates without removing device
- **Implementation**: Add `async_step_reconfigure` method
- **Validation**: Prevent changing underlying account with `_abort_if_unique_id_mismatch`
### Device Discovery
- **Manifest Configuration**: Add discovery method (zeroconf, dhcp, etc.)
```json
{
"zeroconf": ["_mydevice._tcp.local."]
}
```
- **Discovery Handler**: Implement appropriate `async_step_*` method:
```python
async def async_step_zeroconf(self, discovery_info):
"""Handle zeroconf discovery."""
await self.async_set_unique_id(discovery_info.properties["serialno"])
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.host})
```
- **Network Updates**: Use discovery to update dynamic IP addresses
### Network Discovery Implementation
- **Zeroconf/mDNS**: Use async instances
```python
aiozc = await zeroconf.async_get_async_instance(hass)
```
- **SSDP Discovery**: Register callbacks with cleanup
```python
entry.async_on_unload(
ssdp.async_register_callback(
hass, _async_discovered_device,
{"st": "urn:schemas-upnp-org:device:ZonePlayer:1"}
)
)
```
### Bluetooth Integration
- **Manifest Dependencies**: Add `bluetooth_adapters` to dependencies
- **Connectable**: Set `"connectable": true` for connection-required devices
- **Scanner Usage**: Always use shared scanner instance
```python
scanner = bluetooth.async_get_scanner()
entry.async_on_unload(
bluetooth.async_register_callback(
hass, _async_discovered_device,
{"service_uuid": "example_uuid"},
bluetooth.BluetoothScanningMode.ACTIVE
)
)
```
- **Connection Handling**: Never reuse `BleakClient` instances, use 10+ second timeouts
### Setup Validation
- **Test Before Setup**: Verify integration can be set up in `async_setup_entry`
- **Exception Handling**:
- `ConfigEntryNotReady`: Device offline or temporary failure
- `ConfigEntryAuthFailed`: Authentication issues
- `ConfigEntryError`: Unresolvable setup problems
### Config Entry Unloading
- **Required**: Implement `async_unload_entry` for runtime removal/reload
- **Platform Unloading**: Use `hass.config_entries.async_unload_platforms`
- **Cleanup**: Register callbacks with `entry.async_on_unload`:
```python
async def async_unload_entry(hass: HomeAssistant, entry: MyConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
entry.runtime_data.listener() # Clean up resources
return unload_ok
```
### Service Actions
- **Registration**: Register all service actions in `async_setup`, NOT in `async_setup_entry`
- **Validation**: Check config entry existence and loaded state:
```python
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def service_action(call: ServiceCall) -> ServiceResponse:
if not (entry := hass.config_entries.async_get_entry(call.data[ATTR_CONFIG_ENTRY_ID])):
raise ServiceValidationError("Entry not found")
if entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError("Entry not loaded")
```
- **Exception Handling**: Raise appropriate exceptions:
```python
# For invalid input
if end_date < start_date:
raise ServiceValidationError("End date must be after start date")
# For service errors
try:
await client.set_schedule(start_date, end_date)
except MyConnectionError as err:
raise HomeAssistantError("Could not connect to the schedule") from err
```
### Service Registration Patterns
- **Entity Services**: Register on platform setup
```python
platform.async_register_entity_service(
"my_entity_service",
{vol.Required("parameter"): cv.string},
"handle_service_method"
)
```
- **Service Schema**: Always validate input
```python
SERVICE_SCHEMA = vol.Schema({
vol.Required("entity_id"): cv.entity_ids,
vol.Required("parameter"): cv.string,
vol.Optional("timeout", default=30): cv.positive_int,
})
```
- **Services File**: Create `services.yaml` with descriptions and field definitions
### Polling
- Use update coordinator pattern when possible
- **Polling intervals are NOT user-configurable**: Never add scan_interval, update_interval, or polling frequency options to config flows or config entries
- **Integration determines intervals**: Set `update_interval` programmatically based on integration logic, not user input
- **Minimum Intervals**:
- Local network: 5 seconds
- Cloud services: 60 seconds
- **Parallel Updates**: Specify number of concurrent updates:
```python
PARALLEL_UPDATES = 1 # Serialize updates to prevent overwhelming device
# OR
PARALLEL_UPDATES = 0 # Unlimited (for coordinator-based or read-only)
```
## Entity Development
### Unique IDs
- **Required**: Every entity must have a unique ID for registry tracking
- Must be unique per platform (not per integration)
- Don't include integration domain or platform in ID
- **Implementation**:
```python
class MySensor(SensorEntity):
def __init__(self, device_id: str) -> None:
self._attr_unique_id = f"{device_id}_temperature"
```
**Acceptable ID Sources**:
- Device serial numbers
- MAC addresses (formatted using `format_mac` from device registry)
- Physical identifiers (printed/EEPROM)
- Config entry ID as last resort: `f"{entry.entry_id}-battery"`
**Never Use**:
- IP addresses, hostnames, URLs
- Device names
- Email addresses, usernames
### Entity Descriptions
- **Lambda/Anonymous Functions**: Often used in EntityDescription for value transformation
- **Multiline Lambdas**: When lambdas exceed line length, wrap in parentheses for readability
- **Bad pattern**:
```python
SensorEntityDescription(
key="temperature",
name="Temperature",
value_fn=lambda data: round(data["temp_value"] * 1.8 + 32, 1) if data.get("temp_value") is not None else None, # ❌ Too long
)
```
- **Good pattern**:
```python
SensorEntityDescription(
key="temperature",
name="Temperature",
value_fn=lambda data: ( # ✅ Parenthesis on same line as lambda
round(data["temp_value"] * 1.8 + 32, 1)
if data.get("temp_value") is not None
else None
),
)
```
### Entity Naming
- **Use has_entity_name**: Set `_attr_has_entity_name = True`
- **For specific fields**:
```python
class MySensor(SensorEntity):
_attr_has_entity_name = True
def __init__(self, device: Device, field: str) -> None:
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.id)},
name=device.name,
)
self._attr_name = field # e.g., "temperature", "humidity"
```
- **For device itself**: Set `_attr_name = None`
### Event Lifecycle Management
- **Subscribe in `async_added_to_hass`**:
```python
async def async_added_to_hass(self) -> None:
"""Subscribe to events."""
self.async_on_remove(
self.client.events.subscribe("my_event", self._handle_event)
)
```
- **Unsubscribe in `async_will_remove_from_hass`** if not using `async_on_remove`
- Never subscribe in `__init__` or other methods
### State Handling
- Unknown values: Use `None` (not "unknown" or "unavailable")
- Availability: Implement `available()` property instead of using "unavailable" state
### Entity Availability
- **Mark Unavailable**: When data cannot be fetched from device/service
- **Coordinator Pattern**:
```python
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self.identifier in self.coordinator.data
```
- **Direct Update Pattern**:
```python
async def async_update(self) -> None:
"""Update entity."""
try:
data = await self.client.get_data()
except MyException:
self._attr_available = False
else:
self._attr_available = True
self._attr_native_value = data.value
```
### Extra State Attributes
- All attribute keys must always be present
- Unknown values: Use `None`
- Provide descriptive attributes
## Device Management
### Device Registry
- **Create Devices**: Group related entities under devices
- **Device Info**: Provide comprehensive metadata:
```python
_attr_device_info = DeviceInfo(
connections={(CONNECTION_NETWORK_MAC, device.mac)},
identifiers={(DOMAIN, device.id)},
name=device.name,
manufacturer="My Company",
model="My Sensor",
sw_version=device.version,
)
```
- For services: Add `entry_type=DeviceEntryType.SERVICE`
### Dynamic Device Addition
- **Auto-detect New Devices**: After initial setup
- **Implementation Pattern**:
```python
def _check_device() -> None:
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities([MySensor(coordinator, device_id) for device_id in new_devices])
entry.async_on_unload(coordinator.async_add_listener(_check_device))
```
### Stale Device Removal
- **Auto-remove**: When devices disappear from hub/account
- **Device Registry Update**:
```python
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
```
- **Manual Deletion**: Implement `async_remove_config_entry_device` when needed
### Entity Categories
- **Required**: Assign appropriate category to entities
- **Implementation**: Set `_attr_entity_category`
```python
class MySensor(SensorEntity):
_attr_entity_category = EntityCategory.DIAGNOSTIC
```
- Categories include: `DIAGNOSTIC` for system/technical information
### Device Classes
- **Use When Available**: Set appropriate device class for entity type
```python
class MyTemperatureSensor(SensorEntity):
_attr_device_class = SensorDeviceClass.TEMPERATURE
```
- Provides context for: unit conversion, voice control, UI representation
### Disabled by Default
- **Disable Noisy/Less Popular Entities**: Reduce resource usage
```python
class MySignalStrengthSensor(SensorEntity):
_attr_entity_registry_enabled_default = False
```
- Target: frequently changing states, technical diagnostics
### Entity Translations
- **Required with has_entity_name**: Support international users
- **Implementation**:
```python
class MySensor(SensorEntity):
_attr_has_entity_name = True
_attr_translation_key = "phase_voltage"
```
- Create `strings.json` with translations:
```json
{
"entity": {
"sensor": {
"phase_voltage": {
"name": "Phase voltage"
}
}
}
}
```
### Exception Translations (Gold)
- **Translatable Errors**: Use translation keys for user-facing exceptions
- **Implementation**:
```python
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="end_date_before_start_date",
)
```
- Add to `strings.json`:
```json
{
"exceptions": {
"end_date_before_start_date": {
"message": "The end date cannot be before the start date."
}
}
}
```
### Icon Translations (Gold)
- **Dynamic Icons**: Support state and range-based icon selection
- **State-based Icons**:
```json
{
"entity": {
"sensor": {
"tree_pollen": {
"default": "mdi:tree",
"state": {
"high": "mdi:tree-outline"
}
}
}
}
}
```
- **Range-based Icons** (for numeric values):
```json
{
"entity": {
"sensor": {
"battery_level": {
"default": "mdi:battery-unknown",
"range": {
"0": "mdi:battery-outline",
"90": "mdi:battery-90",
"100": "mdi:battery"
}
}
}
}
}
```
## Testing Requirements
- **Location**: `tests/components/{domain}/`
- **Coverage Requirement**: Above 95% test coverage for all modules
- **Best Practices**:
- Use pytest fixtures from `tests.common`
- Mock all external dependencies
- Use snapshots for complex data structures
- Follow existing test patterns
### Config Flow Testing
- **100% Coverage Required**: All config flow paths must be tested
- **Patch Boundaries**: Only patch library or client methods when testing config flows. Do not patch methods defined in `config_flow.py`; exercise the flow logic end-to-end.
- **Test Scenarios**:
- All flow initiation methods (user, discovery, import)
- Successful configuration paths
- Error recovery scenarios
- Prevention of duplicate entries
- Flow completion after errors
- Reauthentication/reconfigure flows
### Testing
- **Integration-specific tests** (recommended):
```bash
pytest ./tests/components/<integration_domain> \
--cov=homeassistant.components.<integration_domain> \
--cov-report term-missing \
--durations-min=1 \
--durations=0 \
--numprocesses=auto
```
### Testing Best Practices
- **Never access `hass.data` directly** - Use fixtures and proper integration setup instead
- **Use snapshot testing** - For verifying entity states and attributes
- **Test through integration setup** - Don't test entities in isolation
- **Mock external APIs** - Use fixtures with realistic JSON data
- **Verify registries** - Ensure entities are properly registered with devices
### Config Flow Testing Template
```python
async def test_user_flow_success(hass, mock_api):
"""Test successful user flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
# Test form submission
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=TEST_USER_INPUT
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == "My Device"
assert result["data"] == TEST_USER_INPUT
async def test_flow_connection_error(hass, mock_api_error):
"""Test connection error handling."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=TEST_USER_INPUT
)
assert result["type"] == FlowResultType.FORM
assert result["errors"] == {"base": "cannot_connect"}
```
### Entity Testing Patterns
```python
@pytest.fixture
def platforms() -> list[Platform]:
"""Overridden fixture to specify platforms to test."""
return [Platform.SENSOR] # Or another specific platform as needed.
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
async def test_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the sensor entities."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
# Ensure entities are correctly assigned to device
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, "device_unique_id")}
)
assert device_entry
entity_entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
for entity_entry in entity_entries:
assert entity_entry.device_id == device_entry.id
```
### Mock Patterns
```python
# Modern integration fixture setup
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
title="My Integration",
domain=DOMAIN,
data={CONF_HOST: "127.0.0.1", CONF_API_KEY: "test_key"},
unique_id="device_unique_id",
)
@pytest.fixture
def mock_device_api() -> Generator[MagicMock]:
"""Return a mocked device API."""
with patch("homeassistant.components.my_integration.MyDeviceAPI", autospec=True) as api_mock:
api = api_mock.return_value
api.get_data.return_value = MyDeviceData.from_json(
load_fixture("device_data.json", DOMAIN)
)
yield api
@pytest.fixture
def platforms() -> list[Platform]:
"""Fixture to specify platforms to test."""
return PLATFORMS
@pytest.fixture
async def init_integration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_device_api: MagicMock,
platforms: list[Platform],
) -> MockConfigEntry:
"""Set up the integration for testing."""
mock_config_entry.add_to_hass(hass)
with patch("homeassistant.components.my_integration.PLATFORMS", platforms):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
return mock_config_entry
```
## Debugging & Troubleshooting
### Common Issues & Solutions
- **Integration won't load**: Check `manifest.json` syntax and required fields
- **Entities not appearing**: Verify `unique_id` and `has_entity_name` implementation
- **Config flow errors**: Check `strings.json` entries and error handling
- **Discovery not working**: Verify manifest discovery configuration and callbacks
- **Tests failing**: Check mock setup and async context
### Debug Logging Setup
```python
# Enable debug logging in tests
caplog.set_level(logging.DEBUG, logger="my_integration")
# In integration code - use proper logging
_LOGGER = logging.getLogger(__name__)
_LOGGER.debug("Processing data: %s", data) # Use lazy logging
```
### Validation Commands
```bash
# Check specific integration
python -m script.hassfest --integration-path homeassistant/components/my_integration
# Validate quality scale
# Check quality_scale.yaml against current rules
# Run integration tests with coverage
pytest ./tests/components/my_integration \
--cov=homeassistant.components.my_integration \
--cov-report term-missing
```
- Tests should avoid interacting or mocking internal integration details. For more info, see https://developers.home-assistant.io/docs/development_testing/#writing-tests-for-integrations
@@ -3,17 +3,4 @@
Platform exists as `homeassistant/components/<domain>/diagnostics.py`.
- **Required**: Implement diagnostic data collection
- **Implementation**:
```python
TO_REDACT = [CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: MyConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
return {
"entry_data": async_redact_data(entry.data, TO_REDACT),
"data": entry.runtime_data.data,
}
```
- **Security**: Never expose passwords, tokens, or sensitive coordinates
@@ -8,29 +8,6 @@ Platform exists as `homeassistant/components/<domain>/repairs.py`.
- Provide specific steps users need to take to resolve the issue
- Use friendly, helpful language
- Include relevant context (device names, error details, etc.)
- **Implementation**:
```python
ir.async_create_issue(
hass,
DOMAIN,
"outdated_version",
is_fixable=False,
issue_domain=DOMAIN,
severity=ir.IssueSeverity.ERROR,
translation_key="outdated_version",
)
```
- **Translation Strings Requirements**: Must contain user-actionable text in `strings.json`:
```json
{
"issues": {
"outdated_version": {
"title": "Device firmware is outdated",
"description": "Your device firmware version {current_version} is below the minimum required version {min_version}. To fix this issue: 1) Open the manufacturer's mobile app, 2) Navigate to device settings, 3) Select 'Update Firmware', 4) Wait for the update to complete, then 5) Restart Home Assistant."
}
}
}
```
- **String Content Must Include**:
- What the problem is
- Why it matters
@@ -41,15 +18,4 @@ Platform exists as `homeassistant/components/<domain>/repairs.py`.
- `CRITICAL`: Reserved for extreme scenarios only
- `ERROR`: Requires immediate user attention
- `WARNING`: Indicates future potential breakage
- **Additional Attributes**:
```python
ir.async_create_issue(
hass, DOMAIN, "issue_id",
breaks_in_ha_version="2024.1.0",
is_fixable=True,
is_persistent=True,
severity=ir.IssueSeverity.ERROR,
translation_key="issue_description",
)
```
- Only create issues for problems users can potentially resolve
+2 -3
View File
@@ -11,10 +11,9 @@
This repository contains the core of Home Assistant, a Python 3 based home automation application.
## Code Review Guidelines
## Git Commit Guidelines
**Git commit practices during review:**
- **Do NOT amend, squash, or rebase commits after review has started** - Reviewers need to see what changed since their last review
- **Do NOT amend, squash, or rebase commits that have already been pushed to the PR branch after the PR is opened** - Reviewers need to follow the commit history, as well as see what changed since their last review
## Development Commands
+3 -3
View File
@@ -112,7 +112,7 @@ jobs:
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16
uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/frontend
@@ -123,7 +123,7 @@ jobs:
- name: Download nightly wheels of intents
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16
uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: OHF-Voice/intents-package
@@ -342,7 +342,7 @@ jobs:
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
steps:
- name: Install Cosign
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
with:
cosign-release: "v2.5.3"
+28 -28
View File
@@ -40,7 +40,7 @@ env:
CACHE_VERSION: 3
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2026.4"
HA_SHORT_VERSION: "2026.5"
ADDITIONAL_PYTHON_VERSIONS: "[]"
# 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
@@ -280,7 +280,7 @@ jobs:
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
echo "::add-matcher::.github/workflows/matchers/codespell.json"
- name: Run prek
uses: j178/prek-action@0bb87d7f00b0c99306c8bcb8b8beba1eb581c037 # v1.1.1
uses: j178/prek-action@53276d8b0d10f8b6672aa85b4588c6921d0370cc # v2.0.1
env:
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config,zizmor
RUFF_OUTPUT_FORMAT: github
@@ -301,7 +301,7 @@ jobs:
with:
persist-credentials: false
- name: Run zizmor
uses: j178/prek-action@0bb87d7f00b0c99306c8bcb8b8beba1eb581c037 # v1.1.1
uses: j178/prek-action@53276d8b0d10f8b6672aa85b4588c6921d0370cc # v2.0.1
with:
extra-args: --all-files zizmor
@@ -364,7 +364,7 @@ jobs:
echo "key=uv-${UV_CACHE_VERSION}-${uv_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
key: >-
@@ -372,7 +372,7 @@ jobs:
needs.info.outputs.python_cache_key }}
- name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ${{ env.UV_CACHE_DIR }}
key: >-
@@ -384,7 +384,7 @@ jobs:
env.HA_SHORT_VERSION }}-
- name: Check if apt cache exists
id: cache-apt-check
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
lookup-only: ${{ steps.cache-venv.outputs.cache-hit == 'true' }}
path: |
@@ -430,7 +430,7 @@ jobs:
fi
- name: Save apt cache
if: steps.cache-apt-check.outputs.cache-hit != 'true'
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -484,7 +484,7 @@ jobs:
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- name: Restore apt cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -515,7 +515,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -552,7 +552,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -643,7 +643,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -694,7 +694,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -747,7 +747,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -804,7 +804,7 @@ jobs:
echo "key=mypy-${MYPY_CACHE_VERSION}-${mypy_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -812,7 +812,7 @@ jobs:
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Restore mypy cache
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: .mypy_cache
key: >-
@@ -854,7 +854,7 @@ jobs:
- base
steps:
- name: Restore apt cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -887,7 +887,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -930,7 +930,7 @@ jobs:
group: ${{ fromJson(needs.info.outputs.test_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -964,7 +964,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -1080,7 +1080,7 @@ jobs:
mariadb-group: ${{ fromJson(needs.info.outputs.mariadb_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -1115,7 +1115,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -1238,7 +1238,7 @@ jobs:
postgresql-group: ${{ fromJson(needs.info.outputs.postgresql_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -1275,7 +1275,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -1392,7 +1392,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
fail_ci_if_error: true
flags: full-suite
@@ -1421,7 +1421,7 @@ jobs:
group: ${{ fromJson(needs.info.outputs.test_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -1455,7 +1455,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -1563,7 +1563,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
@@ -1591,7 +1591,7 @@ jobs:
with:
pattern: test-results-*
- name: Upload test results to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
report_type: test_results
fail_ci_if_error: true
+7
View File
@@ -87,6 +87,13 @@ repos:
language: script
types: [text]
files: ^(homeassistant/.+/manifest\.json|homeassistant/brands/.+\.json|pyproject\.toml|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$
- id: gen_copilot_instructions
name: gen_copilot_instructions
entry: script/run-in-env.sh python3 -m script.gen_copilot_instructions
pass_filenames: false
language: script
types: [text]
files: ^(AGENTS\.md|\.claude/skills/(?!github-pr-reviewer/).+/SKILL\.md|\.github/copilot-instructions\.md|script/gen_copilot_instructions\.py)$
- id: hassfest
name: hassfest
entry: script/run-in-env.sh python3 -m script.hassfest
+2
View File
@@ -174,6 +174,7 @@ homeassistant.components.dnsip.*
homeassistant.components.doorbird.*
homeassistant.components.dormakaba_dkey.*
homeassistant.components.downloader.*
homeassistant.components.dropbox.*
homeassistant.components.droplet.*
homeassistant.components.dsmr.*
homeassistant.components.duckdns.*
@@ -578,6 +579,7 @@ homeassistant.components.trmnl.*
homeassistant.components.tts.*
homeassistant.components.twentemilieu.*
homeassistant.components.unifi.*
homeassistant.components.unifi_access.*
homeassistant.components.unifiprotect.*
homeassistant.components.upcloud.*
homeassistant.components.update.*
+2 -3
View File
@@ -2,10 +2,9 @@
This repository contains the core of Home Assistant, a Python 3 based home automation application.
## Code Review Guidelines
## Git Commit Guidelines
**Git commit practices during review:**
- **Do NOT amend, squash, or rebase commits after review has started** - Reviewers need to see what changed since their last review
- **Do NOT amend, squash, or rebase commits that have already been pushed to the PR branch after the PR is opened** - Reviewers need to follow the commit history, as well as see what changed since their last review
## Development Commands
Generated
+19 -8
View File
@@ -37,6 +37,13 @@ build.json @home-assistant/supervisor
# Other code
/homeassistant/scripts/check_config.py @kellerza
# Agent Configurations
AGENTS.md @home-assistant/core
CLAUDE.md @home-assistant/core
/.agent/ @home-assistant/core
/.claude/ @home-assistant/core
/.gemini/ @home-assistant/core
# Integrations
/homeassistant/components/abode/ @shred86
/tests/components/abode/ @shred86
@@ -222,8 +229,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/binary_sensor/ @home-assistant/core
/tests/components/binary_sensor/ @home-assistant/core
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria
/homeassistant/components/blebox/ @bbx-a @swistakm
/tests/components/blebox/ @bbx-a @swistakm
/homeassistant/components/blebox/ @bbx-a @swistakm @bkobus-bbx
/tests/components/blebox/ @bbx-a @swistakm @bkobus-bbx
/homeassistant/components/blink/ @fronzbot
/tests/components/blink/ @fronzbot
/homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
@@ -401,6 +408,8 @@ build.json @home-assistant/supervisor
/tests/components/dremel_3d_printer/ @tkdrob
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
/homeassistant/components/dropbox/ @bdr99
/tests/components/dropbox/ @bdr99
/homeassistant/components/droplet/ @sarahseidman
/tests/components/droplet/ @sarahseidman
/homeassistant/components/dsmr/ @Robbie1221
@@ -739,8 +748,8 @@ build.json @home-assistant/supervisor
/tests/components/honeywell/ @rdfurman @mkmer
/homeassistant/components/hr_energy_qube/ @MattieGit
/tests/components/hr_energy_qube/ @MattieGit
/homeassistant/components/html5/ @alexyao2015
/tests/components/html5/ @alexyao2015
/homeassistant/components/html5/ @alexyao2015 @tr4nt0r
/tests/components/html5/ @alexyao2015 @tr4nt0r
/homeassistant/components/http/ @home-assistant/core
/tests/components/http/ @home-assistant/core
/homeassistant/components/huawei_lte/ @scop @fphammerle
@@ -1226,12 +1235,12 @@ build.json @home-assistant/supervisor
/tests/components/onewire/ @garbled1 @epenet
/homeassistant/components/onkyo/ @arturpragacz @eclair4151
/tests/components/onkyo/ @arturpragacz @eclair4151
/homeassistant/components/onvif/ @hunterjm @jterrace
/tests/components/onvif/ @hunterjm @jterrace
/homeassistant/components/onvif/ @jterrace
/tests/components/onvif/ @jterrace
/homeassistant/components/open_meteo/ @frenck
/tests/components/open_meteo/ @frenck
/homeassistant/components/open_router/ @joostlek
/tests/components/open_router/ @joostlek
/homeassistant/components/open_router/ @joostlek @ab3lson
/tests/components/open_router/ @joostlek @ab3lson
/homeassistant/components/opendisplay/ @g4bri3lDev
/tests/components/opendisplay/ @g4bri3lDev
/homeassistant/components/openerz/ @misialq
@@ -1299,6 +1308,8 @@ build.json @home-assistant/supervisor
/tests/components/pi_hole/ @shenxn
/homeassistant/components/picnic/ @corneyl @codesalatdev
/tests/components/picnic/ @corneyl @codesalatdev
/homeassistant/components/picotts/ @rooggiieerr
/tests/components/picotts/ @rooggiieerr
/homeassistant/components/ping/ @jpbede
/tests/components/ping/ @jpbede
/homeassistant/components/plaato/ @JohNan
@@ -1 +1 @@
"""The actiontec component."""
"""The Actiontec integration."""
@@ -36,7 +36,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) ->
translation_key="auth_error",
) from err
except ActronAirAPIError as err:
raise ConfigEntryNotReady from err
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="setup_connection_error",
) from err
system_coordinators: dict[str, ActronAirSystemCoordinator] = {}
for system in systems:
@@ -64,7 +64,7 @@ rules:
status: exempt
comment: Not required for this integration at this stage.
entity-translations: todo
exception-translations: todo
exception-translations: done
icon-translations: todo
reconfiguration-flow: todo
repair-issues:
@@ -55,6 +55,9 @@
"auth_error": {
"message": "Authentication failed, please reauthenticate"
},
"setup_connection_error": {
"message": "Failed to connect to the Actron Air API"
},
"update_error": {
"message": "An error occurred while retrieving data from the Actron Air API: {error}"
}
+3 -2
View File
@@ -74,7 +74,8 @@ async def _resolve_attachments(
resolved_attachments.append(
conversation.Attachment(
media_content_id=media_content_id,
mime_type=image_data.content_type,
mime_type=attachment.get("media_content_type")
or image_data.content_type,
path=temp_filename,
)
)
@@ -89,7 +90,7 @@ async def _resolve_attachments(
resolved_attachments.append(
conversation.Attachment(
media_content_id=media_content_id,
mime_type=media.mime_type,
mime_type=attachment.get("media_content_type") or media.mime_type,
path=media.path,
)
)
+21 -4
View File
@@ -33,14 +33,21 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, SECTION_ADVANCED_SETTINGS
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
from .coordinator import (
AirOSConfigEntry,
AirOSDataUpdateCoordinator,
AirOSFirmwareUpdateCoordinator,
AirOSRuntimeData,
)
_PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.SENSOR,
Platform.UPDATE,
]
_LOGGER = logging.getLogger(__name__)
@@ -86,10 +93,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
airos_device = airos_class(**conn_data)
coordinator = AirOSDataUpdateCoordinator(hass, entry, device_data, airos_device)
await coordinator.async_config_entry_first_refresh()
data_coordinator = AirOSDataUpdateCoordinator(
hass, entry, device_data, airos_device
)
await data_coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
firmware_coordinator: AirOSFirmwareUpdateCoordinator | None = None
if device_data["fw_major"] >= 8:
firmware_coordinator = AirOSFirmwareUpdateCoordinator(hass, entry, airos_device)
await firmware_coordinator.async_config_entry_first_refresh()
entry.runtime_data = AirOSRuntimeData(
status=data_coordinator,
firmware=firmware_coordinator,
)
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
@@ -87,7 +87,7 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the AirOS binary sensors from a config entry."""
coordinator = config_entry.runtime_data
coordinator = config_entry.runtime_data.status
entities = [
AirOSBinarySensor(coordinator, description)
+3 -1
View File
@@ -31,7 +31,9 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the AirOS button from a config entry."""
async_add_entities([AirOSRebootButton(config_entry.runtime_data, REBOOT_BUTTON)])
async_add_entities(
[AirOSRebootButton(config_entry.runtime_data.status, REBOOT_BUTTON)]
)
class AirOSRebootButton(AirOSEntity, ButtonEntity):
+1
View File
@@ -5,6 +5,7 @@ from datetime import timedelta
DOMAIN = "airos"
SCAN_INTERVAL = timedelta(minutes=1)
UPDATE_SCAN_INTERVAL = timedelta(days=1)
MANUFACTURER = "Ubiquiti"
+80 -31
View File
@@ -2,7 +2,10 @@
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
import logging
from typing import Any, TypeVar
from airos.airos6 import AirOS6, AirOS6Data
from airos.airos8 import AirOS8, AirOS8Data
@@ -19,20 +22,61 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, SCAN_INTERVAL
from .const import DOMAIN, SCAN_INTERVAL, UPDATE_SCAN_INTERVAL
_LOGGER = logging.getLogger(__name__)
AirOSDeviceDetect = AirOS8 | AirOS6
AirOSDataDetect = AirOS8Data | AirOS6Data
type AirOSDeviceDetect = AirOS8 | AirOS6
type AirOSDataDetect = AirOS8Data | AirOS6Data
type AirOSUpdateData = dict[str, Any]
type AirOSConfigEntry = ConfigEntry[AirOSDataUpdateCoordinator]
type AirOSConfigEntry = ConfigEntry[AirOSRuntimeData]
T = TypeVar("T", bound=AirOSDataDetect | AirOSUpdateData)
@dataclass
class AirOSRuntimeData:
"""Data for AirOS config entry."""
status: AirOSDataUpdateCoordinator
firmware: AirOSFirmwareUpdateCoordinator | None
async def async_fetch_airos_data(
airos_device: AirOSDeviceDetect,
update_method: Callable[[], Awaitable[T]],
) -> T:
"""Fetch data from AirOS device."""
try:
await airos_device.login()
return await update_method()
except AirOSConnectionAuthenticationError as err:
_LOGGER.exception("Error authenticating with airOS device")
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="invalid_auth"
) from err
except (
AirOSConnectionSetupError,
AirOSDeviceConnectionError,
TimeoutError,
) as err:
_LOGGER.error("Error connecting to airOS device: %s", err)
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err
except AirOSDataMissingError as err:
_LOGGER.error("Expected data not returned by airOS device: %s", err)
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="error_data_missing",
) from err
class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSDataDetect]):
"""Class to manage fetching AirOS data from single endpoint."""
"""Class to manage fetching AirOS status data from single endpoint."""
airos_device: AirOSDeviceDetect
config_entry: AirOSConfigEntry
def __init__(
@@ -54,28 +98,33 @@ class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSDataDetect]):
)
async def _async_update_data(self) -> AirOSDataDetect:
"""Fetch data from AirOS."""
try:
await self.airos_device.login()
return await self.airos_device.status()
except AirOSConnectionAuthenticationError as err:
_LOGGER.exception("Error authenticating with airOS device")
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="invalid_auth"
) from err
except (
AirOSConnectionSetupError,
AirOSDeviceConnectionError,
TimeoutError,
) as err:
_LOGGER.error("Error connecting to airOS device: %s", err)
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err
except AirOSDataMissingError as err:
_LOGGER.error("Expected data not returned by airOS device: %s", err)
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="error_data_missing",
) from err
"""Fetch status data from AirOS."""
return await async_fetch_airos_data(self.airos_device, self.airos_device.status)
class AirOSFirmwareUpdateCoordinator(DataUpdateCoordinator[AirOSUpdateData]):
"""Class to manage fetching AirOS firmware."""
config_entry: AirOSConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: AirOSConfigEntry,
airos_device: AirOSDeviceDetect,
) -> None:
"""Initialize the coordinator."""
self.airos_device = airos_device
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_interval=UPDATE_SCAN_INTERVAL,
)
async def _async_update_data(self) -> AirOSUpdateData:
"""Fetch firmware data from AirOS."""
return await async_fetch_airos_data(
self.airos_device, self.airos_device.update_check
)
+11 -1
View File
@@ -29,5 +29,15 @@ async def async_get_config_entry_diagnostics(
"""Return diagnostics for a config entry."""
return {
"entry_data": async_redact_data(entry.data, TO_REDACT_HA),
"data": async_redact_data(entry.runtime_data.data.to_dict(), TO_REDACT_AIROS),
"data": {
"status_data": async_redact_data(
entry.runtime_data.status.data.to_dict(), TO_REDACT_AIROS
),
"firmware_data": async_redact_data(
entry.runtime_data.firmware.data
if entry.runtime_data.firmware is not None
else {},
TO_REDACT_AIROS,
),
},
}
+1 -1
View File
@@ -180,7 +180,7 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the AirOS sensors from a config entry."""
coordinator = config_entry.runtime_data
coordinator = config_entry.runtime_data.status
entities = [AirOSSensor(coordinator, description) for description in COMMON_SENSORS]
@@ -206,6 +206,12 @@
},
"reboot_failed": {
"message": "The device did not accept the reboot request. Try again, or check your device web interface for errors."
},
"update_connection_authentication_error": {
"message": "Authentication or connection failed during firmware update"
},
"update_error": {
"message": "Connection failed during firmware update"
}
}
}
+101
View File
@@ -0,0 +1,101 @@
"""AirOS update component for Home Assistant."""
from __future__ import annotations
import logging
from typing import Any
from airos.exceptions import AirOSConnectionAuthenticationError, AirOSException
from homeassistant.components.update import (
UpdateDeviceClass,
UpdateEntity,
UpdateEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import (
AirOSConfigEntry,
AirOSDataUpdateCoordinator,
AirOSFirmwareUpdateCoordinator,
)
from .entity import AirOSEntity
PARALLEL_UPDATES = 0
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AirOSConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the AirOS update entity from a config entry."""
runtime_data = config_entry.runtime_data
if runtime_data.firmware is None: # Unsupported device
return
async_add_entities([AirOSUpdateEntity(runtime_data.status, runtime_data.firmware)])
class AirOSUpdateEntity(AirOSEntity, UpdateEntity):
"""Update entity for AirOS firmware updates."""
_attr_device_class = UpdateDeviceClass.FIRMWARE
_attr_supported_features = UpdateEntityFeature.INSTALL
def __init__(
self,
status: AirOSDataUpdateCoordinator,
firmware: AirOSFirmwareUpdateCoordinator,
) -> None:
"""Initialize the AirOS update entity."""
super().__init__(status)
self.status = status
self.firmware = firmware
self._attr_unique_id = f"{status.data.derived.mac}_firmware_update"
@property
def installed_version(self) -> str | None:
"""Return the installed firmware version."""
return self.status.data.host.fwversion
@property
def latest_version(self) -> str | None:
"""Return the latest firmware version."""
if not self.firmware.data.get("update", False):
return self.status.data.host.fwversion
return self.firmware.data.get("version")
@property
def release_url(self) -> str | None:
"""Return the release url of the latest firmware."""
return self.firmware.data.get("changelog")
async def async_install(
self,
version: str | None,
backup: bool,
**kwargs: Any,
) -> None:
"""Handle the firmware update installation."""
_LOGGER.debug("Starting firmware update")
try:
await self.status.airos_device.login()
await self.status.airos_device.download()
await self.status.airos_device.install()
except AirOSConnectionAuthenticationError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="update_connection_authentication_error",
) from err
except AirOSException as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="update_error",
) from err
@@ -13,6 +13,9 @@ from homeassistant.helpers import (
config_entry_oauth2_flow,
device_registry as dr,
)
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from . import api
from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION, DOMAIN
@@ -25,11 +28,17 @@ async def async_setup_entry(
hass: HomeAssistant, entry: AladdinConnectConfigEntry
) -> bool:
"""Set up Aladdin Connect Genie from a config entry."""
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
@@ -37,6 +37,9 @@
"close_door_failed": {
"message": "Failed to close the garage door"
},
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
},
"open_door_failed": {
"message": "Failed to open the garage door"
}
@@ -3,10 +3,10 @@
from __future__ import annotations
import logging
from typing import Any
from typing import Any, cast
from adext import AdExt
from alarmdecoder.devices import SerialDevice, SocketDevice
from alarmdecoder.devices import Device, SerialDevice, SocketDevice
from alarmdecoder.util import NoDeviceError
import voluptuous as vol
@@ -102,16 +102,21 @@ class AlarmDecoderFlowHandler(ConfigFlow, domain=DOMAIN):
self._async_current_entries(), user_input, self.protocol
):
return self.async_abort(reason="already_configured")
connection = {}
connection: dict[str, Any] = {}
baud = None
device: Device
if self.protocol == PROTOCOL_SOCKET:
host = connection[CONF_HOST] = user_input[CONF_HOST]
port = connection[CONF_PORT] = user_input[CONF_PORT]
title = f"{host}:{port}"
host = connection[CONF_HOST] = cast(str, user_input[CONF_HOST])
port = connection[CONF_PORT] = cast(int, user_input[CONF_PORT])
title: str = f"{host}:{port}"
device = SocketDevice(interface=(host, port))
if self.protocol == PROTOCOL_SERIAL:
path = connection[CONF_DEVICE_PATH] = user_input[CONF_DEVICE_PATH]
baud = connection[CONF_DEVICE_BAUD] = user_input[CONF_DEVICE_BAUD]
path = connection[CONF_DEVICE_PATH] = cast(
str, user_input[CONF_DEVICE_PATH]
)
baud = connection[CONF_DEVICE_BAUD] = cast(
int, user_input[CONF_DEVICE_BAUD]
)
title = path
device = SerialDevice(interface=path)
@@ -132,6 +137,7 @@ class AlarmDecoderFlowHandler(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception during AlarmDecoder setup")
errors["base"] = "unknown"
schema: vol.Schema
if self.protocol == PROTOCOL_SOCKET:
schema = vol.Schema(
{
+5 -18
View File
@@ -2,19 +2,15 @@
from __future__ import annotations
import anthropic
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
from homeassistant.config_entries import ConfigSubentry
from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
issue_registry as ir,
)
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.typing import ConfigType
from .const import (
@@ -24,12 +20,11 @@ from .const import (
DOMAIN,
LOGGER,
)
from .coordinator import AnthropicConfigEntry, AnthropicCoordinator
PLATFORMS = (Platform.AI_TASK, Platform.CONVERSATION)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
type AnthropicConfigEntry = ConfigEntry[anthropic.AsyncClient]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Anthropic."""
@@ -39,17 +34,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) -> bool:
"""Set up Anthropic from a config entry."""
client = anthropic.AsyncAnthropic(
api_key=entry.data[CONF_API_KEY], http_client=get_async_client(hass)
)
try:
await client.models.list(timeout=10.0)
except anthropic.AuthenticationError as err:
raise ConfigEntryAuthFailed(err) from err
except anthropic.AnthropicError as err:
raise ConfigEntryNotReady(err) from err
entry.runtime_data = client
coordinator = AnthropicCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -12,6 +12,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.json import json_loads
from .const import DOMAIN
from .entity import AnthropicBaseLLMEntity
if TYPE_CHECKING:
@@ -60,7 +61,7 @@ class AnthropicTaskEntity(
if not isinstance(chat_log.content[-1], conversation.AssistantContent):
raise HomeAssistantError(
"Last content in chat log is not an AssistantContent"
translation_domain=DOMAIN, translation_key="response_not_found"
)
text = chat_log.content[-1].content or ""
@@ -78,7 +79,9 @@ class AnthropicTaskEntity(
err,
text,
)
raise HomeAssistantError("Error with Claude structured response") from err
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="json_parse_error"
) from err
return ai_task.GenDataTaskResult(
conversation_id=chat_log.conversation_id,
@@ -71,6 +71,16 @@ CODE_EXECUTION_UNSUPPORTED_MODELS = [
"claude-3-haiku",
]
PROGRAMMATIC_TOOL_CALLING_UNSUPPORTED_MODELS = [
"claude-haiku-4-5",
"claude-opus-4-1",
"claude-opus-4-0",
"claude-opus-4-20250514",
"claude-sonnet-4-0",
"claude-sonnet-4-20250514",
"claude-3-haiku",
]
DEPRECATED_MODELS = [
"claude-3",
]
@@ -0,0 +1,78 @@
"""Coordinator for the Anthropic integration."""
from __future__ import annotations
from datetime import timedelta
import anthropic
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER
UPDATE_INTERVAL_CONNECTED = timedelta(hours=12)
UPDATE_INTERVAL_DISCONNECTED = timedelta(minutes=1)
type AnthropicConfigEntry = ConfigEntry[AnthropicCoordinator]
class AnthropicCoordinator(DataUpdateCoordinator[None]):
"""DataUpdateCoordinator which uses different intervals after successful and unsuccessful updates."""
client: anthropic.AsyncAnthropic
def __init__(self, hass: HomeAssistant, config_entry: AnthropicConfigEntry) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
LOGGER,
config_entry=config_entry,
name=config_entry.title,
update_interval=UPDATE_INTERVAL_CONNECTED,
update_method=self.async_update_data,
always_update=False,
)
self.client = anthropic.AsyncAnthropic(
api_key=config_entry.data[CONF_API_KEY], http_client=get_async_client(hass)
)
@callback
def async_set_updated_data(self, data: None) -> None:
"""Manually update data, notify listeners and update refresh interval."""
self.update_interval = UPDATE_INTERVAL_CONNECTED
super().async_set_updated_data(data)
async def async_update_data(self) -> None:
"""Fetch data from the API."""
try:
self.update_interval = UPDATE_INTERVAL_DISCONNECTED
await self.client.models.list(timeout=10.0)
self.update_interval = UPDATE_INTERVAL_CONNECTED
except anthropic.APITimeoutError as err:
raise TimeoutError(err.message or str(err)) from err
except anthropic.AuthenticationError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="api_authentication_error",
translation_placeholders={"message": err.message},
) from err
except anthropic.APIError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="api_error",
translation_placeholders={"message": err.message},
) from err
def mark_connection_error(self) -> None:
"""Mark the connection as having an error and reschedule background check."""
self.update_interval = UPDATE_INTERVAL_DISCONNECTED
if self.last_update_success:
self.last_update_success = False
self.async_update_listeners()
if self._listeners and not self.hass.is_stopping:
self._schedule_refresh()
@@ -0,0 +1,64 @@
"""Diagnostics support for Anthropic."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from anthropic import __title__, __version__
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import entity_registry as er
from .const import (
CONF_PROMPT,
CONF_WEB_SEARCH_CITY,
CONF_WEB_SEARCH_COUNTRY,
CONF_WEB_SEARCH_REGION,
CONF_WEB_SEARCH_TIMEZONE,
)
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
from . import AnthropicConfigEntry
TO_REDACT = {
CONF_API_KEY,
CONF_PROMPT,
CONF_WEB_SEARCH_CITY,
CONF_WEB_SEARCH_REGION,
CONF_WEB_SEARCH_COUNTRY,
CONF_WEB_SEARCH_TIMEZONE,
}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: AnthropicConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
return {
"client": f"{__title__}=={__version__}",
"title": entry.title,
"entry_id": entry.entry_id,
"entry_version": f"{entry.version}.{entry.minor_version}",
"state": entry.state.value,
"data": async_redact_data(entry.data, TO_REDACT),
"options": async_redact_data(entry.options, TO_REDACT),
"subentries": {
subentry.subentry_id: {
"title": subentry.title,
"subentry_type": subentry.subentry_type,
"data": async_redact_data(subentry.data, TO_REDACT),
}
for subentry in entry.subentries.values()
},
"entities": {
entity_entry.entity_id: entity_entry.extended_dict
for entity_entry in er.async_entries_for_config_entry(
er.async_get(hass), entry.entry_id
)
},
}
+106 -34
View File
@@ -19,6 +19,8 @@ from anthropic.types import (
CitationsWebSearchResultLocation,
CitationWebSearchResultLocationParam,
CodeExecutionTool20250825Param,
CodeExecutionToolResultBlock,
CodeExecutionToolResultBlockParamContentParam,
Container,
ContentBlockParam,
DocumentBlockParam,
@@ -61,15 +63,16 @@ from anthropic.types import (
ToolUseBlockParam,
Usage,
WebSearchTool20250305Param,
WebSearchTool20260209Param,
WebSearchToolResultBlock,
WebSearchToolResultBlockParamContentParam,
)
from anthropic.types.bash_code_execution_tool_result_block_param import (
Content as BashCodeExecutionToolResultContentParam,
Content as BashCodeExecutionToolResultBlockParamContentParam,
)
from anthropic.types.message_create_params import MessageCreateParamsStreaming
from anthropic.types.text_editor_code_execution_tool_result_block_param import (
Content as TextEditorCodeExecutionToolResultContentParam,
Content as TextEditorCodeExecutionToolResultBlockParamContentParam,
)
import voluptuous as vol
from voluptuous_openapi import convert
@@ -79,12 +82,11 @@ from homeassistant.config_entries import ConfigSubentry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, llm
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify
from homeassistant.util.json import JsonObjectType
from . import AnthropicConfigEntry
from .const import (
CONF_CHAT_MODEL,
CONF_CODE_EXECUTION,
@@ -105,8 +107,10 @@ from .const import (
MIN_THINKING_BUDGET,
NON_ADAPTIVE_THINKING_MODELS,
NON_THINKING_MODELS,
PROGRAMMATIC_TOOL_CALLING_UNSUPPORTED_MODELS,
UNSUPPORTED_STRUCTURED_OUTPUT_MODELS,
)
from .coordinator import AnthropicConfigEntry, AnthropicCoordinator
# Max number of back and forth with the LLM to generate a response
MAX_TOOL_ITERATIONS = 10
@@ -224,12 +228,22 @@ def _convert_content(
},
),
}
elif content.tool_name == "code_execution":
tool_result_block = {
"type": "code_execution_tool_result",
"tool_use_id": content.tool_call_id,
"content": cast(
CodeExecutionToolResultBlockParamContentParam,
content.tool_result,
),
}
elif content.tool_name == "bash_code_execution":
tool_result_block = {
"type": "bash_code_execution_tool_result",
"tool_use_id": content.tool_call_id,
"content": cast(
BashCodeExecutionToolResultContentParam, content.tool_result
BashCodeExecutionToolResultBlockParamContentParam,
content.tool_result,
),
}
elif content.tool_name == "text_editor_code_execution":
@@ -237,7 +251,7 @@ def _convert_content(
"type": "text_editor_code_execution_tool_result",
"tool_use_id": content.tool_call_id,
"content": cast(
TextEditorCodeExecutionToolResultContentParam,
TextEditorCodeExecutionToolResultBlockParamContentParam,
content.tool_result,
),
}
@@ -368,6 +382,7 @@ def _convert_content(
name=cast(
Literal[
"web_search",
"code_execution",
"bash_code_execution",
"text_editor_code_execution",
],
@@ -379,6 +394,7 @@ def _convert_content(
and tool_call.tool_name
in [
"web_search",
"code_execution",
"bash_code_execution",
"text_editor_code_execution",
]
@@ -401,7 +417,11 @@ def _convert_content(
messages[-1]["content"] = messages[-1]["content"][0]["text"]
else:
# Note: We don't pass SystemContent here as it's passed to the API as the prompt
raise HomeAssistantError("Unexpected content type in chat log")
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="unexpected_chat_log_content",
translation_placeholders={"type": type(content).__name__},
)
return messages, container_id
@@ -443,7 +463,9 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
Each message could contain multiple blocks of the same type.
"""
if stream is None or not hasattr(stream, "__aiter__"):
raise HomeAssistantError("Expected a stream of messages")
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="unexpected_stream_object"
)
current_tool_block: ToolUseBlockParam | ServerToolUseBlockParam | None = None
current_tool_args: str
@@ -464,7 +486,7 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
type="tool_use",
id=response.content_block.id,
name=response.content_block.name,
input={},
input=response.content_block.input or {},
)
current_tool_args = ""
if response.content_block.name == output_tool:
@@ -526,13 +548,14 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
type="server_tool_use",
id=response.content_block.id,
name=response.content_block.name,
input={},
input=response.content_block.input or {},
)
current_tool_args = ""
elif isinstance(
response.content_block,
(
WebSearchToolResultBlock,
CodeExecutionToolResultBlock,
BashCodeExecutionToolResultBlock,
TextEditorCodeExecutionToolResultBlock,
),
@@ -588,13 +611,13 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
current_tool_block = None
continue
tool_args = json.loads(current_tool_args) if current_tool_args else {}
current_tool_block["input"] = tool_args
current_tool_block["input"] |= tool_args
yield {
"tool_calls": [
llm.ToolInput(
id=current_tool_block["id"],
tool_name=current_tool_block["name"],
tool_args=tool_args,
tool_args=current_tool_block["input"],
external=current_tool_block["type"] == "server_tool_use",
)
]
@@ -605,7 +628,9 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
chat_log.async_trace(_create_token_stats(input_usage, usage))
content_details.container = response.delta.container
if response.delta.stop_reason == "refusal":
raise HomeAssistantError("Potential policy violation detected")
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="api_refusal"
)
elif isinstance(response, RawMessageStopEvent):
if content_details:
content_details.delete_empty()
@@ -633,7 +658,7 @@ def _create_token_stats(
}
class AnthropicBaseLLMEntity(Entity):
class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
"""Anthropic base LLM entity."""
_attr_has_entity_name = True
@@ -641,6 +666,7 @@ class AnthropicBaseLLMEntity(Entity):
def __init__(self, entry: AnthropicConfigEntry, subentry: ConfigSubentry) -> None:
"""Initialize the entity."""
super().__init__(entry.runtime_data)
self.entry = entry
self.subentry = subentry
self._attr_unique_id = subentry.subentry_id
@@ -664,7 +690,9 @@ class AnthropicBaseLLMEntity(Entity):
system = chat_log.content[0]
if not isinstance(system, conversation.SystemContent):
raise HomeAssistantError("First message must be a system message")
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="system_message_not_found"
)
# System prompt with caching enabled
system_prompt: list[TextBlockParam] = [
@@ -725,19 +753,34 @@ class AnthropicBaseLLMEntity(Entity):
]
if options.get(CONF_CODE_EXECUTION):
tools.append(
CodeExecutionTool20250825Param(
name="code_execution",
type="code_execution_20250825",
),
)
# The `web_search_20260209` tool automatically enables `code_execution_20260120` tool
if model.startswith(
tuple(PROGRAMMATIC_TOOL_CALLING_UNSUPPORTED_MODELS)
) or not options.get(CONF_WEB_SEARCH):
tools.append(
CodeExecutionTool20250825Param(
name="code_execution",
type="code_execution_20250825",
),
)
if options.get(CONF_WEB_SEARCH):
web_search = WebSearchTool20250305Param(
name="web_search",
type="web_search_20250305",
max_uses=options.get(CONF_WEB_SEARCH_MAX_USES),
)
if model.startswith(
tuple(PROGRAMMATIC_TOOL_CALLING_UNSUPPORTED_MODELS)
) or not options.get(CONF_CODE_EXECUTION):
web_search: WebSearchTool20250305Param | WebSearchTool20260209Param = (
WebSearchTool20250305Param(
name="web_search",
type="web_search_20250305",
max_uses=options.get(CONF_WEB_SEARCH_MAX_USES),
)
)
else:
web_search = WebSearchTool20260209Param(
name="web_search",
type="web_search_20260209",
max_uses=options.get(CONF_WEB_SEARCH_MAX_USES),
)
if options.get(CONF_WEB_SEARCH_USER_LOCATION):
web_search["user_location"] = {
"type": "approximate",
@@ -754,7 +797,7 @@ class AnthropicBaseLLMEntity(Entity):
last_message = messages[-1]
if last_message["role"] != "user":
raise HomeAssistantError(
"Last message must be a user message to add attachments"
translation_domain=DOMAIN, translation_key="user_message_not_found"
)
if isinstance(last_message["content"], str):
last_message["content"] = [
@@ -835,7 +878,8 @@ class AnthropicBaseLLMEntity(Entity):
if tools:
model_args["tools"] = tools
client = self.entry.runtime_data
coordinator = self.entry.runtime_data
client = coordinator.client
# To prevent infinite loops, we limit the number of iterations
for _iteration in range(max_iterations):
@@ -857,16 +901,36 @@ class AnthropicBaseLLMEntity(Entity):
)
messages.extend(new_messages)
except anthropic.AuthenticationError as err:
self.entry.async_start_reauth(self.hass)
# Trigger coordinator to confirm the auth failure and trigger the reauth flow.
await coordinator.async_request_refresh()
raise HomeAssistantError(
"Authentication error with Anthropic API, reauthentication required"
translation_domain=DOMAIN,
translation_key="api_authentication_error",
translation_placeholders={"message": err.message},
) from err
except anthropic.APIConnectionError as err:
LOGGER.info("Connection error while talking to Anthropic: %s", err)
coordinator.mark_connection_error()
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="api_error",
translation_placeholders={"message": err.message},
) from err
except anthropic.AnthropicError as err:
# Non-connection error, mark connection as healthy
coordinator.async_set_updated_data(None)
raise HomeAssistantError(
f"Sorry, I had a problem talking to Anthropic: {err}"
translation_domain=DOMAIN,
translation_key="api_error",
translation_placeholders={
"message": err.message
if isinstance(err, anthropic.APIError)
else str(err)
},
) from err
if not chat_log.unresponded_tool_results:
coordinator.async_set_updated_data(None)
break
@@ -883,15 +947,23 @@ async def async_prepare_files_for_prompt(
for file_path, mime_type in files:
if not file_path.exists():
raise HomeAssistantError(f"`{file_path}` does not exist")
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="wrong_file_path",
translation_placeholders={"file_path": file_path.as_posix()},
)
if mime_type is None:
mime_type = guess_file_type(file_path)[0]
if not mime_type or not mime_type.startswith(("image/", "application/pdf")):
raise HomeAssistantError(
"Only images and PDF are supported by the Anthropic API,"
f"`{file_path}` is not an image file or PDF"
translation_domain=DOMAIN,
translation_key="wrong_file_type",
translation_placeholders={
"file_path": file_path.as_posix(),
"mime_type": mime_type or "unknown",
},
)
if mime_type == "image/jpg":
mime_type = "image/jpeg"
@@ -35,9 +35,9 @@ rules:
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: todo
entity-unavailable: done
integration-owner: done
log-when-unavailable: todo
log-when-unavailable: done
parallel-updates:
status: exempt
comment: |
@@ -46,7 +46,7 @@ rules:
test-coverage: done
# Gold
devices: done
diagnostics: todo
diagnostics: done
discovery-update-info:
status: exempt
comment: |
@@ -59,17 +59,11 @@ rules:
status: exempt
comment: |
No data updates.
docs-examples:
status: todo
comment: |
To give examples of how people use the integration
docs-examples: done
docs-known-limitations: done
docs-supported-devices:
status: todo
comment: |
To write something about what models we support.
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: todo
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices:
status: exempt
@@ -88,7 +82,7 @@ rules:
comment: |
No entities disabled by default.
entity-translations: todo
exception-translations: todo
exception-translations: done
icon-translations: done
reconfiguration-flow: done
repair-issues: done
@@ -58,7 +58,7 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
if entry.entry_id in self._model_list_cache:
model_list = self._model_list_cache[entry.entry_id]
else:
client = entry.runtime_data
client = entry.runtime_data.client
model_list = [
model_option
for model_option in await get_model_list(client)
@@ -161,7 +161,9 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
is None
or (subentry := entry.subentries.get(self._current_subentry_id)) is None
):
raise HomeAssistantError("Subentry not found")
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="subentry_not_found"
)
updated_data = {
**subentry.data,
@@ -190,4 +192,6 @@ async def async_create_fix_flow(
"""Create flow."""
if issue_id == "model_deprecated":
return ModelDeprecatedRepairFlow()
raise HomeAssistantError("Unknown issue ID")
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="unknown_issue_id"
)
@@ -149,6 +149,47 @@
}
}
},
"exceptions": {
"api_authentication_error": {
"message": "Authentication error with Anthropic API: {message}. Reauthentication required."
},
"api_error": {
"message": "Anthropic API error: {message}."
},
"api_refusal": {
"message": "Potential policy violation detected."
},
"json_parse_error": {
"message": "Error with Claude structured response."
},
"response_not_found": {
"message": "Last content in chat log is not an AssistantContent."
},
"subentry_not_found": {
"message": "Subentry not found."
},
"system_message_not_found": {
"message": "First message must be a system message."
},
"unexpected_chat_log_content": {
"message": "Unexpected content type in chat log: {type}."
},
"unexpected_stream_object": {
"message": "Expected a stream of messages."
},
"unknown_issue_id": {
"message": "Unknown issue ID."
},
"user_message_not_found": {
"message": "Last message must be a user message to add attachments."
},
"wrong_file_path": {
"message": "`{file_path}` does not exist."
},
"wrong_file_type": {
"message": "Only images and PDF are supported by the Anthropic API, `{file_path}` ({mime_type}) is not an image file or PDF."
}
},
"issues": {
"model_deprecated": {
"fix_flow": {
+11 -1
View File
@@ -30,9 +30,10 @@ from homeassistant.const import (
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_CREDENTIALS,
@@ -42,9 +43,12 @@ from .const import (
SIGNAL_CONNECTED,
SIGNAL_DISCONNECTED,
)
from .services import async_setup_services
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
DEFAULT_NAME_TV = "Apple TV"
DEFAULT_NAME_HP = "HomePod"
@@ -77,6 +81,12 @@ DEVICE_EXCEPTIONS = (
type AppleTvConfigEntry = ConfigEntry[AppleTVManager]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Apple TV component."""
async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: AppleTvConfigEntry) -> bool:
"""Set up a config entry for Apple TV."""
manager = AppleTVManager(hass, entry)
@@ -9,3 +9,5 @@ CONF_START_OFF = "start_off"
SIGNAL_CONNECTED = "apple_tv_connected"
SIGNAL_DISCONNECTED = "apple_tv_disconnected"
ATTR_TEXT = "text"
@@ -8,5 +8,16 @@
}
}
}
},
"services": {
"append_keyboard_text": {
"service": "mdi:keyboard"
},
"clear_keyboard_text": {
"service": "mdi:keyboard-off"
},
"set_keyboard_text": {
"service": "mdi:keyboard"
}
}
}
@@ -0,0 +1,130 @@
"""Define services for the Apple TV integration."""
from __future__ import annotations
from pyatv.const import KeyboardFocusState
from pyatv.exceptions import NotSupportedError, ProtocolError
from pyatv.interface import AppleTV as AppleTVInterface
import voluptuous as vol
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv, service
from .const import ATTR_TEXT, DOMAIN
SERVICE_SET_KEYBOARD_TEXT = "set_keyboard_text"
SERVICE_SET_KEYBOARD_TEXT_SCHEMA = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string,
vol.Required(ATTR_TEXT): cv.string,
}
)
SERVICE_APPEND_KEYBOARD_TEXT = "append_keyboard_text"
SERVICE_APPEND_KEYBOARD_TEXT_SCHEMA = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string,
vol.Required(ATTR_TEXT): cv.string,
}
)
SERVICE_CLEAR_KEYBOARD_TEXT = "clear_keyboard_text"
SERVICE_CLEAR_KEYBOARD_TEXT_SCHEMA = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string,
}
)
def _get_atv(call: ServiceCall) -> AppleTVInterface:
"""Get the AppleTVInterface for a service call."""
entry = service.async_get_config_entry(
call.hass, DOMAIN, call.data[ATTR_CONFIG_ENTRY_ID]
)
atv: AppleTVInterface | None = entry.runtime_data.atv
if atv is None:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="not_connected",
)
return atv
def _check_keyboard_focus(atv: AppleTVInterface) -> None:
"""Check that keyboard is focused on the device."""
try:
focus_state = atv.keyboard.text_focus_state
except NotSupportedError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="keyboard_not_available",
) from err
if focus_state != KeyboardFocusState.Focused:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="keyboard_not_focused",
)
async def _async_set_keyboard_text(call: ServiceCall) -> None:
"""Set text in the keyboard input field on an Apple TV."""
atv = _get_atv(call)
_check_keyboard_focus(atv)
try:
await atv.keyboard.text_set(call.data[ATTR_TEXT])
except ProtocolError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="keyboard_error",
) from err
async def _async_append_keyboard_text(call: ServiceCall) -> None:
"""Append text to the keyboard input field on an Apple TV."""
atv = _get_atv(call)
_check_keyboard_focus(atv)
try:
await atv.keyboard.text_append(call.data[ATTR_TEXT])
except ProtocolError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="keyboard_error",
) from err
async def _async_clear_keyboard_text(call: ServiceCall) -> None:
"""Clear text in the keyboard input field on an Apple TV."""
atv = _get_atv(call)
_check_keyboard_focus(atv)
try:
await atv.keyboard.text_clear()
except ProtocolError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="keyboard_error",
) from err
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the services for the Apple TV integration."""
hass.services.async_register(
DOMAIN,
SERVICE_SET_KEYBOARD_TEXT,
_async_set_keyboard_text,
schema=SERVICE_SET_KEYBOARD_TEXT_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_APPEND_KEYBOARD_TEXT,
_async_append_keyboard_text,
schema=SERVICE_APPEND_KEYBOARD_TEXT_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_CLEAR_KEYBOARD_TEXT,
_async_clear_keyboard_text,
schema=SERVICE_CLEAR_KEYBOARD_TEXT_SCHEMA,
)
@@ -0,0 +1,31 @@
set_keyboard_text:
fields:
config_entry_id:
required: true
selector:
config_entry:
integration: apple_tv
text:
required: true
selector:
text:
append_keyboard_text:
fields:
config_entry_id:
required: true
selector:
config_entry:
integration: apple_tv
text:
required: true
selector:
text:
clear_keyboard_text:
fields:
config_entry_id:
required: true
selector:
config_entry:
integration: apple_tv
@@ -69,6 +69,20 @@
}
}
},
"exceptions": {
"keyboard_error": {
"message": "An error occurred while sending text to the Apple TV"
},
"keyboard_not_available": {
"message": "Keyboard input is not supported by this device"
},
"keyboard_not_focused": {
"message": "No text input field is currently focused on the Apple TV"
},
"not_connected": {
"message": "Apple TV is not connected"
}
},
"options": {
"step": {
"init": {
@@ -78,5 +92,45 @@
"description": "Configure general device settings"
}
}
},
"services": {
"append_keyboard_text": {
"description": "Appends text to the currently focused text input field on an Apple TV.",
"fields": {
"config_entry_id": {
"description": "[%key:component::apple_tv::services::set_keyboard_text::fields::config_entry_id::description%]",
"name": "[%key:component::apple_tv::services::set_keyboard_text::fields::config_entry_id::name%]"
},
"text": {
"description": "The text to append.",
"name": "[%key:component::apple_tv::services::set_keyboard_text::fields::text::name%]"
}
},
"name": "Append keyboard text"
},
"clear_keyboard_text": {
"description": "Clears the text in the currently focused text input field on an Apple TV.",
"fields": {
"config_entry_id": {
"description": "[%key:component::apple_tv::services::set_keyboard_text::fields::config_entry_id::description%]",
"name": "[%key:component::apple_tv::services::set_keyboard_text::fields::config_entry_id::name%]"
}
},
"name": "Clear keyboard text"
},
"set_keyboard_text": {
"description": "Sets the text in the currently focused text input field on an Apple TV.",
"fields": {
"config_entry_id": {
"description": "The Apple TV to send text to.",
"name": "Apple TV"
},
"text": {
"description": "The text to set.",
"name": "Text"
}
},
"name": "Set keyboard text"
}
}
}
+16 -21
View File
@@ -2,8 +2,8 @@
import asyncio
from asyncio import timeout
from contextlib import AsyncExitStack
import logging
from typing import Any
from arcam.fmj import ConnectionFailed
from arcam.fmj.client import Client
@@ -54,36 +54,31 @@ async def _run_client(
client = runtime_data.client
coordinators = runtime_data.coordinators
def _listen(_: Any) -> None:
for coordinator in coordinators.values():
coordinator.async_notify_data_updated()
while True:
try:
async with timeout(interval):
await client.start()
async with AsyncExitStack() as stack:
async with timeout(interval):
await client.start()
stack.push_async_callback(client.stop)
_LOGGER.debug("Client connected %s", client.host)
_LOGGER.debug("Client connected %s", client.host)
try:
for coordinator in coordinators.values():
await coordinator.state.start()
with client.listen(_listen):
try:
for coordinator in coordinators.values():
coordinator.async_notify_connected()
await client.process()
finally:
await client.stop()
await stack.enter_async_context(
coordinator.async_monitor_client()
)
_LOGGER.debug("Client disconnected %s", client.host)
for coordinator in coordinators.values():
coordinator.async_notify_disconnected()
await client.process()
finally:
_LOGGER.debug("Client disconnected %s", client.host)
except ConnectionFailed:
await asyncio.sleep(interval)
pass
except TimeoutError:
continue
except Exception:
_LOGGER.exception("Unexpected exception, aborting arcam client")
return
await asyncio.sleep(interval)
@@ -2,11 +2,13 @@
from __future__ import annotations
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from dataclasses import dataclass
import logging
from arcam.fmj import ConnectionFailed
from arcam.fmj.client import Client
from arcam.fmj.client import AmxDuetResponse, Client, ResponsePacket
from arcam.fmj.state import State
from homeassistant.config_entries import ConfigEntry
@@ -51,7 +53,7 @@ class ArcamFmjCoordinator(DataUpdateCoordinator[None]):
)
self.client = client
self.state = State(client, zone)
self.last_update_success = False
self.update_in_progress = False
name = config_entry.title
unique_id = config_entry.unique_id or config_entry.entry_id
@@ -74,24 +76,34 @@ class ArcamFmjCoordinator(DataUpdateCoordinator[None]):
async def _async_update_data(self) -> None:
"""Fetch data for manual refresh."""
try:
self.update_in_progress = True
await self.state.update()
except ConnectionFailed as err:
raise UpdateFailed(
f"Connection failed during update for zone {self.state.zn}"
) from err
finally:
self.update_in_progress = False
@callback
def async_notify_data_updated(self) -> None:
"""Notify that new data has been received from the device."""
self.async_set_updated_data(None)
def _async_notify_packet(self, packet: ResponsePacket | AmxDuetResponse) -> None:
"""Packet callback to detect changes to state."""
if (
not isinstance(packet, ResponsePacket)
or packet.zn != self.state.zn
or self.update_in_progress
):
return
@callback
def async_notify_connected(self) -> None:
"""Handle client connected."""
self.hass.async_create_task(self.async_refresh())
@callback
def async_notify_disconnected(self) -> None:
"""Handle client disconnected."""
self.last_update_success = False
self.async_update_listeners()
@asynccontextmanager
async def async_monitor_client(self) -> AsyncGenerator[None]:
"""Monitor a client and state for changes while connected."""
async with self.state:
self.hass.async_create_task(self.async_refresh())
try:
with self.client.listen(self._async_notify_packet):
yield
finally:
self.hass.async_create_task(self.async_refresh())
@@ -26,3 +26,8 @@ class ArcamFmjEntity(CoordinatorEntity[ArcamFmjCoordinator]):
if description is not None:
self._attr_unique_id = f"{self._attr_unique_id}-{description.key}"
self.entity_description = description
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self.coordinator.client.connected
@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["arcam"],
"requirements": ["arcam-fmj==1.8.2"],
"requirements": ["arcam-fmj==1.8.3"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
@@ -91,6 +91,7 @@ SENSORS: tuple[ArcamFmjSensorEntityDescription, ...] = (
value_fn=lambda state: (
vp.colorspace.name.lower()
if (vp := state.get_incoming_video_parameters()) is not None
and vp.colorspace is not None
else None
),
),
@@ -1 +1 @@
"""The Arris TG2492LG component."""
"""The Arris TG2492LG integration."""
+1 -1
View File
@@ -1 +1 @@
"""The aruba component."""
"""The Aruba integration."""
@@ -75,7 +75,7 @@
},
"services": {
"announce": {
"description": "Lets a satellite announce a message.",
"description": "Lets an Assist satellite announce a message.",
"fields": {
"media_id": {
"description": "The media ID to announce instead of using text-to-speech.",
@@ -94,10 +94,10 @@
"name": "Preannounce media ID"
}
},
"name": "Announce"
"name": "Announce on satellite"
},
"ask_question": {
"description": "Asks a question and gets the user's response.",
"description": "Lets an Assist satellite ask a question and get the user's response.",
"fields": {
"answers": {
"description": "Possible answers to the question.",
@@ -124,10 +124,10 @@
"name": "Question media ID"
}
},
"name": "Ask question"
"name": "Ask question on satellite"
},
"start_conversation": {
"description": "Starts a conversation from a satellite.",
"description": "Starts a conversation from an Assist satellite.",
"fields": {
"extra_system_prompt": {
"description": "Provide background information to the AI about the request.",
@@ -150,13 +150,13 @@
"name": "Message"
}
},
"name": "Start conversation"
"name": "Start conversation on satellite"
}
},
"title": "Assist satellite",
"triggers": {
"idle": {
"description": "Triggers after one or more voice assistant satellites become idle after having processed a command.",
"description": "Triggers after one or more Assist satellites become idle after having processed a command.",
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
@@ -165,7 +165,7 @@
"name": "Satellite became idle"
},
"listening": {
"description": "Triggers after one or more voice assistant satellites start listening for a command from someone.",
"description": "Triggers after one or more Assist satellites start listening for a command from someone.",
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
@@ -174,7 +174,7 @@
"name": "Satellite started listening"
},
"processing": {
"description": "Triggers after one or more voice assistant satellites start processing a command after having heard it.",
"description": "Triggers after one or more Assist satellites start processing a command after having heard it.",
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
@@ -183,7 +183,7 @@
"name": "Satellite started processing"
},
"responding": {
"description": "Triggers after one or more voice assistant satellites start responding to a command after having processed it, or start announcing something.",
"description": "Triggers after one or more Assist satellites start responding to a command after having processed it, or start announcing something.",
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
@@ -142,6 +142,13 @@ class WellKnownOAuthInfoView(HomeAssistantView):
"authorization_endpoint": f"{url_prefix}/auth/authorize",
"token_endpoint": f"{url_prefix}/auth/token",
"revocation_endpoint": f"{url_prefix}/auth/revoke",
# Home Assistant already accepts URL-based client_ids via
# IndieAuth without prior registration, which is compatible with
# draft-ietf-oauth-client-id-metadata-document. This flag
# advertises that support to encourage clients to use it. The
# metadata document is not actually fetched as IndieAuth doesn't
# require it.
"client_id_metadata_document_supported": True,
"response_types_supported": ["code"],
"service_documentation": (
"https://developers.home-assistant.io/docs/auth_api"
@@ -54,7 +54,7 @@
"message": "Storage account {account_name} not found"
},
"cannot_connect": {
"message": "Can not connect to storage account {account_name}"
"message": "Cannot connect to storage account {account_name}"
},
"container_not_found": {
"message": "Storage container {container_name} not found"
+1 -1
View File
@@ -1 +1 @@
"""The bbox component."""
"""The Bbox integration."""
+1 -1
View File
@@ -1 +1 @@
"""The bitcoin component."""
"""The Bitcoin integration."""
@@ -1,7 +1,7 @@
{
"domain": "blebox",
"name": "BleBox devices",
"codeowners": ["@bbx-a", "@swistakm"],
"codeowners": ["@bbx-a", "@swistakm", "@bkobus-bbx"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/blebox",
"integration_type": "device",
@@ -1 +1 @@
"""The blinksticklight component."""
"""The BlinkStick integration."""
@@ -1,4 +1,4 @@
"""Support for Blinkstick lights."""
"""Support for BlinkStick lights."""
# mypy: ignore-errors
from __future__ import annotations
@@ -40,7 +40,7 @@ def setup_platform(
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up Blinkstick device specified by serial number."""
"""Set up BlinkStick device specified by serial number."""
name = config[CONF_NAME]
serial = config[CONF_SERIAL]
+3 -1
View File
@@ -52,7 +52,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Rotate the access token."""
access_tokens.append(hex(_RND.getrandbits(256))[2:])
async_track_time_interval(hass, _rotate_token, TOKEN_CHANGE_INTERVAL)
async_track_time_interval(
hass, _rotate_token, TOKEN_CHANGE_INTERVAL, cancel_on_shutdown=True
)
hass.http.register_view(BrandsIntegrationView(hass))
hass.http.register_view(BrandsHardwareView(hass))
@@ -578,13 +578,13 @@ class CalendarEntity(Entity):
return STATE_OFF
@callback
def async_write_ha_state(self) -> None:
def _async_write_ha_state(self) -> None:
"""Write the state to the state machine.
This sets up listeners to handle state transitions for start or end of
the current or upcoming event.
"""
super().async_write_ha_state()
super()._async_write_ha_state()
if self._alarm_unsubs is None:
self._alarm_unsubs = []
_LOGGER.debug(
+2 -2
View File
@@ -760,12 +760,12 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
return CameraCapabilities(frontend_stream_types)
@callback
def async_write_ha_state(self) -> None:
def _async_write_ha_state(self) -> None:
"""Write the state to the state machine.
Schedules async_refresh_providers if support of streams have changed.
"""
super().async_write_ha_state()
super()._async_write_ha_state()
if self.__supports_stream != (
supports_stream := self.supported_features & CameraEntityFeature.STREAM
):
@@ -11,7 +11,13 @@ from homeassistant.exceptions import ConfigEntryNotReady
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT]
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.LIGHT,
Platform.SELECT,
Platform.SENSOR,
]
async def async_setup_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -> bool:
@@ -4,7 +4,11 @@ from __future__ import annotations
from pycasperglow import GlowState
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -21,7 +25,12 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the binary sensor platform for Casper Glow."""
async_add_entities([CasperGlowPausedBinarySensor(entry.runtime_data)])
async_add_entities(
[
CasperGlowPausedBinarySensor(entry.runtime_data),
CasperGlowChargingBinarySensor(entry.runtime_data),
]
)
class CasperGlowPausedBinarySensor(CasperGlowEntity, BinarySensorEntity):
@@ -46,6 +55,34 @@ class CasperGlowPausedBinarySensor(CasperGlowEntity, BinarySensorEntity):
@callback
def _async_handle_state_update(self, state: GlowState) -> None:
"""Handle a state update from the device."""
if state.is_paused is not None:
if state.is_paused is not None and state.is_paused != self._attr_is_on:
self._attr_is_on = state.is_paused
self.async_write_ha_state()
self.async_write_ha_state()
class CasperGlowChargingBinarySensor(CasperGlowEntity, BinarySensorEntity):
"""Binary sensor indicating whether the Casper Glow is charging."""
_attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(self, coordinator: CasperGlowCoordinator) -> None:
"""Initialize the charging binary sensor."""
super().__init__(coordinator)
self._attr_unique_id = f"{format_mac(coordinator.device.address)}_charging"
if coordinator.device.state.is_charging is not None:
self._attr_is_on = coordinator.device.state.is_charging
async def async_added_to_hass(self) -> None:
"""Register state update callback when entity is added."""
await super().async_added_to_hass()
self.async_on_remove(
self._device.register_callback(self._async_handle_state_update)
)
@callback
def _async_handle_state_update(self, state: GlowState) -> None:
"""Handle a state update from the device."""
if state.is_charging is not None and state.is_charging != self._attr_is_on:
self._attr_is_on = state.is_charging
self.async_write_ha_state()
@@ -12,5 +12,7 @@ SORTED_BRIGHTNESS_LEVELS = sorted(BRIGHTNESS_LEVELS)
DEFAULT_DIMMING_TIME_MINUTES: int = DIMMING_TIME_MINUTES[0]
DIMMING_TIME_OPTIONS: tuple[str, ...] = tuple(str(m) for m in DIMMING_TIME_MINUTES)
# Interval between periodic state polls to catch externally-triggered changes.
STATE_POLL_INTERVAL = timedelta(seconds=30)
@@ -19,7 +19,7 @@ from homeassistant.components.bluetooth.active_update_coordinator import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from .const import STATE_POLL_INTERVAL
from .const import SORTED_BRIGHTNESS_LEVELS, STATE_POLL_INTERVAL
_LOGGER = logging.getLogger(__name__)
@@ -51,6 +51,15 @@ class CasperGlowCoordinator(ActiveBluetoothDataUpdateCoordinator[None]):
)
self.title = title
# The device API couples brightness and dimming time into a
# single command (set_brightness_and_dimming_time), so both
# values must be tracked here for cross-entity use.
self.last_brightness_pct: int = (
device.state.brightness_level
if device.state.brightness_level is not None
else SORTED_BRIGHTNESS_LEVELS[0]
)
@callback
def _needs_poll(
self,
@@ -0,0 +1,31 @@
"""Diagnostics support for the Casper Glow integration."""
from __future__ import annotations
from typing import Any
from homeassistant.components import bluetooth
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.core import HomeAssistant
from .coordinator import CasperGlowConfigEntry
SERVICE_INFO_TO_REDACT = frozenset({"address", "name", "source", "device"})
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: CasperGlowConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data
service_info = bluetooth.async_last_service_info(
hass, coordinator.device.address, connectable=True
)
return {
"service_info": async_redact_data(
service_info.as_dict() if service_info else None,
SERVICE_INFO_TO_REDACT,
),
}
@@ -12,6 +12,11 @@
"resume": {
"default": "mdi:play"
}
},
"select": {
"dimming_time": {
"default": "mdi:timer-outline"
}
}
}
}
@@ -71,6 +71,7 @@ class CasperGlowLight(CasperGlowEntity, LightEntity):
self._attr_color_mode = ColorMode.BRIGHTNESS
if state.brightness_level is not None:
self._attr_brightness = _device_pct_to_ha_brightness(state.brightness_level)
self.coordinator.last_brightness_pct = state.brightness_level
@callback
def _async_handle_state_update(self, state: GlowState) -> None:
@@ -97,6 +98,7 @@ class CasperGlowLight(CasperGlowEntity, LightEntity):
)
)
self._attr_brightness = _device_pct_to_ha_brightness(brightness_pct)
self.coordinator.last_brightness_pct = brightness_pct
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
@@ -15,5 +15,5 @@
"iot_class": "local_polling",
"loggers": ["pycasperglow"],
"quality_scale": "silver",
"requirements": ["pycasperglow==1.1.0"]
"requirements": ["pycasperglow==1.2.0"]
}
@@ -39,7 +39,7 @@ rules:
# Gold
devices: done
diagnostics: todo
diagnostics: done
discovery-update-info:
status: exempt
comment: No network discovery.
@@ -52,14 +52,16 @@ rules:
docs-troubleshooting: done
docs-use-cases: todo
dynamic-devices: todo
entity-category: todo
entity-device-class: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default: todo
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow: todo
repair-issues: todo
repair-issues:
status: exempt
comment: Integration does not register repair issues.
stale-devices: todo
# Platinum
@@ -0,0 +1,92 @@
"""Casper Glow integration select platform for dimming time."""
from __future__ import annotations
from pycasperglow import GlowState
from homeassistant.components.select import SelectEntity
from homeassistant.const import EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from .const import DIMMING_TIME_OPTIONS
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
from .entity import CasperGlowEntity
PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant,
entry: CasperGlowConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the select platform for Casper Glow."""
async_add_entities([CasperGlowDimmingTimeSelect(entry.runtime_data)])
class CasperGlowDimmingTimeSelect(CasperGlowEntity, SelectEntity, RestoreEntity):
"""Select entity for Casper Glow dimming time."""
_attr_translation_key = "dimming_time"
_attr_entity_category = EntityCategory.CONFIG
_attr_options = list(DIMMING_TIME_OPTIONS)
_attr_unit_of_measurement = UnitOfTime.MINUTES
def __init__(self, coordinator: CasperGlowCoordinator) -> None:
"""Initialize the dimming time select entity."""
super().__init__(coordinator)
self._attr_unique_id = f"{format_mac(coordinator.device.address)}_dimming_time"
@property
def current_option(self) -> str | None:
"""Return the currently selected dimming time from the coordinator."""
if self.coordinator.last_dimming_time_minutes is None:
return None
return str(self.coordinator.last_dimming_time_minutes)
async def async_added_to_hass(self) -> None:
"""Restore last known dimming time and register state update callback."""
await super().async_added_to_hass()
if self.coordinator.last_dimming_time_minutes is None and (
last_state := await self.async_get_last_state()
):
if last_state.state in DIMMING_TIME_OPTIONS:
self.coordinator.last_dimming_time_minutes = int(last_state.state)
self.async_on_remove(
self._device.register_callback(self._async_handle_state_update)
)
@callback
def _async_handle_state_update(self, state: GlowState) -> None:
"""Handle a state update from the device."""
if state.brightness_level is not None:
self.coordinator.last_brightness_pct = state.brightness_level
if (
state.configured_dimming_time_minutes is not None
and self.coordinator.last_dimming_time_minutes is None
):
self.coordinator.last_dimming_time_minutes = (
state.configured_dimming_time_minutes
)
# Dimming time is not part of the device state
# that is provided via BLE update. Therefore
# we need to trigger a state update for the select entity
# to update the current state.
self.async_write_ha_state()
async def async_select_option(self, option: str) -> None:
"""Set the dimming time."""
await self._async_command(
self._device.set_brightness_and_dimming_time(
self.coordinator.last_brightness_pct, int(option)
)
)
self.coordinator.last_dimming_time_minutes = int(option)
# Dimming time is not part of the device state
# that is provided via BLE update. Therefore
# we need to trigger a state update for the select entity
# to update the current state.
self.async_write_ha_state()
@@ -0,0 +1,61 @@
"""Casper Glow integration sensor platform."""
from __future__ import annotations
from pycasperglow import GlowState
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
from .entity import CasperGlowEntity
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
entry: CasperGlowConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the sensor platform for Casper Glow."""
async_add_entities([CasperGlowBatterySensor(entry.runtime_data)])
class CasperGlowBatterySensor(CasperGlowEntity, SensorEntity):
"""Sensor entity for Casper Glow battery level."""
_attr_device_class = SensorDeviceClass.BATTERY
_attr_native_unit_of_measurement = PERCENTAGE
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(self, coordinator: CasperGlowCoordinator) -> None:
"""Initialize the battery sensor."""
super().__init__(coordinator)
self._attr_unique_id = f"{format_mac(coordinator.device.address)}_battery"
if coordinator.device.state.battery_level is not None:
self._attr_native_value = coordinator.device.state.battery_level.percentage
async def async_added_to_hass(self) -> None:
"""Register state update callback when entity is added."""
await super().async_added_to_hass()
self.async_on_remove(
self._device.register_callback(self._async_handle_state_update)
)
@callback
def _async_handle_state_update(self, state: GlowState) -> None:
"""Handle a state update from the device."""
if state.battery_level is not None:
new_value = state.battery_level.percentage
if new_value != self._attr_native_value:
self._attr_native_value = new_value
self.async_write_ha_state()
@@ -39,6 +39,11 @@
"resume": {
"name": "Resume dimming"
}
},
"select": {
"dimming_time": {
"name": "Dimming time"
}
}
},
"exceptions": {
@@ -239,7 +239,7 @@
"message": "Provided humidity {humidity} is not valid. Accepted range is {min_humidity} to {max_humidity}."
},
"low_temp_higher_than_high_temp": {
"message": "'Lower target temperature' can not be higher than 'Upper target temperature'."
"message": "'Lower target temperature' cannot be higher than 'Upper target temperature'."
},
"missing_target_temperature_entity_feature": {
"message": "Set temperature action was used with the 'Target temperature' parameter but the entity does not support it."
+2 -3
View File
@@ -9,7 +9,6 @@ from aiocomelit import ComelitSerialBridgeObject
from aiocomelit.const import CLIMATE
from homeassistant.components.climate import (
DOMAIN as CLIMATE_DOMAIN,
ClimateEntity,
ClimateEntityFeature,
HVACAction,
@@ -92,7 +91,7 @@ async def async_setup_entry(
entities: list[ClimateEntity] = []
for device in coordinator.data[CLIMATE].values():
values = load_api_data(device, CLIMATE_DOMAIN)
values = load_api_data(device, "climate")
if values[0] == 0 and values[4] == 0:
# No climate data, device is only a humidifier/dehumidifier
@@ -140,7 +139,7 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
def _update_attributes(self) -> None:
"""Update class attributes."""
device = self.coordinator.data[CLIMATE][self._device.index]
values = load_api_data(device, CLIMATE_DOMAIN)
values = load_api_data(device, "climate")
_active = values[1]
_mode = values[2] # Values from API: "O", "L", "U"
@@ -9,7 +9,6 @@ from aiocomelit import ComelitSerialBridgeObject
from aiocomelit.const import CLIMATE
from homeassistant.components.humidifier import (
DOMAIN as HUMIDIFIER_DOMAIN,
MODE_AUTO,
MODE_NORMAL,
HumidifierAction,
@@ -68,7 +67,7 @@ async def async_setup_entry(
entities: list[ComelitHumidifierEntity] = []
for device in coordinator.data[CLIMATE].values():
values = load_api_data(device, HUMIDIFIER_DOMAIN)
values = load_api_data(device, "humidifier")
if values[0] == 0 and values[4] == 0:
# No humidity data, device is only a climate
@@ -142,7 +141,7 @@ class ComelitHumidifierEntity(ComelitBridgeBaseEntity, HumidifierEntity):
def _update_attributes(self) -> None:
"""Update class attributes."""
device = self.coordinator.data[CLIMATE][self._device.index]
values = load_api_data(device, HUMIDIFIER_DOMAIN)
values = load_api_data(device, "humidifier")
_active = values[1]
_mode = values[2] # Values from API: "O", "L", "U"
@@ -113,9 +113,6 @@
"humidity_while_off": {
"message": "Cannot change humidity while off"
},
"invalid_clima_data": {
"message": "Invalid 'clima' data"
},
"update_failed": {
"message": "Failed to update data: {error}"
}
+10 -9
View File
@@ -2,13 +2,12 @@
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from typing import TYPE_CHECKING, Any, Concatenate
from typing import TYPE_CHECKING, Any, Concatenate, Literal
from aiocomelit.api import ComelitSerialBridgeObject
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
from aiohttp import ClientSession, CookieJar
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -30,17 +29,19 @@ async def async_client_session(hass: HomeAssistant) -> ClientSession:
)
def load_api_data(device: ComelitSerialBridgeObject, domain: str) -> list[Any]:
def load_api_data(
device: ComelitSerialBridgeObject,
domain: Literal["climate", "humidifier"],
) -> list[Any]:
"""Load data from the API."""
# This function is called when the data is loaded from the API
if not isinstance(device.val, list):
raise HomeAssistantError(
translation_domain=domain, translation_key="invalid_clima_data"
)
# This function is called when the data is loaded from the API.
# For climate and humidifier device.val is always a list.
if TYPE_CHECKING:
assert isinstance(device.val, list)
# CLIMATE has a 2 item tuple:
# - first for Clima
# - second for Humidifier
return device.val[0] if domain == CLIMATE_DOMAIN else device.val[1]
return device.val[0] if domain == "climate" else device.val[1]
async def cleanup_stale_entity(
@@ -210,7 +210,7 @@ def websocket_update_entity(
)
return
changes = {}
changes: dict[str, Any] = {}
for key in (
"area_id",
+6 -6
View File
@@ -44,18 +44,18 @@ class DemoRemote(RemoteEntity):
return {"last_command_sent": self._last_command_sent}
return None
def turn_on(self, **kwargs: Any) -> None:
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the remote on."""
self._attr_is_on = True
self.schedule_update_ha_state()
self.async_write_ha_state()
def turn_off(self, **kwargs: Any) -> None:
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the remote off."""
self._attr_is_on = False
self.schedule_update_ha_state()
self.async_write_ha_state()
def send_command(self, command: Iterable[str], **kwargs: Any) -> None:
async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
"""Send a command to a device."""
for com in command:
self._last_command_sent = com
self.schedule_update_ha_state()
self.async_write_ha_state()
+5 -5
View File
@@ -61,12 +61,12 @@ class DemoSwitch(SwitchEntity):
name=device_name,
)
def turn_on(self, **kwargs: Any) -> None:
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
self._attr_is_on = True
self.schedule_update_ha_state()
self.async_write_ha_state()
def turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
self._attr_is_on = False
self.schedule_update_ha_state()
self.async_write_ha_state()
+1 -1
View File
@@ -1 +1 @@
"""The denon component."""
"""The Denon Network Receivers integration."""
@@ -1,4 +1,4 @@
"""The denonavr component."""
"""The Denon AVR Network Receivers integration."""
import logging
@@ -21,6 +21,7 @@ from .const import ( # noqa: F401
ATTR_DEV_ID,
ATTR_GPS,
ATTR_HOST_NAME,
ATTR_IN_ZONES,
ATTR_IP,
ATTR_LOCATION_NAME,
ATTR_MAC,
@@ -3,7 +3,7 @@
from __future__ import annotations
import asyncio
from typing import final
from typing import Any, final
from propcache.api import cached_property
@@ -18,7 +18,7 @@ from homeassistant.const import (
STATE_NOT_HOME,
EntityCategory,
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.core import Event, HomeAssistant, State, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.device_registry import (
DeviceInfo,
@@ -33,6 +33,7 @@ from homeassistant.util.hass_dict import HassKey
from .const import (
ATTR_HOST_NAME,
ATTR_IN_ZONES,
ATTR_IP,
ATTR_MAC,
ATTR_SOURCE_TYPE,
@@ -223,6 +224,9 @@ class TrackerEntity(
_attr_longitude: float | None = None
_attr_source_type: SourceType = SourceType.GPS
__active_zone: State | None = None
__in_zones: list[str] | None = None
@cached_property
def should_poll(self) -> bool:
"""No polling for entities that have location pushed."""
@@ -256,6 +260,18 @@ class TrackerEntity(
"""Return longitude value of the device."""
return self._attr_longitude
@callback
def _async_write_ha_state(self) -> None:
"""Calculate active zones."""
if self.available and self.latitude is not None and self.longitude is not None:
self.__active_zone, self.__in_zones = zone.async_in_zones(
self.hass, self.latitude, self.longitude, self.location_accuracy
)
else:
self.__active_zone = None
self.__in_zones = None
super()._async_write_ha_state()
@property
def state(self) -> str | None:
"""Return the state of the device."""
@@ -263,9 +279,7 @@ class TrackerEntity(
return self.location_name
if self.latitude is not None and self.longitude is not None:
zone_state = zone.async_active_zone(
self.hass, self.latitude, self.longitude, self.location_accuracy
)
zone_state = self.__active_zone
if zone_state is None:
state = STATE_NOT_HOME
elif zone_state.entity_id == zone.ENTITY_ID_HOME:
@@ -278,12 +292,13 @@ class TrackerEntity(
@final
@property
def state_attributes(self) -> dict[str, StateType]:
def state_attributes(self) -> dict[str, Any]:
"""Return the device state attributes."""
attr: dict[str, StateType] = {}
attr: dict[str, Any] = {ATTR_IN_ZONES: []}
attr.update(super().state_attributes)
if self.latitude is not None and self.longitude is not None:
attr[ATTR_IN_ZONES] = self.__in_zones or []
attr[ATTR_LATITUDE] = self.latitude
attr[ATTR_LONGITUDE] = self.longitude
attr[ATTR_GPS_ACCURACY] = self.location_accuracy
@@ -43,6 +43,7 @@ ATTR_BATTERY: Final = "battery"
ATTR_DEV_ID: Final = "dev_id"
ATTR_GPS: Final = "gps"
ATTR_HOST_NAME: Final = "host_name"
ATTR_IN_ZONES: Final = "in_zones"
ATTR_LOCATION_NAME: Final = "location_name"
ATTR_MAC: Final = "mac"
ATTR_SOURCE_TYPE: Final = "source_type"
@@ -2,6 +2,8 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from devolo_home_control_api.devices.zwave import Zwave
from devolo_home_control_api.homecontrol import HomeControl
@@ -188,6 +190,8 @@ class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity):
def sync_callback(self, message: tuple) -> None:
"""Update the consumption sensor state."""
if message[0] == self._attr_unique_id:
if TYPE_CHECKING:
assert self._attr_unique_id is not None
self._value = getattr(
self._device_instance.consumption_property[self._attr_unique_id],
self._sensor_type,
@@ -353,10 +353,10 @@ class DlnaDmrEntity(MediaPlayerEntity):
# Device was de/re-connected, state might have changed
self.async_write_ha_state()
def async_write_ha_state(self) -> None:
def _async_write_ha_state(self) -> None:
"""Write the state."""
self._attr_supported_features = self._supported_features()
super().async_write_ha_state()
super()._async_write_ha_state()
async def _device_connect(self, location: str) -> None:
"""Connect to the device now that it's available."""
+2 -2
View File
@@ -1,4 +1,4 @@
"""The dnsip component."""
"""The DNS IP integration."""
from __future__ import annotations
@@ -17,7 +17,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload dnsip config entry."""
"""Unload DNS IP config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -0,0 +1,64 @@
"""The Dropbox integration."""
from __future__ import annotations
from python_dropbox_api import (
DropboxAPIClient,
DropboxAuthException,
DropboxUnknownException,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
from .auth import DropboxConfigEntryAuth
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN
type DropboxConfigEntry = ConfigEntry[DropboxAPIClient]
async def async_setup_entry(hass: HomeAssistant, entry: DropboxConfigEntry) -> bool:
"""Set up Dropbox from a config entry."""
try:
oauth2_implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
oauth2_session = OAuth2Session(hass, entry, oauth2_implementation)
auth = DropboxConfigEntryAuth(
aiohttp_client.async_get_clientsession(hass), oauth2_session
)
client = DropboxAPIClient(auth)
try:
await client.get_account_info()
except DropboxAuthException as err:
raise ConfigEntryAuthFailed from err
except (DropboxUnknownException, TimeoutError) as err:
raise ConfigEntryNotReady from err
entry.runtime_data = client
def async_notify_backup_listeners() -> None:
for listener in hass.data.get(DATA_BACKUP_AGENT_LISTENERS, []):
listener()
entry.async_on_unload(entry.async_on_state_change(async_notify_backup_listeners))
return True
async def async_unload_entry(hass: HomeAssistant, entry: DropboxConfigEntry) -> bool:
"""Unload a config entry."""
return True
@@ -0,0 +1,38 @@
"""Application credentials platform for the Dropbox integration."""
from homeassistant.components.application_credentials import ClientCredential
from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_entry_oauth2_flow import (
AbstractOAuth2Implementation,
LocalOAuth2ImplementationWithPkce,
)
from .const import OAUTH2_AUTHORIZE, OAUTH2_SCOPES, OAUTH2_TOKEN
async def async_get_auth_implementation(
hass: HomeAssistant, auth_domain: str, credential: ClientCredential
) -> AbstractOAuth2Implementation:
"""Return custom auth implementation."""
return DropboxOAuth2Implementation(
hass,
auth_domain,
credential.client_id,
OAUTH2_AUTHORIZE,
OAUTH2_TOKEN,
credential.client_secret,
)
class DropboxOAuth2Implementation(LocalOAuth2ImplementationWithPkce):
"""Custom Dropbox OAuth2 implementation to add the necessary authorize url parameters."""
@property
def extra_authorize_data(self) -> dict:
"""Extra data that needs to be appended to the authorize url."""
data: dict = {
"token_access_type": "offline",
"scope": " ".join(OAUTH2_SCOPES),
}
data.update(super().extra_authorize_data)
return data
+44
View File
@@ -0,0 +1,44 @@
"""Authentication for Dropbox."""
from typing import cast
from aiohttp import ClientSession
from python_dropbox_api import Auth
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
class DropboxConfigEntryAuth(Auth):
"""Provide Dropbox authentication tied to an OAuth2 based config entry."""
def __init__(
self,
websession: ClientSession,
oauth_session: OAuth2Session,
) -> None:
"""Initialize DropboxConfigEntryAuth."""
super().__init__(websession)
self._oauth_session = oauth_session
async def async_get_access_token(self) -> str:
"""Return a valid access token."""
await self._oauth_session.async_ensure_token_valid()
return cast(str, self._oauth_session.token["access_token"])
class DropboxConfigFlowAuth(Auth):
"""Provide authentication tied to a fixed token for the config flow."""
def __init__(
self,
websession: ClientSession,
token: str,
) -> None:
"""Initialize DropboxConfigFlowAuth."""
super().__init__(websession)
self._token = token
async def async_get_access_token(self) -> str:
"""Return the fixed access token."""
return self._token
+230
View File
@@ -0,0 +1,230 @@
"""Backup platform for the Dropbox integration."""
from collections.abc import AsyncIterator, Callable, Coroutine
from functools import wraps
import json
import logging
from typing import Any, Concatenate
from python_dropbox_api import (
DropboxAPIClient,
DropboxAuthException,
DropboxFileOrFolderNotFoundException,
DropboxUnknownException,
)
from homeassistant.components.backup import (
AgentBackup,
BackupAgent,
BackupAgentError,
BackupNotFound,
suggested_filename,
)
from homeassistant.core import HomeAssistant, callback
from . import DropboxConfigEntry
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN
_LOGGER = logging.getLogger(__name__)
def _suggested_filenames(backup: AgentBackup) -> tuple[str, str]:
"""Return the suggested filenames for the backup and metadata."""
base_name = suggested_filename(backup).rsplit(".", 1)[0]
return f"{base_name}.tar", f"{base_name}.metadata.json"
async def _async_string_iterator(content: str) -> AsyncIterator[bytes]:
"""Yield a string as a single bytes chunk."""
yield content.encode()
def handle_backup_errors[_R, **P](
func: Callable[Concatenate[DropboxBackupAgent, P], Coroutine[Any, Any, _R]],
) -> Callable[Concatenate[DropboxBackupAgent, P], Coroutine[Any, Any, _R]]:
"""Handle backup errors."""
@wraps(func)
async def wrapper(
self: DropboxBackupAgent, *args: P.args, **kwargs: P.kwargs
) -> _R:
try:
return await func(self, *args, **kwargs)
except DropboxFileOrFolderNotFoundException as err:
raise BackupNotFound(
f"Failed to {func.__name__.removeprefix('async_').replace('_', ' ')}"
) from err
except DropboxAuthException as err:
self._entry.async_start_reauth(self._hass)
raise BackupAgentError("Authentication error") from err
except DropboxUnknownException as err:
_LOGGER.error(
"Error during %s: %s",
func.__name__,
err,
)
_LOGGER.debug("Full error: %s", err, exc_info=True)
raise BackupAgentError(
f"Failed to {func.__name__.removeprefix('async_').replace('_', ' ')}"
) from err
return wrapper
async def async_get_backup_agents(
hass: HomeAssistant,
**kwargs: Any,
) -> list[BackupAgent]:
"""Return a list of backup agents."""
entries = hass.config_entries.async_loaded_entries(DOMAIN)
return [DropboxBackupAgent(hass, entry) for entry in entries]
@callback
def async_register_backup_agents_listener(
hass: HomeAssistant,
*,
listener: Callable[[], None],
**kwargs: Any,
) -> Callable[[], None]:
"""Register a listener to be called when agents are added or removed.
:return: A function to unregister the listener.
"""
hass.data.setdefault(DATA_BACKUP_AGENT_LISTENERS, []).append(listener)
@callback
def remove_listener() -> None:
"""Remove the listener."""
hass.data[DATA_BACKUP_AGENT_LISTENERS].remove(listener)
if not hass.data[DATA_BACKUP_AGENT_LISTENERS]:
del hass.data[DATA_BACKUP_AGENT_LISTENERS]
return remove_listener
class DropboxBackupAgent(BackupAgent):
"""Backup agent for the Dropbox integration."""
domain = DOMAIN
def __init__(self, hass: HomeAssistant, entry: DropboxConfigEntry) -> None:
"""Initialize the backup agent."""
super().__init__()
self._hass = hass
self._entry = entry
self.name = entry.title
assert entry.unique_id
self.unique_id = entry.unique_id
self._api: DropboxAPIClient = entry.runtime_data
async def _async_get_backups(self) -> list[tuple[AgentBackup, str]]:
"""Get backups and their corresponding file names."""
files = await self._api.list_folder("")
tar_files = {f.name for f in files if f.name.endswith(".tar")}
metadata_files = [f for f in files if f.name.endswith(".metadata.json")]
backups: list[tuple[AgentBackup, str]] = []
for metadata_file in metadata_files:
tar_name = metadata_file.name.removesuffix(".metadata.json") + ".tar"
if tar_name not in tar_files:
_LOGGER.warning(
"Found metadata file '%s' without matching backup file",
metadata_file.name,
)
continue
metadata_stream = self._api.download_file(f"/{metadata_file.name}")
raw = b"".join([chunk async for chunk in metadata_stream])
try:
data = json.loads(raw)
backup = AgentBackup.from_dict(data)
except (json.JSONDecodeError, ValueError, TypeError, KeyError) as err:
_LOGGER.warning(
"Skipping invalid metadata file '%s': %s",
metadata_file.name,
err,
)
continue
backups.append((backup, tar_name))
return backups
@handle_backup_errors
async def async_upload_backup(
self,
*,
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
backup: AgentBackup,
**kwargs: Any,
) -> None:
"""Upload a backup."""
backup_filename, metadata_filename = _suggested_filenames(backup)
backup_path = f"/{backup_filename}"
metadata_path = f"/{metadata_filename}"
file_stream = await open_stream()
await self._api.upload_file(backup_path, file_stream)
metadata_stream = _async_string_iterator(json.dumps(backup.as_dict()))
try:
await self._api.upload_file(metadata_path, metadata_stream)
except (
DropboxAuthException,
DropboxUnknownException,
):
await self._api.delete_file(backup_path)
raise
@handle_backup_errors
async def async_list_backups(self, **kwargs: Any) -> list[AgentBackup]:
"""List backups."""
return [backup for backup, _ in await self._async_get_backups()]
@handle_backup_errors
async def async_download_backup(
self,
backup_id: str,
**kwargs: Any,
) -> AsyncIterator[bytes]:
"""Download a backup file."""
backups = await self._async_get_backups()
for backup, filename in backups:
if backup.backup_id == backup_id:
return self._api.download_file(f"/{filename}")
raise BackupNotFound(f"Backup {backup_id} not found")
@handle_backup_errors
async def async_get_backup(
self,
backup_id: str,
**kwargs: Any,
) -> AgentBackup:
"""Return a backup."""
backups = await self._async_get_backups()
for backup, _ in backups:
if backup.backup_id == backup_id:
return backup
raise BackupNotFound(f"Backup {backup_id} not found")
@handle_backup_errors
async def async_delete_backup(
self,
backup_id: str,
**kwargs: Any,
) -> None:
"""Delete a backup file."""
backups = await self._async_get_backups()
for backup, tar_filename in backups:
if backup.backup_id == backup_id:
metadata_filename = tar_filename.removesuffix(".tar") + ".metadata.json"
await self._api.delete_file(f"/{tar_filename}")
await self._api.delete_file(f"/{metadata_filename}")
return
raise BackupNotFound(f"Backup {backup_id} not found")
@@ -0,0 +1,60 @@
"""Config flow for Dropbox."""
from collections.abc import Mapping
import logging
from typing import Any
from python_dropbox_api import DropboxAPIClient
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
from .auth import DropboxConfigFlowAuth
from .const import DOMAIN
class DropboxConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
"""Config flow to handle Dropbox OAuth2 authentication."""
DOMAIN = DOMAIN
@property
def logger(self) -> logging.Logger:
"""Return logger."""
return logging.getLogger(__name__)
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
"""Create an entry for the flow, or update existing entry."""
access_token = data[CONF_TOKEN][CONF_ACCESS_TOKEN]
auth = DropboxConfigFlowAuth(async_get_clientsession(self.hass), access_token)
client = DropboxAPIClient(auth)
account_info = await client.get_account_info()
await self.async_set_unique_id(account_info.account_id)
if self.source == SOURCE_REAUTH:
self._abort_if_unique_id_mismatch(reason="wrong_account")
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data=data
)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=account_info.email, data=data)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
if user_input is None:
return self.async_show_form(step_id="reauth_confirm")
return await self.async_step_user()
+19
View File
@@ -0,0 +1,19 @@
"""Constants for the Dropbox integration."""
from collections.abc import Callable
from homeassistant.util.hass_dict import HassKey
DOMAIN = "dropbox"
OAUTH2_AUTHORIZE = "https://www.dropbox.com/oauth2/authorize"
OAUTH2_TOKEN = "https://api.dropboxapi.com/oauth2/token"
OAUTH2_SCOPES = [
"account_info.read",
"files.content.read",
"files.content.write",
]
DATA_BACKUP_AGENT_LISTENERS: HassKey[list[Callable[[], None]]] = HassKey(
f"{DOMAIN}.backup_agent_listeners"
)
@@ -0,0 +1,13 @@
{
"domain": "dropbox",
"name": "Dropbox",
"after_dependencies": ["backup"],
"codeowners": ["@bdr99"],
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/dropbox",
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["python-dropbox-api==0.1.3"]
}
@@ -0,0 +1,112 @@
rules:
# Bronze
action-setup:
status: exempt
comment: Integration does not register any actions.
appropriate-polling:
status: exempt
comment: Integration does not poll.
brands: done
common-modules:
status: exempt
comment: Integration does not have any entities or coordinators.
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: Integration does not register any actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: Integration does not have any entities.
entity-unique-id:
status: exempt
comment: Integration does not have any entities.
has-entity-name:
status: exempt
comment: Integration does not have any entities.
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: Integration does not register any actions.
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: Integration does not have any configuration parameters.
docs-installation-parameters: done
entity-unavailable:
status: exempt
comment: Integration does not have any entities.
integration-owner: done
log-when-unavailable: todo
parallel-updates:
status: exempt
comment: Integration does not make any entity updates.
reauthentication-flow: done
test-coverage: done
# Gold
devices:
status: exempt
comment: Integration does not have any entities.
diagnostics:
status: exempt
comment: Integration does not have any data to diagnose.
discovery-update-info:
status: exempt
comment: Integration is a service.
discovery:
status: exempt
comment: Integration is a service.
docs-data-update:
status: exempt
comment: Integration does not update any data.
docs-examples:
status: exempt
comment: Integration only provides backup functionality.
docs-known-limitations: todo
docs-supported-devices:
status: exempt
comment: Integration does not support any devices.
docs-supported-functions: done
docs-troubleshooting: todo
docs-use-cases: done
dynamic-devices:
status: exempt
comment: Integration does not use any devices.
entity-category:
status: exempt
comment: Integration does not have any entities.
entity-device-class:
status: exempt
comment: Integration does not have any entities.
entity-disabled-by-default:
status: exempt
comment: Integration does not have any entities.
entity-translations:
status: exempt
comment: Integration does not have any entities.
exception-translations: todo
icon-translations:
status: exempt
comment: Integration does not have any entities.
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: Integration does not have any repairs.
stale-devices:
status: exempt
comment: Integration does not have any devices.
# Platinum
async-dependency: done
inject-websession: done
strict-typing: done
@@ -0,0 +1,35 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
"wrong_account": "Wrong account: Please authenticate with the correct account."
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
},
"step": {
"pick_implementation": {
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
},
"reauth_confirm": {
"description": "The Dropbox integration needs to re-authenticate your account.",
"title": "[%key:common::config_flow::title::reauth%]"
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
}
}
@@ -5,7 +5,7 @@
"invalid_identifier": "[%key:component::dwd_weather_warnings::config::error::invalid_identifier%]"
},
"error": {
"ambiguous_identifier": "The region identifier and device tracker can not be specified together.",
"ambiguous_identifier": "The region identifier and device tracker cannot be specified together.",
"attribute_not_found": "The required attributes 'Latitude' and 'Longitude' were not found in the specified device tracker.",
"entity_not_found": "The specified device tracker entity was not found.",
"invalid_identifier": "The specified region identifier / device tracker is invalid.",

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