Compare commits

...

675 Commits

Author SHA1 Message Date
Ludovic BOUÉ 0a8a1ff345 Update test_sensor.ambr to use list for aliases and add object_id_base for last change attributes 2026-03-19 22:50:10 +01:00
Ludovic BOUÉ 43b2e26993 Refactor device_to_ha mapping to use get method for SetpointChangeSource 2026-03-19 22:44:30 +01:00
Ludovic BOUÉ 68bea745d4 Merge branch 'dev' into setpoint_change_source 2026-03-19 22:35:53 +01:00
Hai-Nam Nguyen 21d06fdace Fix unit when plant power is above 1000W in Hypontech (#165959)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-19 21:52:59 +01:00
AlCalzone c8cf13ba19 Do not use moving states for Multilevel Switch CC v1-3 Z-Wave covers (#165909) 2026-03-19 21:30:26 +01:00
johanzander d9a29bd486 growatt_server: add translation keys to all raised exceptions (#165927)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2026-03-19 21:08:15 +01:00
Norbert Rittel bd0145cb8d Fix spelling of "Wi-Fi" trademark in user-facing string of sfr_box (#166019) 2026-03-19 20:43:16 +01:00
wollew d002b48335 Replace deprecated library call in Velux integration (#165996)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-19 19:29:35 +01:00
Norbert Rittel c66daf13d3 Fix spelling of "Wi-Fi" in user-facing strings of shelly (#166017) 2026-03-19 19:17:23 +01:00
Christian Lackas 1cae0e3cd3 Bump homematicip to 2.7.0 (#166012) 2026-03-19 17:53:12 +00:00
Paul Tarjan de93d1d52a Skip unmapped and watchdog event types in Hikvision NVR event injection (#165009)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 18:39:28 +01:00
Tucker Kern c67438c515 Snapcast: Fix incorrect identifier extraction in async_join_players (#165020) 2026-03-19 18:36:42 +01:00
Linkplay2020 fa57f72f37 Add WiiM media player integration (#148948)
Co-authored-by: Tao Jiang <tao.jiang@linkplay.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-19 18:33:53 +01:00
Tom Matheussen 29309d1315 Add reconfigure flow to Satel Integra (#164938)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-19 18:31:46 +01:00
Robin Lintermann 130e0db742 Change codeowner of smarla integration (#166015) 2026-03-19 18:30:24 +01:00
Willem-Jan van Rootselaar 450d46f652 Fix optional static values in bsblan (#165488) 2026-03-19 17:07:49 +00:00
DeerMaximum 625603839c Remove DeerMaximum from velux codeowners (#166014) 2026-03-19 17:39:55 +01:00
Michael Hansen fb66d766a8 Ensure STT metadata enums are passed (#165220) 2026-03-19 17:38:43 +01:00
Paul Bottein e5f13b4126 Add state_attr_translated template filter and function (#165317)
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: piitaya <5878303+piitaya@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-19 17:21:43 +01:00
Raj Laud 4a22f2c93e Add reauth flow and auto-trigger to victron_ble integration (#165729)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-19 17:01:04 +01:00
Mike Degatano a5c48b190a Remove get_issues_info from hassio __all__ (#165929) 2026-03-19 16:58:20 +01:00
epenet 5e1a0e2152 Use annotationlib.get_annotations in entity helper (#165331) 2026-03-19 15:27:42 +01:00
Hai-Nam Nguyen 9a5516bb1d Bump hyponcloud from 0.3.0 to 0.9.0 (#166005) 2026-03-19 15:25:44 +01:00
J. Diego Rodríguez Royo b9172cf4a8 Add 3D heating, air fry, and grill programs to Home Connect (#166006) 2026-03-19 15:21:20 +01:00
Ariel Ebersberger 8e4dc29226 Fix backblaze_b2 tests for Python 3.14.3 (#165930) 2026-03-19 14:01:27 +01:00
epenet b152f2f9a6 Add test fixture for Tuya WiFi smart online 8 in 1 tester (#166003) 2026-03-19 13:27:42 +01:00
epenet abca80dc13 Simplify mocking of Tuya device notifications (#165998) 2026-03-19 13:24:10 +01:00
Ville Skyttä 6869369ab2 Add some EZVIZ sensor icons (#166000) 2026-03-19 13:23:33 +01:00
Brett Adams c2dde06713 Fix mixed-language Splunk setup errors in exception translations (#165974) 2026-03-19 13:21:45 +01:00
Retha Runolfsson e455c05721 Added exception handling when switchbot account login. (#165978) 2026-03-19 13:15:45 +01:00
Ariel Ebersberger 085df1de19 Fix Home Asssitant Cloud test for Python 3.14.3 (#165937) 2026-03-19 12:11:26 +00:00
J. Diego Rodríguez Royo 91a1237965 Bump aiohomeconnect to 0.33.0 (#166001) 2026-03-19 13:07:57 +01:00
Raj Laud 680a6bc856 Add sensor tests for missing victron_ble device types (#165498)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-19 12:06:14 +01:00
dependabot[bot] 152912c258 Bump actions/download-artifact from 8.0.0 to 8.0.1 (#165982)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 11:20:30 +01:00
wollew 40e8a1b11a Bump pyvlx to 0.2.32 (#165990) 2026-03-19 11:14:57 +01:00
johanzander 69dc354669 growatt_server: add diagnostics support (#165923)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-19 11:09:25 +01:00
Christopher Fenner bbe1bf14ae Bump PyViCare to 2.58.1 (#165965) 2026-03-19 10:19:48 +01:00
Joost Lekkerkerker 5470d8f8a7 Add switch for microfilter bypass mode to SmartThings (#165919) 2026-03-19 09:29:48 +01:00
Joost Lekkerkerker 99fe4b10d0 Add sensors for microfilter to SmartThings (#165922) 2026-03-19 08:57:51 +01:00
Brett Adams 886b6b08ac Source Tessie phantom drain and battery sensors from state data (#165970) 2026-03-19 08:24:32 +01:00
Robert Svensson 6a1e7c1cca Switch over to aiohttp on the Axis integration (#165963) 2026-03-19 08:23:06 +01:00
Josef Zweck d17df13055 Manually update values instead of sending an event in mold_indicator (#165891) 2026-03-19 08:17:07 +01:00
J. Nick Koston f73502c77a Bump ulid-transform to 2.2.0 (#165964) 2026-03-18 23:15:26 +01:00
Dan Raper 2c37a86bc9 Bump ohme to 1.7.1 (#165951) 2026-03-18 21:47:48 +00:00
tronikos fa8e976de7 Add exception translations to Google Weather (#165935)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 13:25:58 -07:00
Andres Ruiz 877bca28ad Stop manually assigning an entity_id in waterfurnace sensors (#165954) 2026-03-18 20:58:36 +01:00
tronikos a57c65f512 Add reconfigure flow in Google Drive (#165926) 2026-03-18 12:46:43 -07:00
tronikos 7140826dbb Do not abbreviate "reauthentication" in Google Drive (#165941) 2026-03-18 20:38:49 +01:00
Bouwe Westerdijk 5fea8d69d7 Add live firmware update detection to Plugwise (#165936)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 20:37:57 +01:00
Paul Tarjan 98e3b9962e Log Withings webhook URL warning only once (#164551)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 20:21:38 +01:00
Kurt Chrisford afe19147f8 Test coverage for the Actron Air integration (#164446) 2026-03-18 20:20:51 +01:00
Willem-Jan van Rootselaar 0e7c25488c Add reconfigure flow to BSB-LAN (#164070) 2026-03-18 20:19:50 +01:00
Jan Čermák 412e85203d Add issue and repair for NTP sync failure (#165463)
Co-authored-by: Stefan Agner <stefan@agner.ch>
2026-03-18 20:16:46 +01:00
Abílio Costa 55ec4a95fd Update renault snapshots (#165948) 2026-03-18 19:59:39 +01:00
Artur Pragacz 6ea9e9a161 Remove targets from intent response (#165434)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2026-03-18 18:35:30 +00:00
tronikos b56e6d1ff7 Update Google Drive quality scale rules to match #156167 (#165916) 2026-03-18 19:34:55 +01:00
Eduardo Tsen b502cdd15b Add buttons for controlling dishwasher operation (#160269)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-18 19:32:58 +01:00
Mike Ryan b7ba85192d Add Trigger Motion Activity button to fully kiosk browser (#164499)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2026-03-18 14:24:51 -04:00
Erik Montnemery 04d45c8ada Add schedule conditions (#165913)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-18 18:55:47 +01:00
tronikos ba0804fefa Add exception translations to Google Drive (#165932) 2026-03-18 18:51:07 +01:00
Erik Montnemery 538b817bf1 Adjust inheritance tree of EntitySelectorConfig (#165915) 2026-03-18 18:32:44 +01:00
Brandon Rothweiler 7efa2d3cac Add Dropbox backup integration (#155644) 2026-03-18 17:58:57 +01:00
Erik Montnemery 3f872fd196 Allow specifying attribute in state selector (#165928) 2026-03-18 17:54:36 +01:00
Erik Montnemery b00f6593f1 Add unit of measurement to entity selector filter (#165914) 2026-03-18 17:01:21 +01:00
Raj Laud a63516ff71 Allow retry on invalid encryption key in victron_ble config flow (#165600)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-18 15:53:03 +01:00
Joost Lekkerkerker 55b082edb6 Add binary sensor for smartthings microfilter blockage (#165917) 2026-03-18 15:44:43 +01:00
Robert Resch b0c3ede4fd Improve type hints for startca (#165720) 2026-03-18 15:43:50 +01:00
johanzander 84bd1cd336 growatt_server: use icon-translations instead of hardcoded _attr_icon (#165920)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 15:41:48 +01:00
Erik Montnemery 25bbfcc595 Add gate conditions (#165898) 2026-03-18 15:27:27 +01:00
johanzander bf05925c8b growatt_server: replace custom precision with suggested_display_precision (#165858)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 15:20:30 +01:00
Jan Čermák 488d9ad75c Use new home-assistant/builder actions for image builds (#164756) 2026-03-18 14:44:53 +01:00
Vincent Le Ligeour 2dfad3d755 Add battery charge limit controls to Renault (#163079)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-03-18 14:29:36 +01:00
Stefan Agner 7e759bf730 Fix Abort exception caught by wrong handler in backup encrypt/decrypt (#165852)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 14:28:56 +01:00
Robert Svensson 9678049e72 Bump axis to v67 (#165840) 2026-03-18 14:25:54 +01:00
David Bonnes 8602ba2679 Extend Evohome tests to cover legacy service calls (#164316) 2026-03-18 13:57:31 +01:00
Paulus Schoutsen 78c3503b7d Remove unnecessary volume_up/volume_down overrides from ws66i media player (#164433)
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-18 13:56:22 +01:00
Paulus Schoutsen fbb3b81991 Remove unnecessary volume_up/volume_down overrides from songpal media player (#164432)
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-18 13:56:04 +01:00
Paulus Schoutsen 26eaf510ee Remove unnecessary volume_up/volume_down overrides from clementine media player (#164427)
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-18 13:55:50 +01:00
Erik Montnemery 5c83d16995 Add select triggers (#165378)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 13:55:02 +01:00
Joost Lekkerkerker 388b258d6c Add Zinvolt problem binary sensors (#164091) 2026-03-18 13:54:42 +01:00
Jeef 2c9a5c10da Add data-description strings to IntelliFire (#165910)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 13:49:04 +01:00
Erik Montnemery 5a68bafd69 Add garage_door conditions (#165897) 2026-03-18 13:29:13 +01:00
Erik Montnemery 33fce89a2b Add window conditions (#165899) 2026-03-18 13:16:11 +01:00
Erwin Douna 1932f61da3 Proxmox fix restart/reboot action (#165901) 2026-03-18 11:55:51 +01:00
Mike Degatano 5a231b27b9 Add repair for deprecated arch addon issue (#165511) 2026-03-18 11:53:09 +01:00
Steve Easley 5617e8c7bc Move jvc_projector sensor entities to select domain (#165194) 2026-03-18 11:34:03 +01:00
Emil Burzo 2b5b0e9d0f Add battery temperature sensor to Fully Kiosk Browser integration (#165714) 2026-03-18 11:22:25 +01:00
Josef Zweck 732f553b48 Safely consume events in hassio test (#165892) 2026-03-18 10:43:20 +01:00
Erik Montnemery 0a53b227ed Add door conditions (#165885) 2026-03-18 10:19:06 +01:00
Erik Montnemery 44b73ab7bd Add occupancy conditions (#165678) 2026-03-18 09:43:17 +01:00
Erik Montnemery 538061d512 Add motion conditions (#165677) 2026-03-18 09:23:32 +01:00
Raj Laud e307ceccb5 Bump victron-ble-ha-parser to 0.6.2 (#165832)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-18 08:47:34 +01:00
Erik Montnemery ea7558c0ad Improve naming in condition and trigger test helpers (#165847)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 08:44:14 +01:00
johanzander c4399b5547 growatt_server: add serial_number to DeviceInfo (devices quality scale rule) (#165857)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 08:42:24 +01:00
Erwin Douna d989a83d7b Remove NotImplementedError in Volvo integration (#165856) 2026-03-18 08:41:35 +01:00
mettolen d04f3530df Remove the icon property from Huum climate entity (#165870) 2026-03-18 08:28:02 +01:00
mettolen 647d957ffe Removed redundant logging from Huum integration (#165868) 2026-03-18 08:27:13 +01:00
johanzander a3f3c87b39 growatt_server: add EntityCategory.DIAGNOSTIC to diagnostic sensors (#165880)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 08:24:49 +01:00
Nathan Spencer 447b17a2a4 Bump pyweatherflowudp to 1.5.2 (#165874) 2026-03-18 08:24:24 +01:00
Joost Lekkerkerker eb2b92687c Add camera fixture to SmartThings (#165809) 2026-03-18 08:23:44 +01:00
Jack Boswell 6424e3658e Remove myself from Starlink codeowners (#165883) 2026-03-18 08:22:18 +01:00
Erik Montnemery d1d8754853 Fix type annotations for set_or_remove_state test helper (#165843) 2026-03-18 08:03:45 +01:00
Erik Montnemery c4ff7fa676 Fix bug in assert_condition_behavior_any test helper (#165838) 2026-03-18 08:03:18 +01:00
balloob-travel f1fe1d3956 Update config flow testing instructions for AI (#165873)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2026-03-18 06:39:15 +01:00
Christopher Fenner fd0d60b787 Fix return type in ViCare integration (#165861) 2026-03-18 03:12:06 +01:00
Stefan Agner 9ddefaaacd Bump aiohasupervisor to 0.4.2 (#165854) 2026-03-17 23:08:57 +01:00
Ludovic BOUÉ 5c8df048b1 Fix timezone in account creation date in test snapshot (#165831) 2026-03-17 22:53:36 +01:00
Raj Laud d86d85ec56 Fix victron_ble charger error sensor always showing unknown (#165713)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-17 21:45:51 +00:00
tronikos 660f12b683 Implement dynamic-devices and stale-devices in Opower to mark it platinum (#165121)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 22:43:57 +01:00
Jan Bouwhuis b8238c86e6 Cleanup unused vacuum test helpers (#165851) 2026-03-17 22:36:24 +01:00
Raman Gupta 754828188e Refactor Vizio integration to use DataUpdateCoordinator (#162188)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 17:20:01 -04:00
Erik Montnemery 6992a3c72b Adjust name and docstring of some trigger tests (#165846) 2026-03-17 22:11:32 +01:00
Joost Lekkerkerker 738d4f662a Bump pySmartThings to 3.7.2 (#165810) 2026-03-17 21:57:20 +01:00
Carlos Sánchez López 7f33ac72ab Add alarm control panel support for Tuya WG2 alarm panel (Duosmart C30) (#165837) 2026-03-17 21:44:57 +01:00
Carlos Sánchez López 0891d814fa Add sensor support for Tuya WG2 alarm panel (Duosmart C30) (#165834) 2026-03-17 21:42:20 +01:00
Carlos Sánchez López ddab50edcc Add binary sensor support for Tuya WG2 alarm panel (Duosmart C30) (#165833) 2026-03-17 21:41:57 +01:00
Erik Montnemery c8ce4eb32d Deduplicate tests testing conditions in mode all (#165841) 2026-03-17 21:06:26 +01:00
Jan Bouwhuis 22aca8b7af Add clean segment support to MQTT vacuum entities (#164983)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-03-17 20:27:42 +01:00
Erik Montnemery 770864082f Deduplicate tests testing conditions in mode any (#165801) 2026-03-17 19:23:47 +00:00
Abílio Costa 14545660e2 Make TODO subscriptions use TodoItem instead of JSON (#165802) 2026-03-17 19:09:13 +00:00
Jamie Magee 836353015b Detect new garage doors automatically in aladdin_connect (#165004) 2026-03-17 20:04:31 +01:00
Allen Porter c57ffd4d78 Update python-roborock dependency to 4.25.0. (#165800)
Co-authored-by: Ludovic BOUÉ <lboue@users.noreply.github.com>
2026-03-17 19:58:18 +01:00
prana-dev-official cbebfdf149 Add number platform for Prana integration (#165816) 2026-03-17 19:53:50 +01:00
Ludovic BOUÉ d8ed9ca66f Fix timestamps in chess_com test diagnostics (#165829) 2026-03-17 19:30:08 +01:00
Cody 5caf8a5b83 Make Season integration timezone aware (#164876) 2026-03-17 18:09:25 +01:00
Aidan Timson c05210683e Demo valve registry entry and device (#165803)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 17:37:21 +01:00
Joost Lekkerkerker aa8dd4bb66 Add microfiber filter fixture to SmartThings (#165808) 2026-03-17 17:21:51 +01:00
Joost Lekkerkerker ee7d6157d9 Fix Indevolt button snapshot (#165812) 2026-03-17 17:19:03 +01:00
Manu adec1d128c Add exception handling to media source in Radio Browser integration (#164653) 2026-03-17 17:13:11 +01:00
prana-dev-official 0a2fc97696 Import improvement for Prana integration (#165805) 2026-03-17 16:28:53 +01:00
Joost Lekkerkerker 447d616097 Add select for SmartThings RVC sound mode (#164519) 2026-03-17 15:57:59 +01:00
Norbert Rittel d3102e718d Consistenly sentence-case "API token" in habitica (#165369)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-17 14:30:39 +00:00
Josef Zweck 69ee49735a Remove support for homeassistant.update_entity from mold_indicator (#165797) 2026-03-17 15:26:22 +01:00
Daniel Hjelseth Høyer 35a99dd4a4 Fix Tibber update token (#164295)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-03-17 15:11:51 +01:00
Ariel Ebersberger 51c3397be8 Refactor wemo integration to use async service action handlers (#165794)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 15:07:00 +01:00
Brett Adams 57f0fd2ed2 Tesla Fleet: fix malformed energy live response handling (#165101) 2026-03-17 15:04:35 +01:00
Erik Montnemery fa7a216afe Use return value from target_entities directly in condition tests (#165791) 2026-03-17 14:55:17 +01:00
Josef Zweck 20f4426e1d Fix mold_indicator sensor update (#158996) 2026-03-17 14:28:50 +01:00
Erik Montnemery ba30563772 Deduplicate tests testing triggers in mode last (#165789) 2026-03-17 14:28:10 +01:00
A. Gideonse b807c104a3 Add button platform to Indevolt integration (#165283) 2026-03-17 13:59:18 +01:00
epenet 9e6abb719a Add fixture for Kerui/Tuya video doorbell (#165786) 2026-03-17 13:57:28 +01:00
jvmahon ed2083a60d Limit color temperature to maximum Matter MIREDs value (#163892)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-17 12:49:18 +00:00
Kornel 94db0d5eab Handle timeout in HKDevice.async_update (#162071)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-17 12:42:43 +00:00
dckiller51 06eed998b9 Add platform attribute to Xbox sensors (#161661) 2026-03-17 12:40:42 +00:00
Andrej Walilko fb5c2f2566 Add shuffle service and enqueue support to jellyfin media player (#161632)
Co-authored-by: Andrej Walilko <awalilko@liquidweb.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-17 12:17:55 +00:00
Dominik 4f7d065230 Fix fritz target selector for dial and set_guest_wifi_password (#165396) 2026-03-17 13:15:51 +01:00
Erwin Douna d034df9b93 Add Portainer request timeout (#165785) 2026-03-17 12:58:55 +01:00
Erik Montnemery 6c9fc7c7a1 Deduplicate tests testing triggers in mode first (#165779)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 12:44:21 +01:00
Leon Grave ba58ef23d8 Add reauthentication-flow to freshr (#165545)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-17 12:22:54 +01:00
johanzander 0a0fa96ac1 Add silver quality scale for growatt_server (#165500) 2026-03-17 12:18:11 +01:00
Erik Montnemery 9cc7ef75b0 Move cover.trigger.CoverDomainSpec to cover.models (#165774) 2026-03-17 12:11:11 +01:00
Carlos Sánchez López 2e0d6d2bbf Add fixture for Tuya wg2 alarm panel (Duosmart C30) (#165701)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-03-17 11:34:53 +01:00
Artur Pragacz bafef2065f Rework user-given entity name logic (#162763) 2026-03-17 11:09:20 +01:00
Erik Montnemery fdfe87de4c Move condition/trigger test helpers to test.components.common (#165777) 2026-03-17 11:08:38 +01:00
epenet 933d123db3 Move xiaomi_miio coordinator to separate module (#165766) 2026-03-17 11:04:31 +01:00
Brett Adams 1f9946a1b8 Fix sensor reset handling in Tesla Fleet (#165744) 2026-03-17 10:59:35 +01:00
Josef Zweck 403e30b56e Add upload progress tracking to hassio (#165664) 2026-03-17 10:47:58 +01:00
Robert Resch e4524d9b68 Run split tests in the same stage with mypy (#165738) 2026-03-17 10:45:55 +01:00
Ariel Ebersberger 738100c897 Fix wemo tests for Python 3.14.3 (#165768) 2026-03-17 10:40:54 +01:00
Erik Montnemery 67356de21b Deduplicate tests testing triggers in mode any (#165772) 2026-03-17 10:27:12 +01:00
Robert Resch 80c5bd1843 Bump pyOpenSSL to 26.0.0 (#165770) 2026-03-17 10:13:37 +01:00
Erik Montnemery 492883de57 Add cover conditions (#165661) 2026-03-17 10:11:16 +01:00
Samuel Xiao 45f1247237 Switchbot Cloud: Add new supported device(Standing Fan) (#165755) 2026-03-17 09:59:18 +01:00
Ville Skyttä 0e76d927cf Switch to actions/attest for build provenance (#165350) 2026-03-17 09:57:48 +01:00
Erik Montnemery 4769a769e0 Use return value from target_entities directly in all trigger tests (#165761) 2026-03-17 09:55:08 +01:00
J. Nick Koston f2d62049ec Fix ESPHome cold/warm white brightness applied twice (#165405) 2026-03-17 09:24:21 +01:00
dependabot[bot] 751b2638ce Bump sigstore/cosign-installer from 4.0.0 to 4.1.0 (#165758)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-17 09:23:42 +01:00
Joakim Plate 120d3ee85a Add support for aqua contour/precise line of gardena products (#165326) 2026-03-17 08:32:17 +01:00
Joost Lekkerkerker 2d273a86ba Add more connection info to SmartThings (#165472) 2026-03-17 08:30:57 +01:00
Erik Montnemery 9bbd9d8bcd Deduplicate trigger tests checking labs flag (#165760)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 08:19:10 +01:00
mettolen 5ff2cac077 Set parallel updates for Huum integration (#165749) 2026-03-17 07:54:37 +01:00
mettolen 74b0d058ec Fix issues in Huum unit test (#165753) 2026-03-17 07:54:01 +01:00
mettolen 29f96e3f9c Move _async_abort_entries_match before the try block in Huum (#165752) 2026-03-17 07:47:47 +01:00
Mike Degatano 39b44445ec Use aiohasupervisor for all calls from hassio/coordinator (#164413)
Co-authored-by: Stefan Agner <stefan@agner.ch>
2026-03-17 01:06:56 +01:00
Robert Resch 589622c05a Fix pterodactyl tests (#165745) 2026-03-16 23:44:26 +01:00
Brett Adams 6abe576ec9 Platinum quality for Teslemetry (#165727) 2026-03-16 22:31:17 +00:00
Robert Resch 75978d8837 Fix demo tests for Python 3.14.3 (#165724) 2026-03-16 22:52:04 +01:00
Robert Resch a2da13a0b3 Fix kitchen_sink tests for Python 3.14.3 (#165730) 2026-03-16 22:45:36 +01:00
Robert Resch ce081d7e71 Fix local_file tests for Python 3.14.3 (#165731) 2026-03-16 22:45:15 +01:00
Robert Resch 037e123e11 Fix media_player tests for Python 3.14.3 (#165732) 2026-03-16 22:44:52 +01:00
Robert Resch 592b7e5594 Fix wake_on_lan tests for Python 3.14.3 (#165733) 2026-03-16 22:44:23 +01:00
Cyril MARIN a963eed3a7 Add bearer token as optional setting to Ollama (#165325)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-16 22:14:33 +01:00
Devin Slick 2042f2e2bd Add Lojack integration (#162047)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-16 22:09:10 +01:00
mettolen 3580fab26e Initialize quality scale for Huum integration (#164902) 2026-03-16 22:08:43 +01:00
Matt Zimmerman 1817522107 Clean up SmartTub integration and tests (#165517)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-16 22:06:23 +01:00
Matt Zimmerman 98a9ce3a64 Add quality scale file for SmartTub integration (#162376)
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 21:48:09 +01:00
johanzander 163bfb0fdd Add SPH inverter support to Growatt Server integration (#165314)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 21:46:48 +01:00
Jeff Terrace 66f04c702c Update onvif parsers library to latest parsing multiple (#165571)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-16 21:40:37 +01:00
Khole 41c497c49e Hive: Fix bug in config flow for authentication and device registration (#165061) 2026-03-16 21:07:34 +01:00
Ludovic BOUÉ c25a664365 Fix Matter firmware update detection when version strings are identical (#165509) 2026-03-16 21:07:03 +01:00
Raj Laud 3dec70abce Add AC charger sensor support to victron_ble (#165497)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 20:59:30 +01:00
Robert Resch 3c2f696a23 Improve type hints for pilight (#165719) 2026-03-16 20:55:04 +01:00
Nathan Spencer 54745dc1f2 Remove stale devices at setup in Whisker (#165721) 2026-03-16 20:54:02 +01:00
Raj Laud e4345c72d9 Fix SmartLithium 8-cell support in victron_ble (#165496)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 20:49:43 +01:00
J. Diego Rodríguez Royo 7acb253ae2 Add bread baking and dough proving programs to Home Connect (#165717) 2026-03-16 20:47:20 +01:00
J. Diego Rodríguez Royo 812c63eeb7 Bump aiohomeconnect to 0.32.0 (#165716) 2026-03-16 20:46:22 +01:00
Erwin Douna 7f13731035 Start orphaned entries in normal mode only (#164815)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-16 20:45:33 +01:00
Christian Lackas 879178e8a2 Add light support for HmIP-MP3P (Combination Signalling Device) (#162825) 2026-03-16 20:43:36 +01:00
Brett Adams 4d8cedb061 Add dynamic device discovery for Teslemetry (#162143)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Erwin Douna <e.douna@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-16 20:31:05 +01:00
Christian Lackas e9f0d8a550 vicare: Remove heating type config, defaulting to auto-detection (#165649) 2026-03-16 20:26:02 +01:00
Joost Lekkerkerker c5a04deb28 Add integration type to Orvibo (#165706) 2026-03-16 20:04:59 +01:00
Bouwe Westerdijk f2a205e8d7 Improve Plugwise DataUpdateCoordinator (#165715)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-16 20:04:55 +01:00
prana-dev-official 254aa30ad8 Add sensor platform to prana (#165632) 2026-03-16 20:03:36 +01:00
J. Diego Rodríguez Royo de4025634a Add start selected program action to Home Connect (#165362)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-16 20:03:20 +01:00
Artur Pragacz db4af890f4 Use standard syrupy serialisation for registries in homekit controller (#165693) 2026-03-16 18:17:22 +00:00
cdheiser 501c8fecec Bump pylutron to 0.4.0 and maintain switch compatibility (#165592) 2026-03-16 19:13:23 +01:00
Andres Ruiz 03edee1335 Enable support for multiple Waterfurnace devices (#162692)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-16 19:04:14 +01:00
Nathan Spencer 00b0da7d26 Add auto device removal handling to Whisker (#165709) 2026-03-16 18:01:37 +00:00
J. Nick Koston bf23fc5887 Fix choppy HomeKit camera audio with SRTP audio proxy (#165185) 2026-03-16 07:36:08 -10:00
Artur Pragacz 6f746c4375 Add common entity_entry_as_dict util to diagnostics (#165692) 2026-03-16 18:16:13 +01:00
Nathan Spencer e7c3a62569 Add dynamic devices support for Whisker (#165704) 2026-03-16 18:11:10 +01:00
Joost Lekkerkerker b1578a0c8c Add hassfest check to make sure new integrations have an integration type (#164001) 2026-03-16 18:10:30 +01:00
Martin Ecker 56b4d2c015 Add correct speed fan mapping for Z-Wave GE/Jasco Enbrighten ZWA4013 (#164500) 2026-03-16 17:53:58 +01:00
Erwin Douna d5ee99c450 Proxmox re-use sanitize UserID (#164303) 2026-03-16 17:50:51 +01:00
hanwg 7d2a305996 Suggest chat_id for subentry flow for Telegram bot (#165515)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-16 17:33:00 +01:00
Josef Zweck 6945418805 Refactor mold_indicator sensor (#165696) 2026-03-16 17:10:05 +01:00
Ariel Ebersberger ccecbcb389 Refactor condition helpers (#165662) 2026-03-16 16:57:53 +01:00
epenet 8bb51c0662 Move meteo_france coordinators to separate module (#164558)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-16 16:27:54 +01:00
Raj Laud f66edf6b86 Bump victron-ble-ha-parser to 0.6.1 (#165473)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 15:27:06 +00:00
Joost Lekkerkerker 70e469366b Finish test coverage in TRMNL (#165611) 2026-03-16 16:18:20 +01:00
epenet 4a9ba865be Fix HVACMode mappings in Tuya climate (#165691) 2026-03-16 16:15:12 +01:00
Denis Shulyaka 0167182e2e Add support for service tier for OpenAI integration (#165379)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-16 15:38:29 +01:00
Ariel Ebersberger 11411a880d Refactor trigger helpers (#165455)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-16 15:26:57 +01:00
J. Diego Rodríguez Royo ce47abe1d3 Add climate entity for air conditioner to Home Connect (#155981)
Co-authored-by: Joostlek <joostlek@outlook.com>
Co-authored-by: emontnemery <erik@montnemery.com>
2026-03-16 15:19:57 +01:00
epenet b58513c19a Use TuyaCoverAction enum in Tuya cover (#165690) 2026-03-16 15:08:49 +01:00
epenet 4e1dab6d8b Migrate remaining vacuum wrappers to Tuya library (#165688) 2026-03-16 15:06:03 +01:00
epenet 5ae8e1c319 Migrate remaining climate wrappers to Tuya library (#165687) 2026-03-16 15:03:15 +01:00
epenet 17bf6ca591 Migrate remaining alarm control panel wrappers to Tuya library (#165686) 2026-03-16 14:59:10 +01:00
epenet 256d30c38d Migrate remaining fan wrappers to Tuya library (#165685) 2026-03-16 14:56:26 +01:00
Jan Čermák 5d182394c2 Update zizmor to v1.23.1 (#165467) 2026-03-16 14:30:13 +01:00
epenet 011e6863d8 Bump tuya-device-handlers to 0.0.13 (#165684) 2026-03-16 14:11:26 +01:00
Anis Kadri b902b590b1 Add UniFi Access binary sensors (#165569)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 14:03:46 +01:00
peteS-UK 960666e15b Improve discovery flow for Squeezebox (#153958) 2026-03-16 13:50:33 +01:00
Mike Degatano 1fb59c9f11 Remove code notary related unsupported reasons (#165417)
Co-authored-by: Stefan Agner <stefan@agner.ch>
2026-03-16 13:45:58 +01:00
Mike Degatano 332bf95e16 Bump aiohasupervisor to 0.4.1 (#165489)
Co-authored-by: Stefan Agner <stefan@agner.ch>
2026-03-16 13:11:48 +01:00
Joost Lekkerkerker e35fc8267e Fix typing in nsw_fuel_station (#165679) 2026-03-16 12:41:53 +01:00
Joost Lekkerkerker f8b4ffc0d7 Fix translation placeholders in Assist pipeline (#165676) 2026-03-16 12:37:47 +01:00
Mike Degatano 003ee5a699 Remove aiohasupervisor from pyproject.toml (#165512) 2026-03-16 11:56:10 +01:00
epenet c91d805174 Use external library wrapper in Tuya vacuum (#165673) 2026-03-16 11:52:34 +01:00
epenet c478d19ae3 Use external library wrapper in Tuya climate (#165672) 2026-03-16 11:46:59 +01:00
Samuel Xiao 09169b0f06 Switchbot Cloud: Fixed Circulator Fan on start error (#165241)
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-03-16 11:45:21 +01:00
epenet aa1dbee315 Use external library wrapper in Tuya cover (#165656) 2026-03-16 11:37:18 +01:00
TimL daf89e5673 Bump Pysmlight to 0.3.0 (#165658) 2026-03-16 11:35:25 +01:00
Joshua Monta 85dc81c147 Update uhoo IQS to silver (#165665) 2026-03-16 11:31:53 +01:00
epenet 5acf24cb53 Use external library wrapper in Tuya alarm control panel (#165671) 2026-03-16 11:30:51 +01:00
Martin Hjelmare 79829a311c Fix emulated_kasa tests for Python 3.14.3 (#165667) 2026-03-16 11:19:06 +01:00
Martin Hjelmare ce2c62ae28 Fix numato tests for Python 3.14.3 (#165669) 2026-03-16 11:17:29 +01:00
Martin Hjelmare 1cda3f47d6 Fix valve tests for Python 3.14.3 (#165668) 2026-03-16 11:16:27 +01:00
Nathan Spencer e254716615 Remove deprecated entity creation code for Litter-Robot 4 devices (#165636) 2026-03-16 10:40:31 +01:00
epenet 1d410f4cbd Use external library wrapper in Tuya humidifer (#165654) 2026-03-16 10:39:54 +01:00
epenet 6616793e2b Use external library wrapper in Tuya light (#165653) 2026-03-16 10:39:43 +01:00
Joost Lekkerkerker 6766961327 Finish TRMNL docs (#165612) 2026-03-16 10:38:11 +01:00
Denis Shulyaka dd6fc11d28 Bump python-telegram-bot to 22.6 (#165508)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-16 10:34:54 +01:00
Simone Chemelli cb5b8b212c Bump aiocomelit to 2.0.1 (#165663) 2026-03-16 10:32:55 +01:00
epenet 66b96d096e Use external library wrapper in Tuya event (#165655) 2026-03-16 10:32:31 +01:00
epenet e86160de36 Use external library wrapper in Tuya fan (#165464) 2026-03-16 10:24:00 +01:00
Simone Chemelli 7617007edd Update IQS to silver for Fritz (#162280) 2026-03-16 10:19:35 +01:00
epenet 3e065b31b3 Simplify Prana entity descriptions (#165660)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-16 10:12:16 +01:00
Simone Chemelli 5f909a6f3a Fix wifi switch status and add 100% coverage for Fritz (#164696) 2026-03-16 10:05:42 +01:00
Jan Bouwhuis 6117a20ec6 Fix MQTT device tracker overrides via JSON state attributes without reset (#165529) 2026-03-16 10:03:35 +01:00
Simone Chemelli 93bc05bb3f Fix switch set for Vodafone Station (#165273) 2026-03-16 10:00:52 +01:00
Thomas Kadauke e7397ccaa7 fix: Increase WebSocket message size limit to 16MB in Hass.io ingress proxy (#164442)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 09:48:06 +01:00
Joshua Monta 91a43873a2 feat: implement reauthentication requirement (#165641) 2026-03-16 09:03:01 +01:00
Ludovic BOUÉ 469e06fb8c Add Matter certified Silabs fan example to fixtures (#165622) 2026-03-16 09:02:23 +01:00
Joost Lekkerkerker bac370e775 Add diagnostics to Zinvolt (#165623) 2026-03-16 08:22:46 +01:00
Mick Vleeshouwer 1a9da26286 Update Overkiz test fixtures and diagnostics to use more realistic fixture (#165615) 2026-03-16 08:21:01 +01:00
Lukas f795707c53 Pooldose bump python-pooldose to 0.8.6 (#165616) 2026-03-16 07:27:57 +01:00
Allen Porter 9ad1356e4b Upgrade ical dependency to 13.2.2. (#165642) 2026-03-16 07:09:25 +01:00
J. Nick Koston 0f70d5fd39 Bump aioesphomeapi to 44.5.2 (#165644) 2026-03-16 07:07:49 +01:00
Josh Gustafson f4c6724953 Add icons for Arcam sensors (#165637) 2026-03-16 06:56:30 +01:00
Sab44 82432d9ee7 Bump librehardwaremonitor-api to version 1.11.1 (#165629) 2026-03-16 00:10:51 +00:00
Joost Lekkerkerker 8db07f3ceb Add API key url to step description in TRMNL (#165614) 2026-03-15 23:41:22 +01:00
Joost Lekkerkerker 2fe9d1ef86 Add reconfigure flow to TRMNL (#165594) 2026-03-15 21:52:40 +01:00
David Bishop cbb1f3726c Move coordinator tests and migrate test data to JSON fixtures (#165503)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 20:53:45 +01:00
Josh Gustafson beb122bb1a Add binary sensor platform to Arcam FMJ (#165272)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 20:08:05 +01:00
J. Nick Koston 8d6099b055 Bump ulid-transform to 2.0.2 (#165585) 2026-03-15 20:07:19 +01:00
J. Nick Koston 7ebe11c0e6 Bump habluetooth to 5.10.2 (#165591) 2026-03-15 20:07:01 +01:00
Andrew Jackson 12b14b46c0 Bump aiomealie to 1.2.2 (#165610) 2026-03-15 19:56:48 +01:00
tronikos cc45201f2d Redact utility account id in Opower diagnostics (#165145) 2026-03-15 11:56:36 -07:00
Erwin Douna a433a163a3 Migrate unique ID of Portainer integration (#165123) 2026-03-15 18:00:41 +01:00
Joost Lekkerkerker 7fd86145d1 Add 2 more sensors to TRMNL (#165604) 2026-03-15 17:13:35 +01:00
Joost Lekkerkerker f244af590e Handle action exceptions in TRMNL (#165607) 2026-03-15 17:00:14 +01:00
Joost Lekkerkerker 9a7dd98d89 Change initiate flow button text for TRMNL (#165606) 2026-03-15 16:46:57 +01:00
Joost Lekkerkerker 6c4beba465 Bump trmnl to 0.1.1 (#165605) 2026-03-15 16:45:43 +01:00
Joost Lekkerkerker 3a46beec76 Add dynamic device handling to TRMNL (#165548) 2026-03-15 16:43:52 +01:00
Josef Zweck d7c2dfc4d4 Add backup progress callback to onedrive integrations (#165217) 2026-03-15 16:31:04 +01:00
Joost Lekkerkerker 4efbafb003 Add TRMNL time platform (#165537)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-15 14:29:20 +01:00
Simone Chemelli 1b10db28f1 Add 100% coverage of coordinator for Fritz (#164074) 2026-03-15 12:16:42 +01:00
Joost Lekkerkerker 1e988fbb04 Remove stateclass from timestamp entity in Intellifire (#165403) 2026-03-15 11:39:57 +01:00
J. Nick Koston 9ab577aad4 Bump fnv-hash-fast to 2.0.0 (#165586) 2026-03-15 09:55:54 +01:00
Olivier R. ed53469eb6 Fix KeyError 'api_domain' in Freebox zeroconf discovery (#165288)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-15 09:07:28 +01:00
Andres Ruiz 56aa96a00c Add re-auth flow for Waterfurnace (#165406) 2026-03-15 07:09:35 +01:00
Anis Kadri 99c6cdbe44 Bump py-unifi-access to 1.1.0 (#165576) 2026-03-15 06:58:27 +01:00
J. Diego Rodríguez Royo 1fd30b73e7 Add fan speed percentage to service schema (#165557) 2026-03-15 06:57:38 +01:00
Joost Lekkerkerker 14aace0c00 Add stale device handling to TRMNL (#165550) 2026-03-15 06:56:05 +01:00
Joost Lekkerkerker 6eed18623b Add reauthentication to TRMNL (#165546) 2026-03-15 06:54:26 +01:00
Joost Lekkerkerker 66ca7d5782 Add switch platform to TRMNL (#165539) 2026-03-15 06:49:09 +01:00
Joost Lekkerkerker a7436cbdc3 Add diagnostics to TRMNL (#165544) 2026-03-15 06:48:13 +01:00
Joost Lekkerkerker 5e57b0272d Add diagnostics to Chess.com (#165563) 2026-03-15 06:47:37 +01:00
Raphael Hehl e16b6ab026 Add emergency switch platform for UniFi Access integration (#165536)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-03-14 20:43:12 +01:00
Joost Lekkerkerker e21fb14b9a Discover Aeotec hub for SmartThings (#165469) 2026-03-14 19:56:53 +01:00
Simone Chemelli 8e099a874b Bump aioamazondevices to 13.0.1 (#165476) 2026-03-14 19:46:02 +01:00
Åke Strandberg a5302a6219 Fix missing code for Miele dishwasher (#165553) 2026-03-14 19:45:47 +01:00
Nathan Spencer f761ac5b49 Add coordinator exception translations and mark entity/exception-translations rules as done (#165551) 2026-03-14 19:27:11 +01:00
Josh Gustafson 6988e73ddc Add sensor platform to Arcam FMJ (#165271)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-14 18:18:17 +01:00
Norbert Rittel a88374557b Make "Power-on behavior" in zha consistent with matter and tuya (#165549) 2026-03-14 18:04:55 +01:00
Nathan Spencer f2456b2c3a Add reconfiguration flow to Whisker (#165513) 2026-03-14 17:30:29 +01:00
Raphael Hehl c1a525b7aa Add unifi_access to Ubiquiti brand and regenerate integrations.json (#165538)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-03-14 17:09:16 +01:00
Joost Lekkerkerker 9d2febd24e Add TRMNL integration (#165499) 2026-03-14 16:17:19 +01:00
Raphael Hehl 54f96bcc33 Add event platform for UniFi Access integration (#165531)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-03-14 14:12:50 +01:00
Manu 5582d83f7b Remove duplicate sensor entity description for monitor port in Uptime Kuma integration (#165479) 2026-03-14 14:05:48 +01:00
Joost Lekkerkerker 2832456bcd Add binary sensor for cooktop in SmartThings (#165481) 2026-03-14 14:05:24 +01:00
Norbert Rittel 070c5821e4 Make start_up_current_level in zha consistent with matter (#165504) 2026-03-14 13:58:01 +01:00
Lukas 07caa8ed2d Bump python-pooldose to 0.8.5 (#165507) 2026-03-14 13:57:20 +01:00
Kevin Stillhammer b02f447e4d Bump pywaze to 1.2.0 (#165526) 2026-03-14 13:56:15 +01:00
Nathan Spencer 4fbb22e861 Update Whisker quality scale docs rules (#165510) 2026-03-14 11:38:29 +01:00
hanwg 45199a341f Pass web session to download files for Telegram bot (#165424) 2026-03-14 09:57:39 +01:00
Jan-Philipp Benecke de5f42d7a0 Add progress reporting to WebDAV upload (#165398) 2026-03-14 08:35:47 +01:00
Artur Pragacz 4459dce73a Reorder code to group intent errors (#165431) 2026-03-13 18:58:19 -05:00
Artur Pragacz a465905467 Remove speech parameter from service intent handler (#165225) 2026-03-13 18:57:16 -05:00
Raphael Hehl a47faa3ced Add UniFi Access integration (#165404)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-03-14 00:00:18 +01:00
Josh 7276403ab9 Allow deleting UniFi client devices (#165505) 2026-03-13 23:06:58 +01:00
Raj Laud 018717af4f Fix victron_ble warning sensor using duplicate alarm translation key (#165502)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-13 22:23:54 +01:00
Norbert Rittel 274c2b8092 Shorten "Power-on behavior" name in matter to be consistent (#165490) 2026-03-13 21:22:49 +01:00
David Bishop bfe15a55c9 Add entity-unavailable and log-when-unavailable (#165486)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 20:20:55 +00:00
dvdinth 54ad67b810 Bump pyintelliclima dependency for IntelliClima integration (#165478)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2026-03-13 20:16:27 +00:00
Nathan Spencer 4d2732df6f Add diagnostics to Whisker (#165487) 2026-03-13 20:38:57 +01:00
Andres Ruiz 2be3291d8e Update brand name for Subaru integration (#165485) 2026-03-13 20:26:44 +01:00
Joost Lekkerkerker 4326cb96ea Add zigbee address to SmartThings devices (#165474)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-13 20:14:58 +01:00
Norbert Rittel 278894d4b4 Make "power-on behavior" states more consistent in tuya (#165344) 2026-03-13 18:53:32 +00:00
Ariel Ebersberger eb17367229 Add DomainSpec to trigger and condition helpers (#165392) 2026-03-13 19:50:19 +01:00
Mike Degatano d96191723f Improve error handling when addon unavailable for install/update (#165352) 2026-03-13 19:28:19 +01:00
mcisk b6c7b2952e Add autoskope integration (#146772)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-13 19:19:00 +01:00
David Bishop 356de12bce Add parallel-updates and action-exceptions for Whisker (#165433)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 18:33:42 +01:00
epenet 57c49d0c48 Fix missing Tuya climate preset_mode (#165460) 2026-03-13 17:49:10 +01:00
Joost Lekkerkerker af22b5fdbb Bump pySmartThings to 3.7.0 (#165468) 2026-03-13 17:12:15 +01:00
Joost Lekkerkerker 9c710961f0 Add Matter fixtures to SmartThings (#165466) 2026-03-13 17:09:38 +01:00
epenet 2a2da83173 Use external library wrapper in Tuya binary_sensor (#165465) 2026-03-13 17:05:52 +01:00
jvmahon 00a52245e3 Add Matter start-up Power-on level entity (#164775) 2026-03-13 17:04:12 +01:00
TheJulianJES adb30e1ec1 Hide ZWA-2 adapter in Zigbee serial port selector (#155526) 2026-03-13 16:56:12 +01:00
TheJulianJES 34a7fcf8d3 Bump ZHA to 1.0.2 (#165423) 2026-03-13 16:15:51 +01:00
prana-dev-official 95a57a2984 Add fan platform for Prana Integration (#163379)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-13 16:05:37 +01:00
epenet 7f39cc0aeb Bump tuya-device-handlers to 0.0.12 (#165462) 2026-03-13 15:58:12 +01:00
Robin Lintermann 6962288e85 Add spring status sensor entity (#164332) 2026-03-13 14:29:37 +01:00
Eli Sand fab4355cc8 Enhance generic_thermostat with min/max run time and cooldown time (#136298) 2026-03-13 14:22:33 +01:00
Robin Lintermann e39d84e8fc Bump pysmarlaapi to 1.0.2 (#165454) 2026-03-13 12:46:09 +01:00
Christian Lackas 35f597223a Add DHW operating mode select entity to ViCare integration (#163832) 2026-03-13 12:44:24 +01:00
Galorhallen 9d61c8336d Update govee local api to 2.4.0 (#165418) 2026-03-13 12:43:41 +01:00
Robert Resch 6fd3603b7b Bump orjson to 3.11.7 (#165443) 2026-03-13 12:34:13 +01:00
epenet 49ac5c42ee Add base entity to arcam_fmj (#165447) 2026-03-13 12:27:52 +01:00
epenet df0db5853c Fix device name in arcam_fmj (#165448) 2026-03-13 12:25:52 +01:00
dependabot[bot] 7afc5b777c Bump docker/metadata-action from 5.10.0 to 6.0.0 (#165438)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 12:25:35 +01:00
dependabot[bot] 595aeea8cc Bump github/codeql-action from 4.32.4 to 4.32.6 (#165436)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 12:22:09 +01:00
dependabot[bot] 02abba02d1 Bump docker/setup-buildx-action from 3.12.0 to 4.0.0 (#165437)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 12:21:54 +01:00
dependabot[bot] 4ca1ad96f1 Bump docker/build-push-action from 6.19.2 to 7.0.0 (#165435)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 12:21:20 +01:00
Erik Montnemery 9f3beba97a Fix vera test opening sockets (#165439) 2026-03-13 11:00:17 +01:00
johanzander 9f86006328 Update Growatt quality scale: add config flow data descriptions (#165426)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 08:46:14 +01:00
Erik Montnemery 4ac651d0b4 Add occupancy triggers (#165374) 2026-03-13 08:41:48 +01:00
J. Nick Koston 9e54abbcb5 Handle OAuth token request exceptions in Yale setup (#165430) 2026-03-13 08:19:24 +01:00
Erik Montnemery d5915c8811 Add motion triggers (#165373) 2026-03-13 07:54:51 +01:00
Erik Montnemery 0c2887df9e Fix numerical entity trigger schema (#165411) 2026-03-13 07:32:43 +01:00
Zach Feldman 3767bac850 August oauth2 exception migration (#165397)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-12 17:28:08 -10:00
J. Nick Koston 9d962d3815 Add missing ON_OFF support and target_temperature_step to ESPHome water heater (#165427)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-12 16:10:29 -10:00
Bram Kragten 786fd40ae8 Update frontend to 20260312.0 (#165420) 2026-03-12 23:07:04 +01:00
Joakim Plate 5ec65dbd58 Remove use of media player internals in arcam (#165359) 2026-03-12 21:55:39 +00:00
Josef Zweck 35878bb203 Bump onedrive-personal-sdk to 0.1.7 (#165401) 2026-03-12 21:59:40 +01:00
Arie Catsman e14d88ff55 Bump pyenphase to 2.4.6 (#165402) 2026-03-12 20:06:49 +00:00
Erwin Douna d04efbfe48 Add platinum badge to Portainer (#165048)
Co-authored-by: Ariel Ebersberger <31776703+justanotherariel@users.noreply.github.com>
2026-03-12 19:30:31 +01:00
AlCalzone 3f35cd5cd2 Remove Z-Wave Installer panel (#165388)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: AlCalzone <17641229+AlCalzone@users.noreply.github.com>
2026-03-12 17:30:28 +01:00
AlCalzone 86ffd58665 Instruct AI to add type annotations to tests (#165386)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-12 17:10:30 +01:00
prana-dev-official 6206392b28 Bump prana-local-api to 0.12.0 (#165394) 2026-03-12 17:05:26 +01:00
dvdinth b7c36c707f Add IntelliClima Sensor platform (#163901)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-12 16:33:34 +01:00
Joakim Sørensen 973c32b99d Add latency results if available to the support package (#165377) 2026-03-12 10:44:08 +01:00
Erik Montnemery 951775bea6 Add window triggers (#165230) 2026-03-12 10:18:42 +01:00
Artur Pragacz 0f2dbdf4f4 Fix logging of unavailable entities in entity call (#165370) 2026-03-12 09:53:30 +01:00
Jan-Philipp Benecke 443ff7efe1 Bump aiowebdav2 to 0.6.2 (#165353) 2026-03-12 08:17:41 +01:00
Jeef 0ee6b954df Bump intellifire4py to 4.4.0 (#165356) 2026-03-12 08:15:48 +01:00
Norbert Rittel 5681acf0e1 Sentence-case "API token" and "username/password" in growatt (#165368) 2026-03-12 07:49:35 +01:00
Andres Ruiz a94458b8bc Bump waterfurnace version v1.6.2 (#165348) 2026-03-12 07:49:12 +01:00
Josef Zweck f3c38ba2d3 Add "cleaning_up" stage to backup (#165349) 2026-03-12 07:28:17 +01:00
Jan Bouwhuis c1acd1d860 Allow an MQTT entity to show as a group (#152270)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-03-11 22:25:28 +01:00
chli1 f4748aa63d fix #163316: FRITZ!SmartHome integration not showing boost status on … (#164574) 2026-03-11 22:19:43 +01:00
Brett Adams 31f4f618cc Fix duplicate energy remaining sensors in Tessie (#165102) 2026-03-11 21:39:35 +01:00
Oluwatobi Mustapha 30aec4d2ab Migrate OAuth helper token request exception handling in Google Sheets (#165000)
Signed-off-by: Oluwatobi Mustapha <oluwatobimustapha539@gmail.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-11 20:33:26 +01:00
AlCalzone 335abd7002 Support new Z-Wave JS "Opening state" notification variable (#165236) 2026-03-11 20:13:54 +01:00
Joakim Sørensen 3b3f0e9240 Bump hass-nabucasa from 1.15.0 to 2.0.0 (#165335) 2026-03-11 20:02:28 +01:00
Ludovic BOUÉ 9f876757f6 Merge branch 'dev' into setpoint_change_source 2026-03-11 19:30:04 +01:00
Simone Chemelli 49586d1519 Fix dnd switch status for Alexa Devices (#164953) 2026-03-11 19:21:51 +01:00
Erwin Douna c63ded3522 Add Swarm stack to Portainer (#164991) 2026-03-11 18:14:05 +01:00
Josef Zweck 2eb65ab314 Buffer backup upload progress events (#165249)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-11 17:29:35 +01:00
ams2990 402a37b435 Change light.toggle service call to invoke LightEntity.async_toggle (#156196)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-03-11 17:17:10 +01:00
Erik Montnemery aa66e8ef0c Improve humidity triggers (#165323) 2026-03-11 17:11:27 +01:00
noambav f1a1e284b7 Add support for Fish Audio s2-pro model (#165269) 2026-03-11 17:07:56 +01:00
hanwg 08594f4e0c Update migration message for Telegram bot (#165299) 2026-03-11 17:04:16 +01:00
Joakim Plate 8d810588f8 Move secondary zone of arcam to sub-device (#165336) 2026-03-11 16:57:47 +01:00
Sid 70faad15d5 Add binary_sensor to eheimdigital (#165035)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-11 16:21:16 +01:00
TheJulianJES d447843687 Bump python-otbr-api to 2.9.0 (#165298) 2026-03-11 16:15:35 +01:00
Steve Easley 83b64e29fa Bump pyjvcprojector to 2.0.3 (#165327) 2026-03-11 16:13:26 +01:00
tronikos 4558a10e05 Improve test coverage in Opower to make it silver (#165124) 2026-03-11 15:56:31 +01:00
johanzander 5ad9e81082 Add reauthentication flow to growatt_server (silver quality scale) (#164993)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 15:51:25 +01:00
cdheiser ba00a14772 Fix flakiness in lutron tests and isolate platforms per test file (#165328) 2026-03-11 15:08:00 +01:00
J. Diego Rodríguez Royo 49f4d07eeb Add fan entity for air conditioner to Home Connect (#155983)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-11 14:29:01 +01:00
Dan Raper 5d271a0d30 Bump ohme to 1.7.0 (#165318) 2026-03-11 12:49:07 +01:00
Joakim Plate 474b683d3c Update gardena to 2.1.0 (#165322) 2026-03-11 12:48:24 +01:00
Erik Montnemery d37106a360 Add gate triggers (#165228) 2026-03-11 10:59:53 +01:00
epenet e115c90719 Reduce internal testing in arcam_fmj tests (#165315) 2026-03-11 10:14:24 +01:00
epenet 6ad3adf0c3 Remove duplicate fixture in arcam_fmj tests (#165312) 2026-03-11 09:51:51 +01:00
dependabot[bot] 2a8d59be4c Bump docker/login-action from 3.7.0 to 4.0.0 (#165302) 2026-03-11 09:16:34 +01:00
dependabot[bot] 6e6e35bc3b Bump actions/dependency-review-action from 4.8.3 to 4.9.0 (#165304) 2026-03-11 09:15:36 +01:00
epenet 795b4c8414 Fix incorrect type annotations in tests (#165305) 2026-03-11 08:38:58 +01:00
Luke Lashley 16389dc18e Bump python-roborock to 4.20.0 (#165292) 2026-03-11 08:21:28 +01:00
Erik Montnemery e7a1c8d001 Remove triggers binary_sensor.occupancy_cleared and occupancy_detected (#165181) 2026-03-11 07:37:40 +01:00
Luke Lashley 4efb10dae1 Remove an extra roborock trait from updating (#165297) 2026-03-11 02:31:10 +01:00
Erik Montnemery f163576e78 Fail more tests when pytest_socket.SocketBlockedError is raised (#155398)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-11 00:00:00 +01:00
Erik Montnemery cad8f97e97 Prevent network access in telegram_bot tests (#165284) 2026-03-10 21:53:35 +01:00
Jeef 4ae6099d84 Add local/cloud option to Intellifire (#162739)
Co-authored-by: Ariel Ebersberger <31776703+justanotherariel@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-10 21:11:57 +01:00
epenet 60dc88fa15 Move NUT coordinator to separate module (#164848)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 09:13:00 -10:00
Josh Gustafson 2d2c6d676d Address Arcam FMJ post-merge feedback (#165277)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 20:09:54 +01:00
Matthias Alphart f3879335ab KNX: add config for unit_of_measurement for yaml sensor entities (#165082) 2026-03-10 19:27:59 +01:00
Matthias Alphart 11bc00038e KNX: add config for device_class and unit_of_measurement for yaml number entities (#165083) 2026-03-10 19:27:48 +01:00
David Bonnes 6845e8b880 Extend RESET_SYSTEM action to all Evohome controller types (#164459) 2026-03-10 19:27:35 +01:00
cdheiser 5741016931 Bump pylutron version to 0.3.0 (#164707)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-10 18:10:28 +01:00
WardZhou 6cbc4e7f62 Add support for Thread Integration to Display Icons for Aeotec SmartThings TBRs (#165275) 2026-03-10 18:07:50 +01:00
Joost Lekkerkerker 4064df0114 Create reset HEPA filter button for main component in SmartThings (#165262) 2026-03-10 18:00:55 +01:00
Troels Schwarz-Linnet 789f850691 Implement 2 new sensors in pyvicare (#164523) 2026-03-10 17:59:36 +01:00
Abílio Costa efca71852b Implement exception-translations for whirlpool integration (#165017) 2026-03-10 17:56:59 +01:00
A. Gideonse 1967e9f309 Add reconfiguration flow to Indevolt integration (#165132) 2026-03-10 17:43:19 +01:00
Artur Pragacz 6ac0c163aa Improve group entities (#160860) 2026-03-10 17:34:52 +01:00
Norbert Rittel bbe20fd698 Improve descriptions of bond actions (#164744) 2026-03-10 17:08:23 +01:00
hanwg f576743340 Fix proxy settings not applied for Telegram bot (#165240) 2026-03-10 16:42:46 +01:00
John O'Nolan 3b4a1fba5f Update Ghost integration quality scale to gold (#165215) 2026-03-10 16:25:15 +01:00
Artur Pragacz 1677a9bfa6 Add clean area intent for vacuum (#165182) 2026-03-10 16:24:18 +01:00
Jordan Harvey 0d9c458705 Anglian Water: Add last meter reading processed sensor (#159144)
Co-authored-by: Ariel Ebersberger <31776703+justanotherariel@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-03-10 16:18:11 +01:00
epenet 57026a862d Ensure actions have name and description translations (#158243)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-10 16:06:51 +01:00
Josh Gustafson fd05be4c52 Refactor Arcam FMJ to use coordinator pattern (#165232)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 15:37:09 +01:00
Dave Love b1f038849e Add Midea Smart Inverter Window AC to Matter Fan Only mode list (#165170) 2026-03-10 15:28:09 +01:00
Ariel Ebersberger b46c9ccc65 Influxdb: Add reconfigure flow (#165186) 2026-03-10 15:06:31 +01:00
epenet 80601426cf Move spotify coordinator to separate module (#164927) 2026-03-10 15:01:04 +01:00
Michael 9519bd2428 Add turned off and turned on triggers to input boolean (#158824)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-03-10 14:26:15 +01:00
Manu be0b7f06a8 Bump pyrate-limiter to 4.0.2, PSNAWP to 3.0.3, python-roborock to 4.17.2 (#164133)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2026-03-10 13:54:37 +01:00
Joost Lekkerkerker d30c6de168 Add another air purifier fixture to SmartThings (#165261) 2026-03-10 12:30:12 +01:00
Sab44 0fa666518e Dynamically add new devices to Libre Hardware Monitor (#165250) 2026-03-10 09:19:50 +01:00
Josef Zweck cf454a1fa3 Bump onedrive-personal-sdk to 0.1.6 (#165219) 2026-03-10 09:13:07 +01:00
Panda-NZ a36733c4dc Add ambient temperature range controls to ToGrill integration (#165235) 2026-03-09 23:40:30 +01:00
Bram Kragten bf846e0756 Validate reorder is only used when multiple is true (#165216) 2026-03-09 22:32:02 +01:00
Erik Montnemery c037dad093 Add humidity triggers (#165197) 2026-03-09 20:34:26 +01:00
Erik Montnemery ce11e66e1f Add cover triggers (#165188)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-09 19:37:36 +01:00
David Bishop f38ca7b04a Add unique_id to Whisker (Litter-Robot) config entries (#164766)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-09 19:35:34 +01:00
Tor André Roland 01200ef0a8 Optimizations to Adax local device control (#162109)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-09 19:29:43 +01:00
mettolen c5e0c78cbc Minor Saunum integration improvements (#164705) 2026-03-09 19:22:27 +01:00
g4bri3lDev 7681caa936 Add diagnostics to OpenDisplay integration (#165222) 2026-03-09 19:05:52 +01:00
Bram Kragten 230a2ff045 Add reorder support to area selector (#165211) 2026-03-09 17:40:34 +01:00
A. Gideonse 9d828502a3 Fix code owner for indevolt integration (#165214) 2026-03-09 17:40:00 +01:00
Samuel Xiao 28088a7e1a Switchbot Cloud: Compatible with new device types (#165191) 2026-03-09 17:12:39 +01:00
epenet 9e8171fb77 Improve test coverage in Tuya light (#164954) 2026-03-09 17:11:26 +01:00
John O'Nolan 1660d3b28a Add stale device removal to Ghost integration (#165134) 2026-03-09 17:10:13 +01:00
Josef Zweck 2ef81a54a5 Allow backups to report the upload progress (#163608)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-09 17:12:49 +02:00
Samuel Xiao ce6154839e Switchbot Cloud: Fixed light mode settings error (#164723) 2026-03-09 15:50:02 +01:00
Erik Montnemery a25300b8e1 Fix import in cover (#165199) 2026-03-09 15:27:12 +01:00
Leon Grave 6fa8e71b21 Add freshr integration, based on pyfreshr (#164538)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-09 15:26:03 +01:00
tronikos c983978a10 Remove type: ignore in Android TV Remote (#165126) 2026-03-09 14:42:51 +01:00
Joost Lekkerkerker 68b8b6b675 Add fixture for Air Purifier to SmartThings (#165187) 2026-03-09 14:21:34 +01:00
Martin Hjelmare ee4d313b10 Fix update tests for Python 3.14.3 (#165196) 2026-03-09 14:21:18 +01:00
Erik Montnemery 5e665093c9 Revert "Add number.changed trigger" (#165193) 2026-03-09 13:55:08 +01:00
A. Gideonse 9a5f509ab9 Fix missing Gen-2 sensor for the Indevolt integration (#165133) 2026-03-09 13:49:54 +01:00
Erik Montnemery 8d0cd5edaa Remove some climate and humidifier triggers (#165192) 2026-03-09 13:37:31 +01:00
epenet 71726272f5 Speed up SmartThings tests (#165184)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-09 13:25:14 +01:00
epenet 9c6c27ab56 Avoid duplicate id/label in smartthings device fixtures (#165190) 2026-03-09 12:40:11 +01:00
Joost Lekkerkerker db20cf8161 Rename SmartThings devices to maintain uniqueness (#165189) 2026-03-09 12:16:07 +01:00
John O'Nolan 59b6270157 Add reconfigure flow to Ghost integration (#165131) 2026-03-09 11:57:40 +01:00
epenet a65ba01bbe Mark climate type hints as mandatory (#164982)
Co-authored-by: Robert Resch <robert@resch.dev>
2026-03-09 11:50:42 +01:00
Erik Montnemery a5d0350560 Add garage_door triggers (#165144)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-09 11:42:09 +01:00
Shai Ungar 368993556f Bump pyseventeentrack to 1.1.2 (#165089)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 10:38:48 +01:00
Daniel Shneyder 23ea17eaef Bump kaiterra-async-client to 1.1.0 (#165166) 2026-03-09 09:59:55 +01:00
g4bri3lDev 6ace93e45b Bump py-opendisplay to 5.5.0 (#165138)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-09 09:29:57 +01:00
epenet 237a0ae03f Improve type hints in ecobee climate (#165178) 2026-03-09 09:16:43 +01:00
epenet 6067be6f49 Improve type hints in lightwave climate (#165179) 2026-03-09 09:16:29 +01:00
J. Nick Koston a35c3d5de5 Bump yalexs-ble to 3.3.0 (#165168) 2026-03-08 16:39:30 -10:00
J. Nick Koston e9c3634cb6 Bump habluetooth to 5.9.1 and bleak-retry-connector to 4.6.0 (#165022) 2026-03-08 16:16:53 -10:00
J. Nick Koston 2ba4544180 Bump yalexs-ble to 3.2.8 (#165018) 2026-03-09 03:07:49 +01:00
Artur Pragacz 5235ce7ae4 Lower ssdp discovery timeout log severity in Onkyo (#165156) 2026-03-09 02:19:42 +01:00
Oscar 56b601e577 Add basic auth support to remote_calendar (#158075) 2026-03-08 16:52:58 -07:00
Justin Boyd f01a0586cb Bump airtouch5py to 0.4.0 (#161640)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-03-08 21:47:06 +01:00
Erwin Douna ca641a097b Fix forced VERIFY_SSL in Portainer (#165079) 2026-03-08 13:19:45 +01:00
Åke Strandberg df2f9d9ef8 Add missing code for Miele dryer (#165122) 2026-03-08 13:18:54 +01:00
Bouwe Westerdijk 501301f4e0 Bump plugwise to v1.11.3 (#165053) 2026-03-08 13:15:44 +01:00
Joakim Plate 89231a1a29 Update pychromecast to 14.0.10 (#165069) 2026-03-08 13:14:34 +01:00
John O'Nolan fe11a6d38f Add diagnostics to Ghost integration (#165130) 2026-03-08 13:03:57 +01:00
Artur Pragacz 3154c3c962 Make restore state resilient to extra_restore_state_data errors (#165086) 2026-03-08 10:39:53 +01:00
mettolen 5031323dea Add description strings to Huum integration (#165094) 2026-03-08 10:24:15 +01:00
Henning Kerstan 017a9e6938 Bump enocean-async to 0.4.2 (#165084) 2026-03-08 09:02:51 +00:00
tronikos 9e974ab30e Add diagnostics in Opower (#165113) 2026-03-08 09:14:15 +01:00
Norbert Rittel 30c0d6792a Make spelling of "auto-empty dock" consistent in roborock (#165117) 2026-03-08 09:12:56 +01:00
Erwin Douna 9ffb9aa824 Bump pyportainer to 1.0.33 (#165080) 2026-03-08 08:33:33 +01:00
A. Gideonse 9ad71711da Add diagnostics to Indevolt integration (#165096) 2026-03-08 08:32:18 +01:00
Steve Easley ef83165159 Bump jvc_projector dependency to 2.0.2 (#165099) 2026-03-08 08:29:53 +01:00
Jordan Harvey f0108c1175 Bump pyanglianwater to 3.1.1 (#165097) 2026-03-08 08:28:06 +01:00
Richard Kroegel 802aa991a9 Remove broken BMW & Mini integrations (#165075) 2026-03-08 00:00:03 +00:00
Sab44 f055c6c7fd Add quality scale exemptions for discovery in Libre Hardware Monitor (#165085) 2026-03-07 23:29:07 +01:00
Joel Hawksley 2a8b045f43 Update weatherkit to fetch hourly data for 7 days (#164494) 2026-03-07 19:08:13 +00:00
Erik Montnemery 281f439bc9 Add trigger door.closed (#165057) 2026-03-07 13:18:46 +00:00
Erik Montnemery 71b420b433 Add trigger door.opened (#164728) 2026-03-07 12:59:09 +01:00
J. Nick Koston 2f02d0f0dc Bump bleak-esphome to 3.7.1 (#165025) 2026-03-07 11:27:59 +00:00
Allen Porter 37cb3cbd50 Bump pyrainbird to 6.1.1 (#165030) 2026-03-07 11:27:28 +00:00
AlCalzone beec21c4a9 Fix cover state updates for legacy Multilevel Switch based Z-Wave covers (#165003) 2026-03-07 12:16:30 +01:00
Pete Sage 642f603ea2 Add binary_sensors for Rehlko load shedding (#164984) 2026-03-07 11:59:44 +01:00
Abílio Costa a3d8d76678 Simplify AGENTS.md (#164894) 2026-03-07 06:27:44 +01:00
J. Nick Koston c25feaa62b Bump aioesphomeapi to 44.3.1 (#165023) 2026-03-06 19:02:18 -10:00
Glenn Waters 50bde6fccd Hunter Douglas Powerview: Fix missing class in hierarchy. (#164264)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-06 21:16:38 +01:00
Karl Beecken 1b7398c271 Bump teltasync to 0.2.0 (#164995) 2026-03-06 21:16:19 +01:00
Sid 7e4b8e802e Add support for the reeflexUV+e to eheimdigital (#163656) 2026-03-06 20:28:39 +01:00
Joost Lekkerkerker 4bcea27151 Bump spotifyaio to 2.0.2 (#164114)
Co-authored-by: Robert Resch <robert@resch.dev>
2026-03-06 20:28:04 +01:00
konsulten ffca43027f Add reconfigure flow for systemnexa2 (#164361) 2026-03-06 20:23:17 +01:00
Joshua Leaper 01e94ca5b2 Update ness_alarm scan interval to 5 secs (#164835) 2026-03-06 20:12:35 +01:00
Petro31 b8ea6b4162 Update template light test framework (#164688) 2026-03-06 20:12:10 +01:00
epenet 1471cb93bc Move smart_meter_texas coordinator to separate module (#164926)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:11:38 +01:00
Erwin Douna 2f7ac2b439 Migrate Smartthings OAuth exceptions (#164939)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-06 20:10:41 +01:00
epenet 0accb403be Move WattTime coordinator to separate module (#164726)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 20:10:14 +01:00
epenet f49a323faf Move wolflink coordinator to separate module (#164929)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-06 20:08:29 +01:00
TimL 21d303dbbc Fix button entity creation for devices with more than two radios (#164699) 2026-03-06 20:07:56 +01:00
Antonio Mello c080a460a2 Fix IntesisHome outdoor_temp not reported when value is 0.0 (#164703)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:07:11 +01:00
epenet 75d675f299 Move AirVisual coordinator to separate module (#164738)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 20:06:18 +01:00
epenet a7e7d01b7a Move launch_library coordinator to separate module (#164747)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 20:05:42 +01:00
epenet 8a0569e279 Move AirVisual Pro coordinator to separate module (#164742)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-06 20:05:30 +01:00
epenet e8279bd20f Move LED BLE coordinator to separate module (#164749)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 20:04:51 +01:00
epenet 852dbf8986 Move peco coordinator to separate module (#164851)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 20:04:34 +01:00
hanwg 6f0eb1d07a Upgrade IQS to gold for Telegram bot (#164911) 2026-03-06 20:04:01 +01:00
epenet 6f68d91593 Move DataUpdateCoordinator to coordinator module in tesla_wall_connector (#164937)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:01:16 +01:00
epenet ffc17b6e91 Move whois coordinator to separate module (#164936)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-06 20:00:18 +01:00
epenet 0d04d79844 Move DataUpdateCoordinator to separate module in reolink (#164914)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 19:59:56 +01:00
epenet f57884cb95 Move kraken API wrapper class to coordinator module (#164942)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 19:54:20 +01:00
Manu 3a83fe5c72 Change setpoint step size in IronOS integration (#164979) 2026-03-06 19:38:26 +01:00
Willem-Jan van Rootselaar 973feb71c1 Bump python-bsblan to 5.1.2 (#164963) 2026-03-06 19:37:55 +01:00
epenet ecee23fc7a Move pi_hole coordinator to separate module (#164869)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-06 19:36:52 +01:00
epenet 442d2282dc Improve type hints in maxcube climate (#164978) 2026-03-06 18:10:51 +01:00
Robert Resch 8853d3e17d Add lawn mower started_returning trigger (#164834) 2026-03-06 18:08:28 +01:00
epenet 6d1e387911 Improve type hints in airtouch4 climate (#164977) 2026-03-06 18:05:27 +01:00
epenet 13fe135e7f Improve type hints in nexia climate (#164976) 2026-03-06 18:04:56 +01:00
epenet 618687ea05 Improve type hints in nuheat climate (#164975) 2026-03-06 18:04:24 +01:00
epenet 8b545a6e76 Improve type hints in oem climate (#164974) 2026-03-06 18:04:07 +01:00
epenet 42fa13200d Improve type hints in proliphix climate (#164972) 2026-03-06 18:03:39 +01:00
epenet d56e944a86 Improve type hints in schluter climate (#164970) 2026-03-06 18:03:17 +01:00
epenet fb357390ce Remove disabled Tfiac integration (#164966) 2026-03-06 18:00:42 +01:00
Shay Levy 702450e209 Bump aioswitcher to 6.1.1 (#164981) 2026-03-06 17:54:38 +01:00
g4bri3lDev bbe45e0759 Add OpenDisplay integration (#164048)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2026-03-06 16:23:09 +01:00
epenet 92902c7aa1 Improve type hints in smarttub climate (#164968) 2026-03-06 16:07:41 +01:00
epenet 5d92dd7760 Use shorthand attributes in zhong_hong climate (#164964) 2026-03-06 16:00:14 +01:00
Joost Lekkerkerker 0ab62dabde Create Chess.com integration (#164960) 2026-03-06 15:55:59 +01:00
Sean O'Keeffe fc68828c78 more programs for Miele steam ovens (#164768)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-06 15:15:43 +01:00
Sab44 7644036592 Add diagnostics to Libre Hardware Monitor (#164958)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-06 15:15:18 +01:00
epenet f19068f7de Mark device_info type hint as mandatory (#164951) 2026-03-06 15:15:05 +01:00
Robin Lintermann 13d2211755 Add sensor entity for total swing time (#164334) 2026-03-06 15:10:06 +01:00
epenet 87e63591d1 Use shorthand attributes in heatmiser climate (#164957) 2026-03-06 15:00:51 +01:00
epenet fc02bbcdd0 Improve type hints in coolmaster climate (#164956) 2026-03-06 15:00:13 +01:00
Simone Chemelli 388d619604 Bump aiovodafone to 3.1.3 (#164955) 2026-03-06 14:59:51 +01:00
Daniel Hjelseth Høyer 3777acff95 Fix energy unit in Homevolt (#164959)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-03-06 14:58:44 +01:00
Jamie Magee e0fd6784cf Test aladdin_connect stale device cleanup (#164119) 2026-03-06 13:03:09 +01:00
epenet 305463d882 Move DataUpdateCoordinator to coordinator module in nsw_fuel_station (#164940)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:25:07 +01:00
Erwin Douna de16edc55b Replace assert in Proxmox coordinator (#164892) 2026-03-06 11:16:14 +01:00
Erwin Douna bd6438937b Adjust read-only parallel updates for Portainer (#164890) 2026-03-06 11:14:58 +01:00
Erwin Douna 45e453791e Update Proxmox code owners (#164941) 2026-03-06 11:11:06 +01:00
epenet 152137a3a2 Move DataUpdateCoordinator to separate module in simplisafe (#164917)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:10:31 +01:00
epenet e059c51b1d Move wiz coordinator to separate module (#164931)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:21:07 +01:00
epenet 9ef66a3a90 Move supla coordinator to separate module (#164928)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:20:42 +01:00
Petro31 494f8c32d5 Fix 'this' variable in template options flow (#164866) 2026-03-06 09:39:42 +01:00
dependabot[bot] 51f90a328b Bump actions/attest-build-provenance from 3.2.0 to 4.1.0 (#164909)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-06 09:38:33 +01:00
epenet b7bdb7b32a Move DataUpdateCoordinator to separate module in subaru (#164918)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:09:03 +01:00
epenet 76c8bae098 Use typed coordinator in powerwall (#164887)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 08:35:09 +01:00
Erwin Douna 59a75e74fe Bump proxmoxer 2.3.0 (#164884) 2026-03-06 08:34:45 +01:00
Christopher Fenner a4af1ce5f8 Translate device name in Season integration (#164882) 2026-03-06 08:33:20 +01:00
Erwin Douna 30ea0b4923 Proxmoxve add parallel updates (#164889) 2026-03-06 08:32:36 +01:00
Erwin Douna fb889dd524 Optimize init proxmox (#164891) 2026-03-06 08:32:18 +01:00
epenet 31055c5cde Move DataUpdateCoordinator to separate module in recollect_waste (#164913)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:31:15 +01:00
epenet a264e5949f Move DataUpdateCoordinator to separate module in senz (#164916)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:30:29 +01:00
Colin 84260ac3f7 Use shared aiohttp session in openevse (#164552) 2026-03-06 07:49:53 +01:00
epenet f50a35877d Move RDW DataUpdateCoordinator to separate module (#164910)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 07:47:08 +01:00
Luke Lashley 6bc94a318a Pass in Base Url during Roborock reauth (#164903) 2026-03-05 20:24:59 -08:00
Blake Messer b0904917ca Fix Rain Bird controllers updated by Rain Bird 2.x (#163915) 2026-03-05 19:37:15 -08:00
Michael 536cfc4c67 Add number.changed trigger (#163984) 2026-03-05 21:36:39 +01:00
Erwin Douna 27b647fa36 Add backoff/max retries in Portainer API (#164805)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-03-05 21:26:22 +01:00
Michael 16fb2dfa91 Add domain driven triggers to schedule helper (#159325) 2026-03-05 21:26:05 +01:00
Josef Zweck 664b75e060 Bump onedrive-personal-sdk to 0.1.5 (#164880) 2026-03-05 20:19:19 +00:00
Erik Montnemery 1cd302eb17 Fix flaky bang_olufsen tests (#164868) 2026-03-05 21:18:10 +01:00
Dan Carroll 8da86796d2 Bump pyeconet to 0.2.2 (#164859) 2026-03-05 20:17:57 +00:00
Denis Shulyaka 33c0edc994 Add GPT-5.4 support to OpenAI conversation (#164883) 2026-03-05 20:16:53 +00:00
epenet 3e8833da54 Refactor Tuya wrappers to use generics (#164587) 2026-03-05 19:22:48 +01:00
Michael Hansen 3858d557b3 Add missing parameters from handle REST API (#164687)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
2026-03-05 11:48:57 -06:00
Renat Sibgatulin 0923bed4b6 Add zeroconf support for air-Q (#164727)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-05 17:55:34 +01:00
Marc Mueller 9b8432eac3 Fix volvo test RuntimeWarning (#164845) 2026-03-05 17:51:12 +01:00
Tucker Kern 5232c05702 Ensure Snapcast client has a valid current group before accessing group attributes. (#164683) 2026-03-05 17:50:31 +01:00
Erik Montnemery e5f77801a7 Unconditionally set up base platform integrations (#164863) 2026-03-05 17:30:34 +01:00
Erik Montnemery bc138b3485 Fix incomplete device info in laundrify sensor (#164824)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-05 17:08:31 +01:00
Andrew Jackson ae90c5fa92 Update Mastodon quality scale to gold (#164842) 2026-03-05 16:50:45 +01:00
Matthias Alphart 2fce45abe1 Fix KNX sensor default attributes for energy and volume DPTs (#164838)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-05 16:48:47 +01:00
karwosts e4417f7b00 Add unique_id to demo water_heater (#164857) 2026-03-05 16:40:17 +01:00
Ariel Ebersberger b57c7f8a95 Fix ffmpeg fixture (#164860) 2026-03-05 16:37:43 +01:00
Henning Kerstan 0618460d73 Replace enocean library (#164272)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-05 16:30:06 +01:00
epenet 92dd045772 Move Mullvad VPN coordinator to separate module (#164750)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-05 16:20:58 +01:00
Michael Hansen fc723e1a42 Add missing features to Wyoming conversation agent (#164278) 2026-03-05 15:56:21 +01:00
Joshua Monta 5907356309 Add new influenza index sensor to Uhoo (#164710) 2026-03-05 15:37:22 +01:00
J. Diego Rodríguez Royo 1c221b4714 Bump aiohomeconnect to 0.30.0 (#164846)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-05 15:34:12 +01:00
Retha Runolfsson 05d57167d2 Add support for switchbot keypad vision (#160484)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-05 14:54:07 +01:00
epenet 69a98dd53e Move nuheat coordinator to separate module (#164833)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 14:16:55 +01:00
John O'Nolan 3c7dd93c7f Add reauthentication flow to Ghost integration (Silver) (#164847) 2026-03-05 14:16:03 +01:00
reneboer 1327712be4 Add sensor charging settings mode (#164455)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-03-05 13:24:23 +01:00
epenet 933e57ba6a Simplify Netgear entity initialisation (#164837) 2026-03-05 13:17:19 +01:00
John O'Nolan 77d54aadc6 Fix Ghost config flow using wrong field name for site UUID (#164836) 2026-03-05 12:46:59 +01:00
Andreas Jakl 5fe2ab93ff Add device tracker to NRGkick integration (#164804)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-05 12:00:30 +01:00
Glenn de Haan 0e4698eb99 Add device class to active_liter_lpm sensor (#164809) 2026-03-05 11:50:37 +01:00
epenet 698c5eca00 Migrate remaining netgear coordinators to separate module (#164826) 2026-03-05 11:49:28 +01:00
Raphael Hehl c7776057b7 Enforce SSRF redirect protection only for connector allowed_protocol_schema_set (#164769)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-05 11:45:05 +01:00
Erik Montnemery e87c677cc4 Improve homee tests (#164820) 2026-03-05 11:15:50 +01:00
Erik Montnemery c3858a0841 Improve tuya diagnostic tests (#164819) 2026-03-05 11:13:01 +01:00
Michael 42bc5c3a5f Add remote.turned_on and remote.turned_off triggers (#164535)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-05 10:52:29 +01:00
epenet 76bc58da2c Add base NetgearDataCoordinator to netgear (#164816) 2026-03-05 10:52:12 +01:00
epenet fc8719ce35 Remove caio from licenses exception list (#164806) 2026-03-05 10:18:08 +01:00
dependabot[bot] 60a4a97d9c Bump dawidd6/action-download-artifact from 14 to 16 (#164790)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-05 10:16:23 +01:00
Erwin Douna 284721e1df Bump pyportainer 1.0.32 (#164803) 2026-03-05 09:06:46 +01:00
Norbert Rittel bfa707d79e Use common string for "host" in devialet config flow (#164798) 2026-03-05 08:32:46 +01:00
Norbert Rittel 633e2e7469 Use common state for "medium" in smartthings (#164799) 2026-03-05 08:32:35 +01:00
dependabot[bot] ad1c6846e7 Bump actions/upload-artifact from 6.0.0 to 7.0.0 (#164791) 2026-03-05 07:29:59 +01:00
Erwin Douna f75140b626 Add const to Portainer for endpoint up (#164746) 2026-03-05 00:38:59 +01:00
rappenze f83757da7c Use unique fibaro_id in test fixtures (#164763) 2026-03-04 22:04:38 +00:00
Norbert Rittel ca338c98f3 Clarify description of vacuum.clean_area action (#164764) 2026-03-04 21:57:59 +00:00
Ian Foster 18a8afb017 Update keyboard_remote dependencies (#164755) 2026-03-04 19:47:17 +01:00
Italo Lombardi 0136e9c7eb ISS integration: better entity handling (#159050)
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-03-04 17:46:48 +01:00
Erik Montnemery d88c736016 Add is_closed state attribute to cover (#164739) 2026-03-04 16:54:06 +01:00
Robert Resch 780dc178a1 Use Python version file in CI for setting the default python version (#164751) 2026-03-04 16:53:31 +01:00
Petro31 b7ba945dfc Fix this variable preview issue with template entities from the UI (#164740) 2026-03-04 16:01:41 +01:00
Magnus Øverli 01de7052af Add deprecation timeline to flexit_bacnet fireplace switch (#164450) 2026-03-04 15:47:40 +01:00
Allen Porter 3fe6a31ee9 Improve Roborock device info creation and enhance device registration for disabled or failed devices. (#164553) 2026-03-04 15:45:51 +01:00
rappenze 95570643ec Fix handling of several thermostat QuickApp's in fibaro (#164344)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-04 15:40:49 +01:00
starkillerOG e3210b0ab9 Fix Reolink entity unique_id migration when unique_id already exists (#164667) 2026-03-04 15:12:26 +01:00
Artur Pragacz 2edabf903a Add backup integration to recovery mode (#164734) 2026-03-04 14:33:28 +01:00
Stefan Agner 0e4e703b64 Ignore transient empty segments in Matter vacuum (#164737)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-04 14:24:28 +01:00
tobiaswaldvogel 88624f5179 Use jog up/down in motionblinds if no tilt position is available (#164694)
Signed-off-by: Tobias Waldvogel <tobias.waldvogel@gmail.com>
Co-authored-by: starkillerOG <starkiller.og@gmail.com>
2026-03-04 13:27:47 +01:00
Erwin Douna 4a5fdfc0ec Bump pyportainer 1.0.31 (#164733) 2026-03-04 13:26:10 +01:00
Bram Kragten c6e91afae4 Update frontend to 20260304.0 (#164736) 2026-03-04 13:25:57 +01:00
Kamil Breguła db5e7e4521 Refactor AWS S3 tests (#164098)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 13:13:43 +01:00
Joakim Plate 25489c224b Restore handling of is active input for chromecast (#164735) 2026-03-04 13:10:10 +01:00
Tom c4f64598a0 Add informative errors to Proxmox VE buttons (#164417) 2026-03-04 12:48:17 +01:00
starkillerOG 59e579cf5a Bump reolink-aio to 0.19.1 (#164732) 2026-03-04 12:46:38 +01:00
epenet 831c28cf2c Migrate netgear to use runtime_data (#164718) 2026-03-04 11:37:05 +01:00
Erik Montnemery be1affc6ba Pin exact Python version in .python-version (#164722) 2026-03-04 11:21:44 +01:00
J. Diego Rodríguez Royo 94a25b5688 Improve mobile_app notify.notify with not connected targets (#161855) 2026-03-04 11:11:02 +01:00
Ludovic BOUÉ 28f70fab8d Add setpoint_change_source icon with states for external, manual, and schedule 2026-01-13 20:23:07 +00:00
Ludovic BOUÉ 289490faa3 Refactor setpoint_change_timestamp device_to_ha conversion to use matter_epoch_seconds_to_utc 2026-01-12 20:57:39 +00:00
Ludovic BOUÉ dea46f7b2e Add tests for Eve Thermo v5 SetpointChangeSource, timestamp, and amount sensors 2026-01-12 20:54:52 +00:00
Ludovic BOUÉ 82e3221126 Update Matter Eve Thermo sensor entries to reflect last change and change amount attributes 2026-01-12 19:28:15 +00:00
Ludovic BOUÉ 47e8fbc1ed Add Matter Eve Thermo 20ECD1701 sensor entries and update mock thermostat configurations 2026-01-12 19:25:10 +00:00
Ludovic BOUÉ 0428d0b97f Merge branch 'dev' into setpoint_change_source 2026-01-12 20:19:09 +01:00
Ludovic BOUÉ 45344c04c1 Refactor setpoint change source mapping and add utility functions for Matter epoch conversion 2026-01-12 19:14:54 +00:00
Ludovic BOUÉ c472b6ac5e Add support for RoomAirConditioner device type 2025-12-01 15:12:33 +00:00
Ludovic BOUÉ 58f533feb6 Add device_type attribute for Thermostat sensors 2025-11-30 21:43:43 +01:00
Ludovic BOUÉ 0af8c8fd8c Apply suggestion from @Copilot
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-30 21:37:01 +01:00
Ludovic BOUÉ b9d6c3b9fe Update homeassistant/components/matter/strings.json
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-30 21:36:08 +01:00
Ludovic BOUÉ b700940bb9 Merge branch 'dev' into setpoint_change_source 2025-11-30 21:31:19 +01:00
Ludovic BOUÉ 3b73f6d37e Update thermostat setpoint change timestamp to January 1, 2025 2025-11-30 20:21:45 +00:00
Ludovic BOUÉ 2812bb21da Add offset for Matter 2000 epoch in timestamp conversion 2025-11-30 20:20:13 +00:00
Ludovic BOUÉ 5d474675e8 Update mock thermostat state timestamp to January 1, 2025 2025-11-30 20:15:31 +00:00
Ludovic BOUÉ ea7bcf6cda Update mock thermostat JSON to correct timestamp for attribute 1/513/50 2025-11-30 20:11:19 +00:00
Ludovic BOUÉ 725bd3d671 Add mock thermostat entity and state snapshots for temperature display mode 2025-11-21 12:38:04 +00:00
Ludovic BOUÉ cfc4fa6342 Merge branch 'dev' into setpoint_change_source 2025-11-21 13:35:31 +01:00
Ludovic BOUÉ b650e71660 Update mock thermostat snapshots with new attributes and state values 2025-11-18 18:05:29 +00:00
Ludovic BOUÉ 9ddf15e348 Update mock thermostat JSON with additional attributes and values 2025-11-18 18:03:46 +00:00
Ludovic BOUÉ 15082f9111 Merge branch 'dev' into setpoint_change_source 2025-11-18 16:45:05 +01:00
Ludovic BOUÉ 12f16611ff Rename mock thermostat entity IDs and friendly names in snapshots for consistency 2025-11-18 15:30:39 +00:00
Ludovic BOUÉ 8041be3d08 Merge branch 'dev' into setpoint_change_source 2025-11-18 14:08:38 +01:00
Ludovic BOUÉ 40b021e755 Add tests for Thermostat SetpointChangeSource, Timestamp, and Amount sensors 2025-11-18 13:02:44 +00:00
Ludovic BOUÉ aab57eda96 Update mock thermostat product name to "Mock Thermostat" 2025-11-18 13:00:26 +00:00
Ludovic BOUÉ f0dd37caa5 Add mock thermostat sensors and states for testing 2025-11-18 12:49:32 +00:00
Ludovic BOUÉ 662b178495 Remove unused attribute from thermostat fixture 2025-11-18 12:48:56 +00:00
Ludovic BOUÉ cb3d30884a Add mock thermostat fixture for integration tests 2025-11-18 12:48:21 +00:00
Ludovic BOUÉ 49e6f20372 Add Setpoint Change Source timestamp and amount sensors with localization strings 2025-11-18 12:39:28 +00:00
Ludovic BOUÉ 75d02661eb Add Setpoint Change Source sensor and localization strings 2025-11-14 17:19:28 +00:00
2697 changed files with 144251 additions and 72219 deletions
+2
View File
@@ -620,12 +620,14 @@ rules:
### 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):
+1
View File
@@ -16,6 +16,7 @@ Dockerfile.dev linguist-language=Dockerfile
CODEOWNERS linguist-generated=true
Dockerfile linguist-generated=true
homeassistant/generated/*.py linguist-generated=true
machine/* linguist-generated=true
mypy.ini linguist-generated=true
requirements.txt linguist-generated=true
requirements_all.txt linguist-generated=true
+8 -311
View File
@@ -7,328 +7,25 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
## Code Review Guidelines
**When reviewing code, do NOT comment on:**
- **Missing imports** - We use static analysis tooling to catch that
- **Code formatting** - We have ruff as a formatting tool that will catch those if needed (unless specifically instructed otherwise in these instructions)
**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
## Python Requirements
- **Compatibility**: Python 3.13+
- **Language Features**: Use the newest features when possible:
- Pattern matching
- Type hints
- f-strings (preferred over `%` or `.format()`)
- Dataclasses
- Walrus operator
### Strict Typing (Platinum)
- **Comprehensive Type Hints**: Add type hints to all functions, methods, and variables
- **Custom Config Entry Types**: When using runtime_data:
```python
type MyIntegrationConfigEntry = ConfigEntry[MyClient]
```
- **Library Requirements**: Include `py.typed` file for PEP-561 compliance
## Code Quality Standards
- **Formatting**: Ruff
- **Linting**: PyLint and Ruff
- **Type Checking**: MyPy
- **Lint/Type/Format Fixes**: Always prefer addressing the underlying issue (e.g., import the typed source, update shared stubs, align with Ruff expectations, or correct formatting at the source) before disabling a rule, adding `# type: ignore`, or skipping a formatter. Treat suppressions and `noqa` comments as a last resort once no compliant fix exists
- **Testing**: pytest with plain functions and fixtures
- **Language**: American English for all code, comments, and documentation (use sentence case, including titles)
### Writing Style Guidelines
- **Tone**: Friendly and informative
- **Perspective**: Use second-person ("you" and "your") for user-facing messages
- **Inclusivity**: Use objective, non-discriminatory language
- **Clarity**: Write for non-native English speakers
- **Formatting in Messages**:
- Use backticks for: file paths, filenames, variable names, field entries
- Use sentence case for titles and messages (capitalize only the first word and proper nouns)
- Avoid abbreviations when possible
### Documentation Standards
- **File Headers**: Short and concise
```python
"""Integration for Peblar EV chargers."""
```
- **Method/Function Docstrings**: Required for all
```python
async def async_setup_entry(hass: HomeAssistant, entry: PeblarConfigEntry) -> bool:
"""Set up Peblar from a config entry."""
```
- **Comment Style**:
- Use clear, descriptive comments
- Explain the "why" not just the "what"
- Keep code block lines under 80 characters when possible
- Use progressive disclosure (simple explanation first, complex details later)
## Async Programming
- All external I/O operations must be async
- **Best Practices**:
- Avoid sleeping in loops
- Avoid awaiting in loops - use `gather` instead
- No blocking calls
- Group executor jobs when possible - switching between event loop and executor is expensive
### Blocking Operations
- **Use Executor**: For blocking I/O operations
```python
result = await hass.async_add_executor_job(blocking_function, args)
```
- **Never Block Event Loop**: Avoid file operations, `time.sleep()`, blocking HTTP calls
- **Replace with Async**: Use `asyncio.sleep()` instead of `time.sleep()`
### Thread Safety
- **@callback Decorator**: For event loop safe functions
```python
@callback
def async_update_callback(self, event):
"""Safe to run in event loop."""
self.async_write_ha_state()
```
- **Sync APIs from Threads**: Use sync versions when calling from non-event loop threads
- **Registry Changes**: Must be done in event loop thread
### Error Handling
- **Exception Types**: Choose most specific exception available
- `ServiceValidationError`: User input errors (preferred over `ValueError`)
- `HomeAssistantError`: Device communication failures
- `ConfigEntryNotReady`: Temporary setup issues (device offline)
- `ConfigEntryAuthFailed`: Authentication problems
- `ConfigEntryError`: Permanent setup issues
- **Try/Catch Best Practices**:
- Only wrap code that can throw exceptions
- Keep try blocks minimal - process data after the try/catch
- **Avoid bare exceptions** except in specific cases:
- ❌ Generally not allowed: `except:` or `except Exception:`
- ✅ Allowed in config flows to ensure robustness
- ✅ Allowed in functions/methods that run in background tasks
- Bad pattern:
```python
try:
data = await device.get_data() # Can throw
# ❌ Don't process data inside try block
processed = data.get("value", 0) * 100
self._attr_native_value = processed
except DeviceError:
_LOGGER.error("Failed to get data")
```
- Good pattern:
```python
try:
data = await device.get_data() # Can throw
except DeviceError:
_LOGGER.error("Failed to get data")
return
# ✅ Process data outside try block
processed = data.get("value", 0) * 100
self._attr_native_value = processed
```
- **Bare Exception Usage**:
```python
# ❌ Not allowed in regular code
try:
data = await device.get_data()
except Exception: # Too broad
_LOGGER.error("Failed")
# ✅ Allowed in config flow for robustness
async def async_step_user(self, user_input=None):
try:
await self._test_connection(user_input)
except Exception: # Allowed here
errors["base"] = "unknown"
# ✅ Allowed in background tasks
async def _background_refresh():
try:
await coordinator.async_refresh()
except Exception: # Allowed in task
_LOGGER.exception("Unexpected error in background task")
```
- **Setup Failure Patterns**:
```python
try:
await device.async_setup()
except (asyncio.TimeoutError, TimeoutException) as ex:
raise ConfigEntryNotReady(f"Timeout connecting to {device.host}") from ex
except AuthFailed as ex:
raise ConfigEntryAuthFailed(f"Credentials expired for {device.name}") from ex
```
### Logging
- **Format Guidelines**:
- No periods at end of messages
- No integration names/domains (added automatically)
- No sensitive data (keys, tokens, passwords)
- Use debug level for non-user-facing messages
- **Use Lazy Logging**:
```python
_LOGGER.debug("This is a log message with %s", variable)
```
### Unavailability Logging
- **Log Once**: When device/service becomes unavailable (info level)
- **Log Recovery**: When device/service comes back online
- **Implementation Pattern**:
```python
_unavailable_logged: bool = False
if not self._unavailable_logged:
_LOGGER.info("The sensor is unavailable: %s", ex)
self._unavailable_logged = True
# On recovery:
if self._unavailable_logged:
_LOGGER.info("The sensor is back online")
self._unavailable_logged = False
```
## Development Commands
### Environment
- **Local development (non-container)**: Activate the project venv before running commands: `source .venv/bin/activate`
- **Dev container**: No activation needed, the environment is pre-configured
.vscode/tasks.json contains useful commands used for development.
### Code Quality & Linting
- **Run all linters on all files**: `prek run --all-files`
- **Run linters on staged files only**: `prek run`
- **PyLint on everything** (slow): `pylint homeassistant`
- **PyLint on specific folder**: `pylint homeassistant/components/my_integration`
- **MyPy type checking (whole project)**: `mypy homeassistant/`
- **MyPy on specific integration**: `mypy homeassistant/components/my_integration`
## Python Syntax Notes
### Testing
- **Quick test of changed files**: `pytest --timeout=10 --picked`
- **Update test snapshots**: Add `--snapshot-update` to pytest command
- ⚠️ Omit test results after using `--snapshot-update`
- Always run tests again without the flag to verify snapshots
- **Full test suite** (AVOID - very slow): `pytest ./tests`
- Python 3.14 explicitly allows `except TypeA, TypeB:` without parentheses.
### Dependencies & Requirements
- **Update generated files after dependency changes**: `python -m script.gen_requirements_all`
- **Install all Python requirements**:
```bash
uv pip install -r requirements_all.txt -r requirements.txt -r requirements_test.txt
```
- **Install test requirements only**:
```bash
uv pip install -r requirements_test_all.txt -r requirements.txt
```
## Testing
### Translations
- **Update translations after strings.json changes**:
```bash
python -m script.translations develop --all
```
When writing or modifying tests, ensure all test function parameters have type annotations.
Prefer concrete types (for example, `HomeAssistant`, `MockConfigEntry`, etc.) over `Any`.
### Project Validation
- **Run hassfest** (checks project structure and updates generated files):
```bash
python -m script.hassfest
```
## Good practices
## Common Anti-Patterns & Best Practices
### ❌ **Avoid These Patterns**
```python
# Blocking operations in event loop
data = requests.get(url) # ❌ Blocks event loop
time.sleep(5) # ❌ Blocks event loop
# Reusing BleakClient instances
self.client = BleakClient(address)
await self.client.connect()
# Later...
await self.client.connect() # ❌ Don't reuse
# Hardcoded strings in code
self._attr_name = "Temperature Sensor" # ❌ Not translatable
# Missing error handling
data = await self.api.get_data() # ❌ No exception handling
# Storing sensitive data in diagnostics
return {"api_key": entry.data[CONF_API_KEY]} # ❌ Exposes secrets
# Accessing hass.data directly in tests
coordinator = hass.data[DOMAIN][entry.entry_id] # ❌ Don't access hass.data
# User-configurable polling intervals
# In config flow
vol.Optional("scan_interval", default=60): cv.positive_int # ❌ Not allowed
# In coordinator
update_interval = timedelta(minutes=entry.data.get("scan_interval", 1)) # ❌ Not allowed
# User-configurable config entry names (non-helper integrations)
vol.Optional("name", default="My Device"): cv.string # ❌ Not allowed in regular integrations
# Too much code in try block
try:
response = await client.get_data() # Can throw
# ❌ Data processing should be outside try block
temperature = response["temperature"] / 10
humidity = response["humidity"]
self._attr_native_value = temperature
except ClientError:
_LOGGER.error("Failed to fetch data")
# Bare exceptions in regular code
try:
value = await sensor.read_value()
except Exception: # ❌ Too broad - catch specific exceptions
_LOGGER.error("Failed to read sensor")
```
### ✅ **Use These Patterns Instead**
```python
# Async operations with executor
data = await hass.async_add_executor_job(requests.get, url)
await asyncio.sleep(5) # ✅ Non-blocking
# Fresh BleakClient instances
client = BleakClient(address) # ✅ New instance each time
await client.connect()
# Translatable entity names
_attr_translation_key = "temperature_sensor" # ✅ Translatable
# Proper error handling
try:
data = await self.api.get_data()
except ApiException as err:
raise UpdateFailed(f"API error: {err}") from err
# Redacted diagnostics data
return async_redact_data(data, {"api_key", "password"}) # ✅ Safe
# Test through proper integration setup and fixtures
@pytest.fixture
async def init_integration(hass, mock_config_entry, mock_api):
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id) # ✅ Proper setup
# Integration-determined polling intervals (not user-configurable)
SCAN_INTERVAL = timedelta(minutes=5) # ✅ Common pattern: constant in const.py
class MyCoordinator(DataUpdateCoordinator[MyData]):
def __init__(self, hass: HomeAssistant, client: MyClient, config_entry: ConfigEntry) -> None:
# ✅ Integration determines interval based on device capabilities, connection type, etc.
interval = timedelta(minutes=1) if client.is_local else SCAN_INTERVAL
super().__init__(
hass,
logger=LOGGER,
name=DOMAIN,
update_interval=interval,
config_entry=config_entry, # ✅ Pass config_entry - it's accepted and recommended
)
```
Integrations with Platinum or Gold level in the Integration Quality Scale reflect a high standard of code quality and maintainability. When looking for examples of something, these are good places to start. The level is indicated in the manifest.json of the integration.
# Skills
+57 -111
View File
@@ -10,7 +10,6 @@ on:
env:
BUILD_TYPE: core
DEFAULT_PYTHON: "3.14.2"
PIP_TIMEOUT: 60
UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true"
@@ -36,16 +35,17 @@ jobs:
channel: ${{ steps.version.outputs.channel }}
publish: ${{ steps.version.outputs.publish }}
architectures: ${{ env.ARCHITECTURES }}
base_image_version: ${{ env.BASE_IMAGE_VERSION }}
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
python-version-file: ".python-version"
- name: Get information
id: info
@@ -73,14 +73,14 @@ jobs:
- name: Download Translations
run: python3 -m script.translations download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env]
- name: Archive translations
shell: bash
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
- name: Upload translations
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: translations
path: translations.tar.gz
@@ -101,7 +101,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }}
include:
- arch: amd64
os: ubuntu-latest
os: ubuntu-24.04
- arch: aarch64
os: ubuntu-24.04-arm
steps:
@@ -112,7 +112,7 @@ jobs:
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@5c98f0b039f36ef966fdb7dfa9779262785ecb05 # v14
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16
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@5c98f0b039f36ef966fdb7dfa9779262785ecb05 # v14
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: OHF-Voice/intents-package
@@ -132,11 +132,11 @@ jobs:
workflow_conclusion: success
name: package
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- name: Set up Python
if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
python-version-file: ".python-version"
- name: Adjust nightly version
if: needs.init.outputs.channel == 'dev'
@@ -182,7 +182,7 @@ jobs:
fi
- name: Download translations
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: translations
@@ -196,77 +196,20 @@ jobs:
run: |
echo "${GITHUB_SHA};${GITHUB_REF};${GITHUB_EVENT_NAME};${GITHUB_ACTOR}" > rootfs/OFFICIAL_IMAGE
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install Cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
with:
cosign-release: "v2.5.3"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build variables
id: vars
shell: bash
env:
ARCH: ${{ matrix.arch }}
run: |
echo "base_image=ghcr.io/home-assistant/${ARCH}-homeassistant-base:${BASE_IMAGE_VERSION}" >> "$GITHUB_OUTPUT"
echo "cache_image=ghcr.io/home-assistant/${ARCH}-homeassistant:latest" >> "$GITHUB_OUTPUT"
echo "created=$(date --rfc-3339=seconds --utc)" >> "$GITHUB_OUTPUT"
- name: Verify base image signature
env:
BASE_IMAGE: ${{ steps.vars.outputs.base_image }}
run: |
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp "https://github.com/home-assistant/docker/.*" \
"${BASE_IMAGE}"
- name: Verify cache image signature
id: cache
continue-on-error: true
env:
CACHE_IMAGE: ${{ steps.vars.outputs.cache_image }}
run: |
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp "https://github.com/home-assistant/core/.*" \
"${CACHE_IMAGE}"
- name: Build base image
id: build
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
with:
context: .
file: ./Dockerfile
platforms: ${{ steps.vars.outputs.platform }}
push: true
cache-from: ${{ steps.cache.outcome == 'success' && steps.vars.outputs.cache_image || '' }}
arch: ${{ matrix.arch }}
build-args: |
BUILD_FROM=${{ steps.vars.outputs.base_image }}
tags: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
outputs: type=image,push=true,compression=zstd,compression-level=9,force-compression=true,oci-mediatypes=true
labels: |
io.hass.arch=${{ matrix.arch }}
io.hass.version=${{ needs.init.outputs.version }}
org.opencontainers.image.created=${{ steps.vars.outputs.created }}
org.opencontainers.image.version=${{ needs.init.outputs.version }}
- name: Sign image
env:
ARCH: ${{ matrix.arch }}
VERSION: ${{ needs.init.outputs.version }}
DIGEST: ${{ steps.build.outputs.digest }}
run: |
cosign sign --yes "ghcr.io/home-assistant/${ARCH}-homeassistant:${VERSION}@${DIGEST}"
BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ needs.init.outputs.base_image_version }}
cache-gha: false
container-registry-password: ${{ secrets.GITHUB_TOKEN }}
cosign-base-identity: "https://github.com/home-assistant/docker/.*"
cosign-base-verify: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ needs.init.outputs.base_image_version }}
image: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant
image-tags: ${{ needs.init.outputs.version }}
push: true
version: ${{ needs.init.outputs.version }}
build_machine:
name: Build ${{ matrix.machine }} machine core image
@@ -315,35 +258,38 @@ jobs:
with:
persist-credentials: false
- name: Set build additional args
- name: Compute extra tags
id: tags
shell: bash
env:
VERSION: ${{ needs.init.outputs.version }}
run: |
# Create general tags
if [[ "${VERSION}" =~ d ]]; then
echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV
echo "extra_tags=dev" >> "$GITHUB_OUTPUT"
elif [[ "${VERSION}" =~ b ]]; then
echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV
echo "extra_tags=beta" >> "$GITHUB_OUTPUT"
else
echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
echo "extra_tags=stable" >> "$GITHUB_OUTPUT"
fi
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
- name: Build machine image
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@6cb4fd3d1338b6e22d0958a4bcb53e0965ea63b4 # 2026.02.1
with:
image: ${{ matrix.arch }}
args: |
$BUILD_ARGS \
--target /data/machine \
--cosign \
--machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}"
arch: ${{ matrix.arch }}
build-args: |
BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
cache-gha: false
container-registry-password: ${{ secrets.GITHUB_TOKEN }}
context: machine/
cosign-base-identity: "https://github.com/home-assistant/core/.*"
cosign-base-verify: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
file: machine/${{ matrix.machine }}
image: ghcr.io/home-assistant/${{ matrix.machine }}-homeassistant
image-tags: |
${{ needs.init.outputs.version }}
${{ steps.tags.outputs.extra_tags }}
push: true
version: ${{ needs.init.outputs.version }}
publish_ha:
name: Publish version files
@@ -401,19 +347,19 @@ jobs:
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
steps:
- name: Install Cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
with:
cosign-release: "v2.5.3"
- name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -443,7 +389,7 @@ jobs:
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: ${{ matrix.registry }}/home-assistant
sep-tags: ","
@@ -457,7 +403,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.7.1
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v3.7.1
- name: Copy architecture images to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
@@ -538,13 +484,13 @@ jobs:
with:
persist-credentials: false
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
python-version-file: ".python-version"
- name: Download translations
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: translations
@@ -586,14 +532,14 @@ jobs:
persist-credentials: false
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
@@ -606,7 +552,7 @@ jobs:
- name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
@@ -615,7 +561,7 @@ jobs:
- name: Generate artifact attestation
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
+50 -50
View File
@@ -41,8 +41,7 @@ env:
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2026.4"
DEFAULT_PYTHON: "3.14.2"
ALL_PYTHON_VERSIONS: "['3.14.2']"
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)
# 10.6 is the current long-term-support
@@ -166,6 +165,11 @@ jobs:
tests_glob=""
lint_only=""
skip_coverage=""
default_python=$(cat .python-version)
all_python_versions=$(jq -cn \
--arg default_python "${default_python}" \
--argjson additional_python_versions "${ADDITIONAL_PYTHON_VERSIONS}" \
'[$default_python] + $additional_python_versions')
if [[ "${INTEGRATION_CHANGES}" != "[]" ]];
then
@@ -235,8 +239,8 @@ jobs:
echo "mariadb_groups=${mariadb_groups}" >> $GITHUB_OUTPUT
echo "postgresql_groups: ${postgresql_groups}"
echo "postgresql_groups=${postgresql_groups}" >> $GITHUB_OUTPUT
echo "python_versions: ${ALL_PYTHON_VERSIONS}"
echo "python_versions=${ALL_PYTHON_VERSIONS}" >> $GITHUB_OUTPUT
echo "python_versions: ${all_python_versions}"
echo "python_versions=${all_python_versions}" >> $GITHUB_OUTPUT
echo "test_full_suite: ${test_full_suite}"
echo "test_full_suite=${test_full_suite}" >> $GITHUB_OUTPUT
echo "integrations_glob: ${integrations_glob}"
@@ -452,7 +456,7 @@ jobs:
python --version
uv pip freeze >> pip_freeze.txt
- name: Upload pip_freeze artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: pip-freeze-${{ matrix.python-version }}
path: pip_freeze.txt
@@ -503,13 +507,13 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- name: Set up Python
id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
python-version-file: ".python-version"
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
@@ -540,13 +544,13 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- name: Set up Python
id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
python-version-file: ".python-version"
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
@@ -576,11 +580,11 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- name: Set up Python
id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
python-version-file: ".python-version"
check-latest: true
- name: Run gen_copilot_instructions.py
run: |
@@ -605,7 +609,7 @@ jobs:
with:
persist-credentials: false
- name: Dependency review
uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3
uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
with:
license-check: false # We use our own license audit checks
@@ -653,7 +657,7 @@ jobs:
. venv/bin/activate
python -m script.licenses extract --output-file=licenses-${PYTHON_VERSION}.json
- name: Upload licenses
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: licenses-${{ github.run_number }}-${{ matrix.python-version }}
path: licenses-${{ matrix.python-version }}.json
@@ -682,13 +686,13 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- name: Set up Python
id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
python-version-file: ".python-version"
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
@@ -735,13 +739,13 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- name: Set up Python
id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
python-version-file: ".python-version"
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
@@ -786,11 +790,11 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- name: Set up Python
id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
python-version-file: ".python-version"
check-latest: true
- name: Generate partial mypy restore key
id: generate-mypy-key
@@ -798,7 +802,7 @@ jobs:
mypy_version=$(cat requirements_test.txt | grep 'mypy.*=' | cut -d '=' -f 3)
echo "version=${mypy_version}" >> $GITHUB_OUTPUT
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 ${{ env.DEFAULT_PYTHON }} virtual environment
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
@@ -848,10 +852,6 @@ jobs:
needs:
- info
- base
- gen-requirements-all
- hassfest
- prek
- mypy
steps:
- name: Restore apt cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
@@ -879,13 +879,13 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- name: Set up Python
id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
python-version-file: ".python-version"
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
@@ -901,7 +901,7 @@ jobs:
. venv/bin/activate
python -m script.split_tests ${TEST_GROUP_COUNT} tests
- name: Upload pytest_buckets
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: pytest_buckets
path: pytest_buckets.txt
@@ -978,7 +978,7 @@ jobs:
run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Download pytest_buckets
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: pytest_buckets
- name: Compile English translations
@@ -1020,14 +1020,14 @@ jobs:
2>&1 | tee pytest-${PYTHON_VERSION}-${TEST_GROUP}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml
@@ -1040,7 +1040,7 @@ jobs:
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: test-results-full-${{ matrix.python-version }}-${{ matrix.group }}
path: junit.xml
@@ -1177,7 +1177,7 @@ jobs:
2>&1 | tee pytest-${PYTHON_VERSION}-${mariadb}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }}
@@ -1185,7 +1185,7 @@ jobs:
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }}
@@ -1199,7 +1199,7 @@ jobs:
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: test-results-mariadb-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }}
@@ -1338,7 +1338,7 @@ jobs:
2>&1 | tee pytest-${PYTHON_VERSION}-${postgresql}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }}
@@ -1346,7 +1346,7 @@ jobs:
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }}
@@ -1360,7 +1360,7 @@ jobs:
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: test-results-postgres-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }}
@@ -1387,7 +1387,7 @@ jobs:
with:
persist-credentials: false
- name: Download all coverage artifacts
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: coverage-*
- name: Upload coverage to Codecov
@@ -1396,7 +1396,7 @@ jobs:
with:
fail_ci_if_error: true
flags: full-suite
token: ${{ secrets.CODECOV_TOKEN }}
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
pytest-partial:
name: Run tests Python ${{ matrix.python-version }} (${{ matrix.group }})
@@ -1514,14 +1514,14 @@ jobs:
2>&1 | tee pytest-${PYTHON_VERSION}-${TEST_GROUP}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml
@@ -1534,7 +1534,7 @@ jobs:
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: test-results-partial-${{ matrix.python-version }}-${{ matrix.group }}
path: junit.xml
@@ -1558,7 +1558,7 @@ jobs:
with:
persist-credentials: false
- name: Download all coverage artifacts
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: coverage-*
- name: Upload coverage to Codecov
@@ -1566,7 +1566,7 @@ jobs:
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
upload-test-results:
name: Upload test results to Codecov
@@ -1587,7 +1587,7 @@ jobs:
&& needs.info.outputs.skip_coverage != 'true' && !cancelled()
steps:
- name: Download all coverage artifacts
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: test-results-*
- name: Upload test results to Codecov
+2 -2
View File
@@ -28,11 +28,11 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
category: "/language:python"
+2 -2
View File
@@ -58,8 +58,8 @@ jobs:
# v1.7.0
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a
with:
app_id: ${{ secrets.ISSUE_TRIAGE_APP_ID }}
private_key: ${{ secrets.ISSUE_TRIAGE_APP_PEM }}
app_id: ${{ secrets.ISSUE_TRIAGE_APP_ID }} # zizmor: ignore[secrets-outside-env]
private_key: ${{ secrets.ISSUE_TRIAGE_APP_PEM }} # zizmor: ignore[secrets-outside-env]
# The 90 day stale policy for issues
# Used for:
+3 -6
View File
@@ -15,9 +15,6 @@ concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
env:
DEFAULT_PYTHON: "3.14.2"
jobs:
upload:
name: Upload
@@ -29,13 +26,13 @@ jobs:
with:
persist-credentials: false
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
python-version-file: ".python-version"
- name: Upload Translations
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env]
run: |
python3 -m script.translations upload
+12 -15
View File
@@ -16,9 +16,6 @@ on:
- "requirements.txt"
- "script/gen_requirements_all.py"
env:
DEFAULT_PYTHON: "3.14.2"
permissions: {}
concurrency:
@@ -36,11 +33,11 @@ jobs:
with:
persist-credentials: false
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- name: Set up Python
id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
python-version-file: ".python-version"
check-latest: true
- name: Create Python virtual environment
@@ -77,7 +74,7 @@ jobs:
) > .env_file
- name: Upload env_file
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: env_file
path: ./.env_file
@@ -85,7 +82,7 @@ jobs:
overwrite: true
- name: Upload requirements_diff
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: requirements_diff
path: ./requirements_diff.txt
@@ -97,7 +94,7 @@ jobs:
python -m script.gen_requirements_all ci
- name: Upload requirements_all_wheels
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: requirements_all_wheels
path: ./requirements_all_wheels_*.txt
@@ -124,12 +121,12 @@ jobs:
persist-credentials: false
- name: Download env_file
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: env_file
- name: Download requirements_diff
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: requirements_diff
@@ -145,7 +142,7 @@ jobs:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
wheels-key: ${{ secrets.WHEELS_KEY }} # zizmor: ignore[secrets-outside-env]
env-file: true
apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-ng-dev"
skip-binary: aiohttp;multidict;propcache;yarl;SQLAlchemy
@@ -175,17 +172,17 @@ jobs:
persist-credentials: false
- name: Download env_file
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: env_file
- name: Download requirements_diff
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: requirements_diff
- name: Download requirements_all_wheels
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: requirements_all_wheels
@@ -203,7 +200,7 @@ jobs:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
wheels-key: ${{ secrets.WHEELS_KEY }} # zizmor: ignore[secrets-outside-env]
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-ng-dev"
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
+1 -1
View File
@@ -18,7 +18,7 @@ repos:
exclude_types: [csv, json, html]
exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.22.0
rev: v1.23.1
hooks:
- id: zizmor
args:
+1 -1
View File
@@ -1 +1 @@
3.14
3.14.2
+4 -1
View File
@@ -123,7 +123,6 @@ homeassistant.components.blueprint.*
homeassistant.components.bluesound.*
homeassistant.components.bluetooth.*
homeassistant.components.bluetooth_adapters.*
homeassistant.components.bmw_connected_drive.*
homeassistant.components.bond.*
homeassistant.components.bosch_alarm.*
homeassistant.components.braviatv.*
@@ -174,6 +173,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.*
@@ -213,6 +213,7 @@ homeassistant.components.flexit_bacnet.*
homeassistant.components.flux_led.*
homeassistant.components.folder_watcher.*
homeassistant.components.forecast_solar.*
homeassistant.components.freshr.*
homeassistant.components.fritz.*
homeassistant.components.fritzbox.*
homeassistant.components.fritzbox_callmonitor.*
@@ -342,6 +343,7 @@ homeassistant.components.lookin.*
homeassistant.components.lovelace.*
homeassistant.components.luftdaten.*
homeassistant.components.lunatone.*
homeassistant.components.lutron.*
homeassistant.components.madvr.*
homeassistant.components.manual.*
homeassistant.components.mastodon.*
@@ -569,6 +571,7 @@ homeassistant.components.trafikverket_train.*
homeassistant.components.trafikverket_weatherstation.*
homeassistant.components.transmission.*
homeassistant.components.trend.*
homeassistant.components.trmnl.*
homeassistant.components.tts.*
homeassistant.components.twentemilieu.*
homeassistant.components.unifi.*
+8 -311
View File
@@ -4,325 +4,22 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
## Code Review Guidelines
**When reviewing code, do NOT comment on:**
- **Missing imports** - We use static analysis tooling to catch that
- **Code formatting** - We have ruff as a formatting tool that will catch those if needed (unless specifically instructed otherwise in these instructions)
**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
## Python Requirements
- **Compatibility**: Python 3.13+
- **Language Features**: Use the newest features when possible:
- Pattern matching
- Type hints
- f-strings (preferred over `%` or `.format()`)
- Dataclasses
- Walrus operator
### Strict Typing (Platinum)
- **Comprehensive Type Hints**: Add type hints to all functions, methods, and variables
- **Custom Config Entry Types**: When using runtime_data:
```python
type MyIntegrationConfigEntry = ConfigEntry[MyClient]
```
- **Library Requirements**: Include `py.typed` file for PEP-561 compliance
## Code Quality Standards
- **Formatting**: Ruff
- **Linting**: PyLint and Ruff
- **Type Checking**: MyPy
- **Lint/Type/Format Fixes**: Always prefer addressing the underlying issue (e.g., import the typed source, update shared stubs, align with Ruff expectations, or correct formatting at the source) before disabling a rule, adding `# type: ignore`, or skipping a formatter. Treat suppressions and `noqa` comments as a last resort once no compliant fix exists
- **Testing**: pytest with plain functions and fixtures
- **Language**: American English for all code, comments, and documentation (use sentence case, including titles)
### Writing Style Guidelines
- **Tone**: Friendly and informative
- **Perspective**: Use second-person ("you" and "your") for user-facing messages
- **Inclusivity**: Use objective, non-discriminatory language
- **Clarity**: Write for non-native English speakers
- **Formatting in Messages**:
- Use backticks for: file paths, filenames, variable names, field entries
- Use sentence case for titles and messages (capitalize only the first word and proper nouns)
- Avoid abbreviations when possible
### Documentation Standards
- **File Headers**: Short and concise
```python
"""Integration for Peblar EV chargers."""
```
- **Method/Function Docstrings**: Required for all
```python
async def async_setup_entry(hass: HomeAssistant, entry: PeblarConfigEntry) -> bool:
"""Set up Peblar from a config entry."""
```
- **Comment Style**:
- Use clear, descriptive comments
- Explain the "why" not just the "what"
- Keep code block lines under 80 characters when possible
- Use progressive disclosure (simple explanation first, complex details later)
## Async Programming
- All external I/O operations must be async
- **Best Practices**:
- Avoid sleeping in loops
- Avoid awaiting in loops - use `gather` instead
- No blocking calls
- Group executor jobs when possible - switching between event loop and executor is expensive
### Blocking Operations
- **Use Executor**: For blocking I/O operations
```python
result = await hass.async_add_executor_job(blocking_function, args)
```
- **Never Block Event Loop**: Avoid file operations, `time.sleep()`, blocking HTTP calls
- **Replace with Async**: Use `asyncio.sleep()` instead of `time.sleep()`
### Thread Safety
- **@callback Decorator**: For event loop safe functions
```python
@callback
def async_update_callback(self, event):
"""Safe to run in event loop."""
self.async_write_ha_state()
```
- **Sync APIs from Threads**: Use sync versions when calling from non-event loop threads
- **Registry Changes**: Must be done in event loop thread
### Error Handling
- **Exception Types**: Choose most specific exception available
- `ServiceValidationError`: User input errors (preferred over `ValueError`)
- `HomeAssistantError`: Device communication failures
- `ConfigEntryNotReady`: Temporary setup issues (device offline)
- `ConfigEntryAuthFailed`: Authentication problems
- `ConfigEntryError`: Permanent setup issues
- **Try/Catch Best Practices**:
- Only wrap code that can throw exceptions
- Keep try blocks minimal - process data after the try/catch
- **Avoid bare exceptions** except in specific cases:
- ❌ Generally not allowed: `except:` or `except Exception:`
- ✅ Allowed in config flows to ensure robustness
- ✅ Allowed in functions/methods that run in background tasks
- Bad pattern:
```python
try:
data = await device.get_data() # Can throw
# ❌ Don't process data inside try block
processed = data.get("value", 0) * 100
self._attr_native_value = processed
except DeviceError:
_LOGGER.error("Failed to get data")
```
- Good pattern:
```python
try:
data = await device.get_data() # Can throw
except DeviceError:
_LOGGER.error("Failed to get data")
return
# ✅ Process data outside try block
processed = data.get("value", 0) * 100
self._attr_native_value = processed
```
- **Bare Exception Usage**:
```python
# ❌ Not allowed in regular code
try:
data = await device.get_data()
except Exception: # Too broad
_LOGGER.error("Failed")
# ✅ Allowed in config flow for robustness
async def async_step_user(self, user_input=None):
try:
await self._test_connection(user_input)
except Exception: # Allowed here
errors["base"] = "unknown"
# ✅ Allowed in background tasks
async def _background_refresh():
try:
await coordinator.async_refresh()
except Exception: # Allowed in task
_LOGGER.exception("Unexpected error in background task")
```
- **Setup Failure Patterns**:
```python
try:
await device.async_setup()
except (asyncio.TimeoutError, TimeoutException) as ex:
raise ConfigEntryNotReady(f"Timeout connecting to {device.host}") from ex
except AuthFailed as ex:
raise ConfigEntryAuthFailed(f"Credentials expired for {device.name}") from ex
```
### Logging
- **Format Guidelines**:
- No periods at end of messages
- No integration names/domains (added automatically)
- No sensitive data (keys, tokens, passwords)
- Use debug level for non-user-facing messages
- **Use Lazy Logging**:
```python
_LOGGER.debug("This is a log message with %s", variable)
```
### Unavailability Logging
- **Log Once**: When device/service becomes unavailable (info level)
- **Log Recovery**: When device/service comes back online
- **Implementation Pattern**:
```python
_unavailable_logged: bool = False
if not self._unavailable_logged:
_LOGGER.info("The sensor is unavailable: %s", ex)
self._unavailable_logged = True
# On recovery:
if self._unavailable_logged:
_LOGGER.info("The sensor is back online")
self._unavailable_logged = False
```
## Development Commands
### Environment
- **Local development (non-container)**: Activate the project venv before running commands: `source .venv/bin/activate`
- **Dev container**: No activation needed, the environment is pre-configured
.vscode/tasks.json contains useful commands used for development.
### Code Quality & Linting
- **Run all linters on all files**: `prek run --all-files`
- **Run linters on staged files only**: `prek run`
- **PyLint on everything** (slow): `pylint homeassistant`
- **PyLint on specific folder**: `pylint homeassistant/components/my_integration`
- **MyPy type checking (whole project)**: `mypy homeassistant/`
- **MyPy on specific integration**: `mypy homeassistant/components/my_integration`
## Python Syntax Notes
### Testing
- **Quick test of changed files**: `pytest --timeout=10 --picked`
- **Update test snapshots**: Add `--snapshot-update` to pytest command
- ⚠️ Omit test results after using `--snapshot-update`
- Always run tests again without the flag to verify snapshots
- **Full test suite** (AVOID - very slow): `pytest ./tests`
- Python 3.14 explicitly allows `except TypeA, TypeB:` without parentheses.
### Dependencies & Requirements
- **Update generated files after dependency changes**: `python -m script.gen_requirements_all`
- **Install all Python requirements**:
```bash
uv pip install -r requirements_all.txt -r requirements.txt -r requirements_test.txt
```
- **Install test requirements only**:
```bash
uv pip install -r requirements_test_all.txt -r requirements.txt
```
## Testing
### Translations
- **Update translations after strings.json changes**:
```bash
python -m script.translations develop --all
```
When writing or modifying tests, ensure all test function parameters have type annotations.
Prefer concrete types (for example, `HomeAssistant`, `MockConfigEntry`, etc.) over `Any`.
### Project Validation
- **Run hassfest** (checks project structure and updates generated files):
```bash
python -m script.hassfest
```
## Good practices
## Common Anti-Patterns & Best Practices
### ❌ **Avoid These Patterns**
```python
# Blocking operations in event loop
data = requests.get(url) # ❌ Blocks event loop
time.sleep(5) # ❌ Blocks event loop
# Reusing BleakClient instances
self.client = BleakClient(address)
await self.client.connect()
# Later...
await self.client.connect() # ❌ Don't reuse
# Hardcoded strings in code
self._attr_name = "Temperature Sensor" # ❌ Not translatable
# Missing error handling
data = await self.api.get_data() # ❌ No exception handling
# Storing sensitive data in diagnostics
return {"api_key": entry.data[CONF_API_KEY]} # ❌ Exposes secrets
# Accessing hass.data directly in tests
coordinator = hass.data[DOMAIN][entry.entry_id] # ❌ Don't access hass.data
# User-configurable polling intervals
# In config flow
vol.Optional("scan_interval", default=60): cv.positive_int # ❌ Not allowed
# In coordinator
update_interval = timedelta(minutes=entry.data.get("scan_interval", 1)) # ❌ Not allowed
# User-configurable config entry names (non-helper integrations)
vol.Optional("name", default="My Device"): cv.string # ❌ Not allowed in regular integrations
# Too much code in try block
try:
response = await client.get_data() # Can throw
# ❌ Data processing should be outside try block
temperature = response["temperature"] / 10
humidity = response["humidity"]
self._attr_native_value = temperature
except ClientError:
_LOGGER.error("Failed to fetch data")
# Bare exceptions in regular code
try:
value = await sensor.read_value()
except Exception: # ❌ Too broad - catch specific exceptions
_LOGGER.error("Failed to read sensor")
```
### ✅ **Use These Patterns Instead**
```python
# Async operations with executor
data = await hass.async_add_executor_job(requests.get, url)
await asyncio.sleep(5) # ✅ Non-blocking
# Fresh BleakClient instances
client = BleakClient(address) # ✅ New instance each time
await client.connect()
# Translatable entity names
_attr_translation_key = "temperature_sensor" # ✅ Translatable
# Proper error handling
try:
data = await self.api.get_data()
except ApiException as err:
raise UpdateFailed(f"API error: {err}") from err
# Redacted diagnostics data
return async_redact_data(data, {"api_key", "password"}) # ✅ Safe
# Test through proper integration setup and fixtures
@pytest.fixture
async def init_integration(hass, mock_config_entry, mock_api):
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id) # ✅ Proper setup
# Integration-determined polling intervals (not user-configurable)
SCAN_INTERVAL = timedelta(minutes=5) # ✅ Common pattern: constant in const.py
class MyCoordinator(DataUpdateCoordinator[MyData]):
def __init__(self, hass: HomeAssistant, client: MyClient, config_entry: ConfigEntry) -> None:
# ✅ Integration determines interval based on device capabilities, connection type, etc.
interval = timedelta(minutes=1) if client.is_local else SCAN_INTERVAL
super().__init__(
hass,
logger=LOGGER,
name=DOMAIN,
update_interval=interval,
config_entry=config_entry, # ✅ Pass config_entry - it's accepted and recommended
)
```
Integrations with Platinum or Gold level in the Integration Quality Scale reflect a high standard of code quality and maintainability. When looking for examples of something, these are good places to start. The level is indicated in the manifest.json of the integration.
Generated
+42 -15
View File
@@ -186,6 +186,8 @@ build.json @home-assistant/supervisor
/tests/components/auth/ @home-assistant/core
/homeassistant/components/automation/ @home-assistant/core
/tests/components/automation/ @home-assistant/core
/homeassistant/components/autoskope/ @mcisk
/tests/components/autoskope/ @mcisk
/homeassistant/components/avea/ @pattyland
/homeassistant/components/awair/ @ahayworth @ricohageman
/tests/components/awair/ @ahayworth @ricohageman
@@ -234,8 +236,6 @@ build.json @home-assistant/supervisor
/tests/components/bluetooth/ @bdraco
/homeassistant/components/bluetooth_adapters/ @bdraco
/tests/components/bluetooth_adapters/ @bdraco
/homeassistant/components/bmw_connected_drive/ @gerard33 @rikroe
/tests/components/bmw_connected_drive/ @gerard33 @rikroe
/homeassistant/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
/tests/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
/homeassistant/components/bosch_alarm/ @mag1024 @sanjay900
@@ -281,6 +281,8 @@ build.json @home-assistant/supervisor
/tests/components/cert_expiry/ @jjlawren
/homeassistant/components/chacon_dio/ @cnico
/tests/components/chacon_dio/ @cnico
/homeassistant/components/chess_com/ @joostlek
/tests/components/chess_com/ @joostlek
/homeassistant/components/cisco_ios/ @fbradyirl
/homeassistant/components/cisco_mobility_express/ @fbradyirl
/homeassistant/components/cisco_webex_teams/ @fbradyirl
@@ -383,6 +385,8 @@ build.json @home-assistant/supervisor
/tests/components/dlna_dms/ @chishm
/homeassistant/components/dnsip/ @gjohansson-ST
/tests/components/dnsip/ @gjohansson-ST
/homeassistant/components/door/ @home-assistant/core
/tests/components/door/ @home-assistant/core
/homeassistant/components/doorbird/ @oblogic7 @bdraco @flacjacket
/tests/components/doorbird/ @oblogic7 @bdraco @flacjacket
/homeassistant/components/dormakaba_dkey/ @emontnemery
@@ -393,6 +397,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
@@ -549,6 +555,8 @@ build.json @home-assistant/supervisor
/tests/components/freebox/ @hacf-fr @Quentame
/homeassistant/components/freedompro/ @stefano055415
/tests/components/freedompro/ @stefano055415
/homeassistant/components/freshr/ @SierraNL
/tests/components/freshr/ @SierraNL
/homeassistant/components/fressnapf_tracker/ @eifinger
/tests/components/fressnapf_tracker/ @eifinger
/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
@@ -567,10 +575,14 @@ build.json @home-assistant/supervisor
/tests/components/fully_kiosk/ @cgarwood
/homeassistant/components/fyta/ @dontinelli
/tests/components/fyta/ @dontinelli
/homeassistant/components/garage_door/ @home-assistant/core
/tests/components/garage_door/ @home-assistant/core
/homeassistant/components/garages_amsterdam/ @klaasnicolaas
/tests/components/garages_amsterdam/ @klaasnicolaas
/homeassistant/components/gardena_bluetooth/ @elupus
/tests/components/gardena_bluetooth/ @elupus
/homeassistant/components/gate/ @home-assistant/core
/tests/components/gate/ @home-assistant/core
/homeassistant/components/gdacs/ @exxamalte
/tests/components/gdacs/ @exxamalte
/homeassistant/components/generic/ @davet2001
@@ -737,6 +749,8 @@ build.json @home-assistant/supervisor
/tests/components/huisbaasje/ @dennisschroer
/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka
/tests/components/humidifier/ @home-assistant/core @Shulyaka
/homeassistant/components/humidity/ @home-assistant/core
/tests/components/humidity/ @home-assistant/core
/homeassistant/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
/homeassistant/components/husqvarna_automower/ @Thomas55555
@@ -786,8 +800,8 @@ build.json @home-assistant/supervisor
/tests/components/improv_ble/ @emontnemery
/homeassistant/components/incomfort/ @jbouwh
/tests/components/incomfort/ @jbouwh
/homeassistant/components/indevolt/ @xirtnl
/tests/components/indevolt/ @xirtnl
/homeassistant/components/indevolt/ @xirt
/tests/components/indevolt/ @xirt
/homeassistant/components/inels/ @epdevlab
/tests/components/inels/ @epdevlab
/homeassistant/components/influxdb/ @mdegat01 @Robbie1221
@@ -962,6 +976,8 @@ build.json @home-assistant/supervisor
/tests/components/logbook/ @home-assistant/core
/homeassistant/components/logger/ @home-assistant/core
/tests/components/logger/ @home-assistant/core
/homeassistant/components/lojack/ @devinslick
/tests/components/lojack/ @devinslick
/homeassistant/components/london_underground/ @jpbede
/tests/components/london_underground/ @jpbede
/homeassistant/components/lookin/ @ANMalko @bdraco
@@ -1061,6 +1077,8 @@ build.json @home-assistant/supervisor
/tests/components/moon/ @fabaff @frenck
/homeassistant/components/mopeka/ @bdraco
/tests/components/mopeka/ @bdraco
/homeassistant/components/motion/ @home-assistant/core
/tests/components/motion/ @home-assistant/core
/homeassistant/components/motion_blinds/ @starkillerOG
/tests/components/motion_blinds/ @starkillerOG
/homeassistant/components/motionblinds_ble/ @LennP @jerrybboy
@@ -1174,6 +1192,8 @@ build.json @home-assistant/supervisor
/tests/components/nzbget/ @chriscla
/homeassistant/components/obihai/ @dshokouhi @ejpenney
/tests/components/obihai/ @dshokouhi @ejpenney
/homeassistant/components/occupancy/ @home-assistant/core
/tests/components/occupancy/ @home-assistant/core
/homeassistant/components/octoprint/ @rfleming71
/tests/components/octoprint/ @rfleming71
/homeassistant/components/ohmconnect/ @robbiet480
@@ -1200,6 +1220,8 @@ build.json @home-assistant/supervisor
/tests/components/open_meteo/ @frenck
/homeassistant/components/open_router/ @joostlek
/tests/components/open_router/ @joostlek
/homeassistant/components/opendisplay/ @g4bri3lDev
/tests/components/opendisplay/ @g4bri3lDev
/homeassistant/components/openerz/ @misialq
/tests/components/openerz/ @misialq
/homeassistant/components/openevse/ @c00w @firstof9
@@ -1305,8 +1327,8 @@ build.json @home-assistant/supervisor
/tests/components/prosegur/ @dgomes
/homeassistant/components/proximity/ @mib1185
/tests/components/proximity/ @mib1185
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno @erwindouna
/tests/components/proxmoxve/ @jhollowe @Corbeno @erwindouna
/homeassistant/components/proxmoxve/ @Corbeno @erwindouna @CoMPaTech
/tests/components/proxmoxve/ @Corbeno @erwindouna @CoMPaTech
/homeassistant/components/ps4/ @ktnrg45
/tests/components/ps4/ @ktnrg45
/homeassistant/components/pterodactyl/ @elmurato
@@ -1541,8 +1563,8 @@ build.json @home-assistant/supervisor
/tests/components/sma/ @kellerza @rklomp @erwindouna
/homeassistant/components/smappee/ @bsmappee
/tests/components/smappee/ @bsmappee
/homeassistant/components/smarla/ @explicatis @rlint-explicatis
/tests/components/smarla/ @explicatis @rlint-explicatis
/homeassistant/components/smarla/ @explicatis @johannes-exp
/tests/components/smarla/ @explicatis @johannes-exp
/homeassistant/components/smart_meter_texas/ @grahamwetzler
/tests/components/smart_meter_texas/ @grahamwetzler
/homeassistant/components/smartthings/ @joostlek
@@ -1596,8 +1618,6 @@ build.json @home-assistant/supervisor
/tests/components/srp_energy/ @briglx
/homeassistant/components/starline/ @anonym-tsk
/tests/components/starline/ @anonym-tsk
/homeassistant/components/starlink/ @boswelja
/tests/components/starlink/ @boswelja
/homeassistant/components/statistics/ @ThomDietrich @gjohansson-ST
/tests/components/statistics/ @ThomDietrich @gjohansson-ST
/homeassistant/components/steam_online/ @tkdrob
@@ -1650,8 +1670,8 @@ build.json @home-assistant/supervisor
/tests/components/system_bridge/ @timmo001
/homeassistant/components/systemmonitor/ @gjohansson-ST
/tests/components/systemmonitor/ @gjohansson-ST
/homeassistant/components/systemnexa2/ @konsulten @slangstrom
/tests/components/systemnexa2/ @konsulten @slangstrom
/homeassistant/components/systemnexa2/ @konsulten
/tests/components/systemnexa2/ @konsulten
/homeassistant/components/tado/ @erwindouna
/tests/components/tado/ @erwindouna
/homeassistant/components/tag/ @home-assistant/core
@@ -1691,7 +1711,6 @@ build.json @home-assistant/supervisor
/tests/components/tessie/ @Bre77
/homeassistant/components/text/ @home-assistant/core
/tests/components/text/ @home-assistant/core
/homeassistant/components/tfiac/ @fredrike @mellado
/homeassistant/components/thermobeacon/ @bdraco
/tests/components/thermobeacon/ @bdraco
/homeassistant/components/thermopro/ @bdraco @h3ss
@@ -1753,6 +1772,8 @@ build.json @home-assistant/supervisor
/tests/components/trend/ @jpbede
/homeassistant/components/triggercmd/ @rvmey
/tests/components/triggercmd/ @rvmey
/homeassistant/components/trmnl/ @joostlek
/tests/components/trmnl/ @joostlek
/homeassistant/components/tts/ @home-assistant/core
/tests/components/tts/ @home-assistant/core
/homeassistant/components/tuya/ @Tuya @zlinoliver
@@ -1769,6 +1790,8 @@ build.json @home-assistant/supervisor
/tests/components/ukraine_alarm/ @PaulAnnekov
/homeassistant/components/unifi/ @Kane610
/tests/components/unifi/ @Kane610
/homeassistant/components/unifi_access/ @imhotep @RaHehl
/tests/components/unifi_access/ @imhotep @RaHehl
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
/homeassistant/components/unifiled/ @florisvdk
/homeassistant/components/unifiprotect/ @RaHehl
@@ -1808,8 +1831,8 @@ build.json @home-assistant/supervisor
/tests/components/vegehub/ @thulrus
/homeassistant/components/velbus/ @Cereal2nd @brefra
/tests/components/velbus/ @Cereal2nd @brefra
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
/tests/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
/homeassistant/components/velux/ @Julius2342 @pawlizio @wollew
/tests/components/velux/ @Julius2342 @pawlizio @wollew
/homeassistant/components/venstar/ @garbled1 @jhollowe
/tests/components/venstar/ @garbled1 @jhollowe
/homeassistant/components/versasense/ @imstevenxyz
@@ -1892,8 +1915,12 @@ build.json @home-assistant/supervisor
/tests/components/whois/ @frenck
/homeassistant/components/wiffi/ @mampfes
/tests/components/wiffi/ @mampfes
/homeassistant/components/wiim/ @Linkplay2020
/tests/components/wiim/ @Linkplay2020
/homeassistant/components/wilight/ @leofig-rj
/tests/components/wilight/ @leofig-rj
/homeassistant/components/window/ @home-assistant/core
/tests/components/window/ @home-assistant/core
/homeassistant/components/wirelesstag/ @sergeymaysak
/homeassistant/components/withings/ @joostlek
/tests/components/withings/ @joostlek
Generated
-1
View File
@@ -10,7 +10,6 @@ LABEL \
org.opencontainers.image.description="Open-source home automation platform running on Python 3" \
org.opencontainers.image.documentation="https://www.home-assistant.io/docs/" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.source="https://github.com/home-assistant/core" \
org.opencontainers.image.title="Home Assistant" \
org.opencontainers.image.url="https://www.home-assistant.io/"
+14
View File
@@ -236,9 +236,23 @@ DEFAULT_INTEGRATIONS = {
"input_text",
"schedule",
"timer",
#
# Base platforms:
*BASE_PLATFORMS,
#
# Integrations providing triggers and conditions for base platforms:
"door",
"garage_door",
"gate",
"humidity",
"motion",
"occupancy",
"window",
}
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
# These integrations are set up if recovery mode is activated.
"backup",
"cloud",
"frontend",
}
DEFAULT_INTEGRATIONS_SUPERVISOR = {
+8 -1
View File
@@ -1,5 +1,12 @@
{
"domain": "ubiquiti",
"name": "Ubiquiti",
"integrations": ["airos", "unifi", "unifi_direct", "unifiled", "unifiprotect"]
"integrations": [
"airos",
"unifi",
"unifi_access",
"unifi_direct",
"unifiled",
"unifiprotect"
]
}
@@ -120,7 +120,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="timeout",
)
del self.login_task
self.login_task = None
return await self.async_step_user()
async def async_step_reauth(
@@ -12,6 +12,6 @@
"documentation": "https://www.home-assistant.io/integrations/actron_air",
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"quality_scale": "silver",
"requirements": ["actron-neo-api==0.4.1"]
}
@@ -37,7 +37,7 @@ rules:
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: done
test-coverage: todo
test-coverage: done
# Gold
devices: done
+34 -6
View File
@@ -168,29 +168,57 @@ class LocalAdaxDevice(CoordinatorEntity[AdaxLocalCoordinator], ClimateEntity):
if hvac_mode == HVACMode.HEAT:
temperature = self._attr_target_temperature or self._attr_min_temp
await self._adax_data_handler.set_target_temperature(temperature)
self._attr_target_temperature = temperature
self._attr_icon = "mdi:radiator"
elif hvac_mode == HVACMode.OFF:
await self._adax_data_handler.set_target_temperature(0)
self._attr_icon = "mdi:radiator-off"
else:
# Ignore unsupported HVAC modes to avoid desynchronizing entity state
# from the physical device.
return
self._attr_hvac_mode = hvac_mode
self.async_write_ha_state()
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
return
await self._adax_data_handler.set_target_temperature(temperature)
if self._attr_hvac_mode == HVACMode.HEAT:
await self._adax_data_handler.set_target_temperature(temperature)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._attr_target_temperature = temperature
self.async_write_ha_state()
def _update_hvac_attributes(self) -> None:
"""Update hvac mode and temperatures from coordinator data.
The coordinator reports a target temperature of 0 when the heater is
turned off. In that case, only the hvac mode and icon are updated and
the previous non-zero target temperature is preserved. When the
reported target temperature is non-zero, the stored target temperature
is updated to match the coordinator value.
"""
if data := self.coordinator.data:
self._attr_current_temperature = data["current_temperature"]
self._attr_available = self._attr_current_temperature is not None
if (target_temp := data["target_temperature"]) == 0:
self._attr_hvac_mode = HVACMode.OFF
self._attr_icon = "mdi:radiator-off"
if target_temp == 0:
if self._attr_target_temperature is None:
self._attr_target_temperature = self._attr_min_temp
else:
self._attr_hvac_mode = HVACMode.HEAT
self._attr_icon = "mdi:radiator"
self._attr_target_temperature = target_temp
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._update_hvac_attributes()
super()._handle_coordinator_update()
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self._update_hvac_attributes()
@@ -18,6 +18,10 @@ from homeassistant.helpers.schema_config_entry_flow import (
SchemaOptionsFlowHandler,
)
from homeassistant.helpers.selector import BooleanSelector
from homeassistant.helpers.service_info.zeroconf import (
ATTR_PROPERTIES_ID,
ZeroconfServiceInfo,
)
from .const import CONF_CLIP_NEGATIVE, CONF_RETURN_AVERAGE, DOMAIN
@@ -46,6 +50,9 @@ class AirQConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
_discovered_host: str
_discovered_name: str
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -90,6 +97,58 @@ class AirQConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle zeroconf discovery of an air-Q device."""
self._discovered_host = discovery_info.host
self._discovered_name = discovery_info.properties.get("devicename", "air-Q")
device_id = discovery_info.properties.get(ATTR_PROPERTIES_ID)
if not device_id:
return self.async_abort(reason="incomplete_discovery")
await self.async_set_unique_id(device_id)
self._abort_if_unique_id_configured(
updates={CONF_IP_ADDRESS: self._discovered_host},
reload_on_update=True,
)
self.context["title_placeholders"] = {"name": self._discovered_name}
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle user confirmation of a discovered air-Q device."""
errors: dict[str, str] = {}
if user_input is not None:
session = async_get_clientsession(self.hass)
airq = AirQ(self._discovered_host, user_input[CONF_PASSWORD], session)
try:
await airq.validate()
except ClientConnectionError:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
else:
return self.async_create_entry(
title=self._discovered_name,
data={
CONF_IP_ADDRESS: self._discovered_host,
CONF_PASSWORD: user_input[CONF_PASSWORD],
},
)
return self.async_show_form(
step_id="discovery_confirm",
data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}),
description_placeholders={"name": self._discovered_name},
errors=errors,
)
@staticmethod
@callback
def async_get_options_flow(
+9 -1
View File
@@ -7,5 +7,13 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioairq"],
"requirements": ["aioairq==0.4.7"]
"requirements": ["aioairq==0.4.7"],
"zeroconf": [
{
"properties": {
"device": "air-q"
},
"type": "_http._tcp.local."
}
]
}
+10 -1
View File
@@ -1,14 +1,23 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"incomplete_discovery": "The discovered air-Q device did not provide a device ID. Ensure the firmware is up to date."
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_input": "[%key:common::config_flow::error::invalid_host%]"
},
"flow_title": "{name}",
"step": {
"discovery_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"description": "Do you want to set up **{name}**?",
"title": "Set up air-Q"
},
"user": {
"data": {
"ip_address": "[%key:common::config_flow::data::ip%]",
@@ -117,23 +117,23 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
return super()._handle_coordinator_update()
@property
def current_temperature(self):
def current_temperature(self) -> int:
"""Return the current temperature."""
return self._unit.Temperature
@property
def fan_mode(self):
def fan_mode(self) -> str:
"""Return fan mode of the AC this group belongs to."""
return AT_TO_HA_FAN_SPEED[self._airtouch.acs[self._ac_number].AcFanSpeed]
@property
def fan_modes(self):
def fan_modes(self) -> list[str]:
"""Return the list of available fan modes."""
airtouch_fan_speeds = self._airtouch.GetSupportedFanSpeedsForAc(self._ac_number)
return [AT_TO_HA_FAN_SPEED[speed] for speed in airtouch_fan_speeds]
@property
def hvac_mode(self):
def hvac_mode(self) -> HVACMode:
"""Return hvac target hvac state."""
is_off = self._unit.PowerState == "Off"
if is_off:
@@ -236,17 +236,17 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
return self._airtouch.acs[self._unit.BelongsToAc].MaxSetpoint
@property
def current_temperature(self):
def current_temperature(self) -> int:
"""Return the current temperature."""
return self._unit.Temperature
@property
def target_temperature(self):
def target_temperature(self) -> int:
"""Return the temperature we are trying to reach."""
return self._unit.TargetSetpoint
@property
def hvac_mode(self):
def hvac_mode(self) -> HVACMode:
"""Return hvac target hvac state."""
# there are other power states that aren't 'on' but still count as on (eg. 'Turbo')
is_off = self._unit.PowerState == "Off"
@@ -272,12 +272,12 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
self.async_write_ha_state()
@property
def fan_mode(self):
def fan_mode(self) -> str:
"""Return fan mode of the AC this group belongs to."""
return AT_TO_HA_FAN_SPEED[self._airtouch.acs[self._unit.BelongsToAc].AcFanSpeed]
@property
def fan_modes(self):
def fan_modes(self) -> list[str]:
"""Return the list of available fan modes."""
airtouch_fan_speeds = self._airtouch.GetSupportedFanSpeedsByGroup(
self._group_number
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["airtouch5py"],
"requirements": ["airtouch5py==0.3.0"]
"requirements": ["airtouch5py==0.4.0"]
}
+7 -43
View File
@@ -7,13 +7,7 @@ from datetime import timedelta
from math import ceil
from typing import Any
from pyairvisual.cloud_api import (
CloudAPI,
InvalidKeyError,
KeyExpiredError,
UnauthorizedError,
)
from pyairvisual.errors import AirVisualError
from pyairvisual.cloud_api import CloudAPI
from homeassistant.components import automation
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
@@ -28,14 +22,12 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import (
aiohttp_client,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
CONF_CITY,
@@ -47,8 +39,7 @@ from .const import (
INTEGRATION_TYPE_NODE_PRO,
LOGGER,
)
type AirVisualConfigEntry = ConfigEntry[DataUpdateCoordinator]
from .coordinator import AirVisualConfigEntry, AirVisualDataUpdateCoordinator
# We use a raw string for the airvisual_pro domain (instead of importing the actual
# constant) so that we can avoid listing it as a dependency:
@@ -85,8 +76,8 @@ def async_get_cloud_api_update_interval(
@callback
def async_get_cloud_coordinators_by_api_key(
hass: HomeAssistant, api_key: str
) -> list[DataUpdateCoordinator]:
"""Get all DataUpdateCoordinator objects related to a particular API key."""
) -> list[AirVisualDataUpdateCoordinator]:
"""Get all AirVisualDataUpdateCoordinator objects related to a particular API key."""
return [
entry.runtime_data
for entry in hass.config_entries.async_entries(DOMAIN)
@@ -180,38 +171,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) ->
websession = aiohttp_client.async_get_clientsession(hass)
cloud_api = CloudAPI(entry.data[CONF_API_KEY], session=websession)
async def async_update_data() -> dict[str, Any]:
"""Get new data from the API."""
if CONF_CITY in entry.data:
api_coro = cloud_api.air_quality.city(
entry.data[CONF_CITY],
entry.data[CONF_STATE],
entry.data[CONF_COUNTRY],
)
else:
api_coro = cloud_api.air_quality.nearest_city(
entry.data[CONF_LATITUDE],
entry.data[CONF_LONGITUDE],
)
try:
return await api_coro
except (InvalidKeyError, KeyExpiredError, UnauthorizedError) as ex:
raise ConfigEntryAuthFailed from ex
except AirVisualError as err:
raise UpdateFailed(f"Error while retrieving data: {err}") from err
coordinator = DataUpdateCoordinator(
coordinator = AirVisualDataUpdateCoordinator(
hass,
LOGGER,
config_entry=entry,
entry,
cloud_api,
name=async_get_geography_id(entry.data),
# We give a placeholder update interval in order to create the coordinator;
# then, below, we use the coordinator's presence (along with any other
# coordinators using the same API key) to calculate an actual, leveled
# update interval:
update_interval=timedelta(minutes=5),
update_method=async_update_data,
)
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
@@ -0,0 +1,72 @@
"""Define an AirVisual data coordinator."""
from __future__ import annotations
from datetime import timedelta
from typing import Any
from pyairvisual.cloud_api import (
CloudAPI,
InvalidKeyError,
KeyExpiredError,
UnauthorizedError,
)
from pyairvisual.errors import AirVisualError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_COUNTRY, CONF_LATITUDE, CONF_LONGITUDE, CONF_STATE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_CITY, LOGGER
type AirVisualConfigEntry = ConfigEntry[AirVisualDataUpdateCoordinator]
class AirVisualDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to manage fetching AirVisual data."""
config_entry: AirVisualConfigEntry
def __init__(
self,
hass: HomeAssistant,
entry: AirVisualConfigEntry,
cloud_api: CloudAPI,
name: str,
) -> None:
"""Initialize the coordinator."""
self._cloud_api = cloud_api
super().__init__(
hass,
LOGGER,
config_entry=entry,
name=name,
# We give a placeholder update interval in order to create the coordinator;
# then, in async_setup_entry, we use the coordinator's presence (along with
# any other coordinators using the same API key) to calculate an actual,
# leveled update interval:
update_interval=timedelta(minutes=5),
)
async def _async_update_data(self) -> dict[str, Any]:
"""Get new data from the API."""
if CONF_CITY in self.config_entry.data:
api_coro = self._cloud_api.air_quality.city(
self.config_entry.data[CONF_CITY],
self.config_entry.data[CONF_STATE],
self.config_entry.data[CONF_COUNTRY],
)
else:
api_coro = self._cloud_api.air_quality.nearest_city(
self.config_entry.data[CONF_LATITUDE],
self.config_entry.data[CONF_LONGITUDE],
)
try:
return await api_coro
except (InvalidKeyError, KeyExpiredError, UnauthorizedError) as ex:
raise ConfigEntryAuthFailed from ex
except AirVisualError as err:
raise UpdateFailed(f"Error while retrieving data: {err}") from err
@@ -15,8 +15,8 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from . import AirVisualConfigEntry
from .const import CONF_CITY
from .coordinator import AirVisualConfigEntry
CONF_COORDINATES = "coordinates"
CONF_TITLE = "title"
+5 -9
View File
@@ -2,29 +2,25 @@
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import AirVisualDataUpdateCoordinator
class AirVisualEntity(CoordinatorEntity):
class AirVisualEntity(CoordinatorEntity[AirVisualDataUpdateCoordinator]):
"""Define a generic AirVisual entity."""
def __init__(
self,
coordinator: DataUpdateCoordinator,
entry: ConfigEntry,
coordinator: AirVisualDataUpdateCoordinator,
description: EntityDescription,
) -> None:
"""Initialize."""
super().__init__(coordinator)
self._attr_extra_state_attributes = {}
self._entry = entry
self.entity_description = description
async def async_added_to_hass(self) -> None:
+8 -10
View File
@@ -8,7 +8,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_LATITUDE,
ATTR_LONGITUDE,
@@ -24,10 +23,9 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import AirVisualConfigEntry
from .const import CONF_CITY
from .coordinator import AirVisualConfigEntry, AirVisualDataUpdateCoordinator
from .entity import AirVisualEntity
ATTR_CITY = "city"
@@ -113,7 +111,7 @@ async def async_setup_entry(
"""Set up AirVisual sensors based on a config entry."""
coordinator = entry.runtime_data
async_add_entities(
AirVisualGeographySensor(coordinator, entry, description, locale)
AirVisualGeographySensor(coordinator, description, locale)
for locale in GEOGRAPHY_SENSOR_LOCALES
for description in GEOGRAPHY_SENSOR_DESCRIPTIONS
)
@@ -124,14 +122,14 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
def __init__(
self,
coordinator: DataUpdateCoordinator,
entry: ConfigEntry,
coordinator: AirVisualDataUpdateCoordinator,
description: SensorEntityDescription,
locale: str,
) -> None:
"""Initialize."""
super().__init__(coordinator, entry, description)
super().__init__(coordinator, description)
entry = coordinator.config_entry
self._attr_extra_state_attributes.update(
{
ATTR_CITY: entry.data.get(CONF_CITY),
@@ -182,16 +180,16 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
#
# We use any coordinates in the config entry and, in the case of a geography by
# name, we fall back to the latitude longitude provided in the coordinator data:
latitude = self._entry.data.get(
latitude = self.coordinator.config_entry.data.get(
CONF_LATITUDE,
self.coordinator.data["location"]["coordinates"][1],
)
longitude = self._entry.data.get(
longitude = self.coordinator.config_entry.data.get(
CONF_LONGITUDE,
self.coordinator.data["location"]["coordinates"][0],
)
if self._entry.options[CONF_SHOW_ON_MAP]:
if self.coordinator.config_entry.options[CONF_SHOW_ON_MAP]:
self._attr_extra_state_attributes[ATTR_LATITUDE] = latitude
self._attr_extra_state_attributes[ATTR_LONGITUDE] = longitude
self._attr_extra_state_attributes.pop("lati", None)
@@ -4,18 +4,9 @@ from __future__ import annotations
import asyncio
from contextlib import suppress
from dataclasses import dataclass
from datetime import timedelta
from typing import Any
from pyairvisual.node import (
InvalidAuthenticationError,
NodeConnectionError,
NodeProError,
NodeSamba,
)
from pyairvisual.node import NodeProError, NodeSamba
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_IP_ADDRESS,
CONF_PASSWORD,
@@ -23,25 +14,16 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.exceptions import ConfigEntryNotReady
from .const import LOGGER
from .coordinator import (
AirVisualProConfigEntry,
AirVisualProCoordinator,
AirVisualProData,
)
PLATFORMS = [Platform.SENSOR]
UPDATE_INTERVAL = timedelta(minutes=1)
type AirVisualProConfigEntry = ConfigEntry[AirVisualProData]
@dataclass
class AirVisualProData:
"""Define a data class."""
coordinator: DataUpdateCoordinator
node: NodeSamba
async def async_setup_entry(
hass: HomeAssistant, entry: AirVisualProConfigEntry
@@ -54,48 +36,15 @@ async def async_setup_entry(
except NodeProError as err:
raise ConfigEntryNotReady from err
reload_task: asyncio.Task | None = None
async def async_get_data() -> dict[str, Any]:
"""Get data from the device."""
try:
data = await node.async_get_latest_measurements()
data["history"] = {}
if data["settings"].get("follow_mode") == "device":
history = await node.async_get_history(include_trends=False)
data["history"] = history.get("measurements", [])[-1]
except InvalidAuthenticationError as err:
raise ConfigEntryAuthFailed("Invalid Samba password") from err
except NodeConnectionError as err:
nonlocal reload_task
if not reload_task:
reload_task = hass.async_create_task(
hass.config_entries.async_reload(entry.entry_id)
)
raise UpdateFailed(f"Connection to Pro unit lost: {err}") from err
except NodeProError as err:
raise UpdateFailed(f"Error while retrieving data: {err}") from err
return data
coordinator = DataUpdateCoordinator(
hass,
LOGGER,
config_entry=entry,
name="Node/Pro data",
update_interval=UPDATE_INTERVAL,
update_method=async_get_data,
)
coordinator = AirVisualProCoordinator(hass, entry, node)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = AirVisualProData(coordinator=coordinator, node=node)
async def async_shutdown(_: Event) -> None:
"""Define an event handler to disconnect from the websocket."""
nonlocal reload_task
if reload_task:
if coordinator.reload_task:
with suppress(asyncio.CancelledError):
reload_task.cancel()
coordinator.reload_task.cancel()
await node.async_disconnect()
entry.async_on_unload(
@@ -0,0 +1,79 @@
"""DataUpdateCoordinator for the AirVisual Pro integration."""
from __future__ import annotations
import asyncio
from dataclasses import dataclass
from datetime import timedelta
from typing import Any
from pyairvisual.node import (
InvalidAuthenticationError,
NodeConnectionError,
NodeProError,
NodeSamba,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import LOGGER
UPDATE_INTERVAL = timedelta(minutes=1)
@dataclass
class AirVisualProData:
"""Define a data class."""
coordinator: AirVisualProCoordinator
node: NodeSamba
type AirVisualProConfigEntry = ConfigEntry[AirVisualProData]
class AirVisualProCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Coordinator for AirVisual Pro data."""
config_entry: AirVisualProConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: AirVisualProConfigEntry,
node: NodeSamba,
) -> None:
"""Initialize."""
super().__init__(
hass,
LOGGER,
config_entry=config_entry,
name="Node/Pro data",
update_interval=UPDATE_INTERVAL,
)
self._node = node
self.reload_task: asyncio.Task[bool] | None = None
async def _async_update_data(self) -> dict[str, Any]:
"""Get data from the device."""
try:
data = await self._node.async_get_latest_measurements()
data["history"] = {}
if data["settings"].get("follow_mode") == "device":
history = await self._node.async_get_history(include_trends=False)
data["history"] = history.get("measurements", [])[-1]
except InvalidAuthenticationError as err:
raise ConfigEntryAuthFailed("Invalid Samba password") from err
except NodeConnectionError as err:
if self.reload_task is None:
self.reload_task = self.hass.async_create_task(
self.hass.config_entries.async_reload(self.config_entry.entry_id)
)
raise UpdateFailed(f"Connection to Pro unit lost: {err}") from err
except NodeProError as err:
raise UpdateFailed(f"Error while retrieving data: {err}") from err
return data
@@ -8,7 +8,7 @@ from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_PASSWORD
from homeassistant.core import HomeAssistant
from . import AirVisualProConfigEntry
from .coordinator import AirVisualProConfigEntry
CONF_MAC_ADDRESS = "mac_address"
CONF_SERIAL_NUMBER = "serial_number"
@@ -4,19 +4,17 @@ from __future__ import annotations
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import AirVisualProCoordinator
class AirVisualProEntity(CoordinatorEntity):
class AirVisualProEntity(CoordinatorEntity[AirVisualProCoordinator]):
"""Define a generic AirVisual Pro entity."""
def __init__(
self, coordinator: DataUpdateCoordinator, description: EntityDescription
self, coordinator: AirVisualProCoordinator, description: EntityDescription
) -> None:
"""Initialize."""
super().__init__(coordinator)
@@ -22,7 +22,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AirVisualProConfigEntry
from .coordinator import AirVisualProConfigEntry
from .entity import AirVisualProEntity
@@ -46,19 +46,10 @@ async def async_setup_entry(
api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session)
)
try:
doors = await client.get_doors()
except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed(err) from err
raise ConfigEntryNotReady from err
except aiohttp.ClientError as err:
raise ConfigEntryNotReady from err
coordinator = AladdinConnectCoordinator(hass, entry, client)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = {
door.unique_id: AladdinConnectCoordinator(hass, entry, client, door)
for door in doors
}
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -100,7 +91,7 @@ def remove_stale_devices(
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry.entry_id
)
all_device_ids = set(config_entry.runtime_data)
all_device_ids = set(config_entry.runtime_data.data)
for device_entry in device_entries:
device_id: str | None = None
@@ -11,22 +11,24 @@ from genie_partner_sdk.model import GarageDoor
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
_LOGGER = logging.getLogger(__name__)
type AladdinConnectConfigEntry = ConfigEntry[dict[str, AladdinConnectCoordinator]]
type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator]
SCAN_INTERVAL = timedelta(seconds=15)
class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
class AladdinConnectCoordinator(DataUpdateCoordinator[dict[str, GarageDoor]]):
"""Coordinator for Aladdin Connect integration."""
config_entry: AladdinConnectConfigEntry
def __init__(
self,
hass: HomeAssistant,
entry: AladdinConnectConfigEntry,
client: AladdinConnectClient,
garage_door: GarageDoor,
) -> None:
"""Initialize the coordinator."""
super().__init__(
@@ -37,18 +39,16 @@ class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
update_interval=SCAN_INTERVAL,
)
self.client = client
self.data = garage_door
async def _async_update_data(self) -> GarageDoor:
async def _async_update_data(self) -> dict[str, GarageDoor]:
"""Fetch data from the Aladdin Connect API."""
try:
await self.client.update_door(self.data.device_id, self.data.door_number)
doors = await self.client.get_doors()
except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed(err) from err
raise UpdateFailed(f"Error communicating with API: {err}") from err
except aiohttp.ClientError as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
self.data.status = self.client.get_door_status(
self.data.device_id, self.data.door_number
)
self.data.battery_level = self.client.get_battery_status(
self.data.device_id, self.data.door_number
)
return self.data
return {door.unique_id: door for door in doors}
@@ -7,7 +7,7 @@ from typing import Any
import aiohttp
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -24,11 +24,22 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the cover platform."""
coordinators = entry.runtime_data
coordinator = entry.runtime_data
known_devices: set[str] = set()
async_add_entities(
AladdinCoverEntity(coordinator) for coordinator in coordinators.values()
)
@callback
def _async_add_new_devices() -> None:
"""Detect and add entities for new doors."""
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AladdinCoverEntity(coordinator, door_id) for door_id in new_devices
)
_async_add_new_devices()
entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices))
class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
@@ -38,10 +49,10 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
_attr_supported_features = SUPPORTED_FEATURES
_attr_name = None
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
def __init__(self, coordinator: AladdinConnectCoordinator, door_id: str) -> None:
"""Initialize the Aladdin Connect cover."""
super().__init__(coordinator)
self._attr_unique_id = coordinator.data.unique_id
super().__init__(coordinator, door_id)
self._attr_unique_id = door_id
async def async_open_cover(self, **kwargs: Any) -> None:
"""Issue open command to cover."""
@@ -66,16 +77,16 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
@property
def is_closed(self) -> bool | None:
"""Update is closed attribute."""
if (status := self.coordinator.data.status) is None:
if (status := self.door.status) is None:
return None
return status == "closed"
@property
def is_closing(self) -> bool | None:
"""Update is closing attribute."""
return self.coordinator.data.status == "closing"
return self.door.status == "closing"
@property
def is_opening(self) -> bool | None:
"""Update is opening attribute."""
return self.coordinator.data.status == "opening"
return self.door.status == "opening"
@@ -20,13 +20,13 @@ async def async_get_config_entry_diagnostics(
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),
"doors": {
uid: {
"device_id": coordinator.data.device_id,
"door_number": coordinator.data.door_number,
"name": coordinator.data.name,
"status": coordinator.data.status,
"link_status": coordinator.data.link_status,
"battery_level": coordinator.data.battery_level,
"device_id": door.device_id,
"door_number": door.door_number,
"name": door.name,
"status": door.status,
"link_status": door.link_status,
"battery_level": door.battery_level,
}
for uid, coordinator in config_entry.runtime_data.items()
for uid, door in config_entry.runtime_data.data.items()
},
}
@@ -1,6 +1,7 @@
"""Base class for Aladdin Connect entities."""
from genie_partner_sdk.client import AladdinConnectClient
from genie_partner_sdk.model import GarageDoor
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -14,17 +15,28 @@ class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]):
_attr_has_entity_name = True
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
def __init__(self, coordinator: AladdinConnectCoordinator, door_id: str) -> None:
"""Initialize Aladdin Connect entity."""
super().__init__(coordinator)
device = coordinator.data
self._door_id = door_id
door = self.door
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.unique_id)},
identifiers={(DOMAIN, door.unique_id)},
manufacturer="Aladdin Connect",
name=device.name,
name=door.name,
)
self._device_id = device.device_id
self._number = device.door_number
self._device_id = door.device_id
self._number = door.door_number
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available and self._door_id in self.coordinator.data
@property
def door(self) -> GarageDoor:
"""Return the garage door data."""
return self.coordinator.data[self._door_id]
@property
def client(self) -> AladdinConnectClient:
@@ -57,7 +57,7 @@ rules:
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: todo
dynamic-devices: done
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
@@ -66,9 +66,7 @@ rules:
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices:
status: todo
comment: We can automatically remove removed devices
stale-devices: done
# Platinum
async-dependency: todo
@@ -14,7 +14,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
@@ -49,13 +49,24 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Aladdin Connect sensor devices."""
coordinators = entry.runtime_data
coordinator = entry.runtime_data
known_devices: set[str] = set()
async_add_entities(
AladdinConnectSensor(coordinator, description)
for coordinator in coordinators.values()
for description in SENSOR_TYPES
)
@callback
def _async_add_new_devices() -> None:
"""Detect and add entities for new doors."""
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AladdinConnectSensor(coordinator, door_id, description)
for door_id in new_devices
for description in SENSOR_TYPES
)
_async_add_new_devices()
entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices))
class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
@@ -66,14 +77,15 @@ class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
def __init__(
self,
coordinator: AladdinConnectCoordinator,
door_id: str,
entity_description: AladdinConnectSensorEntityDescription,
) -> None:
"""Initialize the Aladdin Connect sensor."""
super().__init__(coordinator)
super().__init__(coordinator, door_id)
self.entity_description = entity_description
self._attr_unique_id = f"{coordinator.data.unique_id}-{entity_description.key}"
self._attr_unique_id = f"{door_id}-{entity_description.key}"
@property
def native_value(self) -> float | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data)
return self.entity_description.value_fn(self.door)
@@ -2,6 +2,7 @@
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
Condition,
EntityStateConditionBase,
@@ -43,7 +44,7 @@ def make_entity_state_required_features_condition(
class CustomCondition(EntityStateRequiredFeaturesCondition):
"""Condition for entity state changes."""
_domain = domain
_domain_specs = {domain: DomainSpec()}
_states = {to_state}
_required_features = required_features
@@ -2,6 +2,7 @@
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.entity import get_supported_features
from homeassistant.helpers.trigger import (
EntityTargetStateTriggerBase,
@@ -44,7 +45,7 @@ def make_entity_state_trigger_required_features(
class CustomTrigger(EntityStateTriggerRequiredFeatures):
"""Trigger for entity state changes."""
_domains = {domain}
_domain_specs = {domain: DomainSpec()}
_to_states = {to_state}
_required_features = required_features
@@ -1,6 +1,5 @@
"""Defines a base Alexa Devices entity."""
from aioamazondevices.const.devices import SPEAKER_GROUP_DEVICE_TYPE
from aioamazondevices.structures import AmazonDevice
from homeassistant.helpers.device_registry import DeviceInfo
@@ -25,20 +24,15 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
"""Initialize the entity."""
super().__init__(coordinator)
self._serial_num = serial_num
model = self.device.model
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_num)},
name=self.device.account_name,
model=model,
model=self.device.model,
model_id=self.device.device_type,
manufacturer=self.device.manufacturer or "Amazon",
hw_version=self.device.hardware_version,
sw_version=(
self.device.software_version
if model != SPEAKER_GROUP_DEVICE_TYPE
else None
),
serial_number=serial_num if model != SPEAKER_GROUP_DEVICE_TYPE else None,
sw_version=self.device.software_version,
serial_number=serial_num,
)
self.entity_description = description
self._attr_unique_id = f"{serial_num}-{description.key}"
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==13.0.0"]
"requirements": ["aioamazondevices==13.0.1"]
}
@@ -101,7 +101,10 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
assert method is not None
await method(self.device, state)
await self.coordinator.async_request_refresh()
self.coordinator.data[self.device.serial_number].sensors[
self.entity_description.key
].value = state
self.async_write_ha_state()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
@@ -338,6 +338,7 @@ class Analytics:
hass = self._hass
supervisor_info = None
addons_info: dict[str, Any] | None = None
operating_system_info: dict[str, Any] = {}
if self._data.uuid is None:
@@ -347,6 +348,7 @@ class Analytics:
if self.supervisor:
supervisor_info = hassio.get_supervisor_info(hass)
operating_system_info = hassio.get_os_info(hass) or {}
addons_info = hassio.get_addons_info(hass) or {}
system_info = await async_get_system_info(hass)
integrations = []
@@ -419,13 +421,10 @@ class Analytics:
integrations.append(integration.domain)
if supervisor_info is not None:
if addons_info is not None:
supervisor_client = hassio.get_supervisor_client(hass)
installed_addons = await asyncio.gather(
*(
supervisor_client.addons.addon_info(addon[ATTR_SLUG])
for addon in supervisor_info[ATTR_ADDONS]
)
*(supervisor_client.addons.addon_info(slug) for slug in addons_info)
)
addons.extend(
{
@@ -27,4 +27,4 @@ def create_api(hass: HomeAssistant, host: str, enable_ime: bool) -> AndroidTVRem
def get_enable_ime(entry: AndroidTVRemoteConfigEntry) -> bool:
"""Get value of enable_ime option or its default value."""
return entry.options.get(CONF_ENABLE_IME, CONF_ENABLE_IME_DEFAULT_VALUE) # type: ignore[no-any-return]
return bool(entry.options.get(CONF_ENABLE_IME, CONF_ENABLE_IME_DEFAULT_VALUE))
@@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["pyanglianwater"],
"quality_scale": "bronze",
"requirements": ["pyanglianwater==3.1.0"]
"requirements": ["pyanglianwater==3.1.1"]
}
@@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from enum import StrEnum
from pyanglianwater.meter import SmartMeter
@@ -32,13 +33,14 @@ class AnglianWaterSensor(StrEnum):
YESTERDAY_WATER_COST = "yesterday_water_cost"
YESTERDAY_SEWERAGE_COST = "yesterday_sewerage_cost"
LATEST_READING = "latest_reading"
LAST_UPDATED = "last_updated"
@dataclass(frozen=True, kw_only=True)
class AnglianWaterSensorEntityDescription(SensorEntityDescription):
"""Describes AnglianWater sensor entity."""
value_fn: Callable[[SmartMeter], float]
value_fn: Callable[[SmartMeter], float | datetime | None]
ENTITY_DESCRIPTIONS: tuple[AnglianWaterSensorEntityDescription, ...] = (
@@ -76,6 +78,13 @@ ENTITY_DESCRIPTIONS: tuple[AnglianWaterSensorEntityDescription, ...] = (
translation_key=AnglianWaterSensor.YESTERDAY_SEWERAGE_COST,
entity_category=EntityCategory.DIAGNOSTIC,
),
AnglianWaterSensorEntityDescription(
key=AnglianWaterSensor.LAST_UPDATED,
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda entity: entity.last_updated,
translation_key=AnglianWaterSensor.LAST_UPDATED,
entity_category=EntityCategory.DIAGNOSTIC,
),
)
@@ -112,6 +121,6 @@ class AnglianWaterSensorEntity(AnglianWaterEntity, SensorEntity):
self.entity_description = description
@property
def native_value(self) -> float | None:
def native_value(self) -> float | datetime | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.smart_meter)
@@ -34,6 +34,9 @@
},
"entity": {
"sensor": {
"last_updated": {
"name": "Last meter reading processed"
},
"latest_reading": {
"name": "Latest reading"
},
+32 -18
View File
@@ -8,46 +8,55 @@ from typing import Any
from arcam.fmj import ConnectionFailed
from arcam.fmj.client import Client
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import (
DEFAULT_SCAN_INTERVAL,
SIGNAL_CLIENT_DATA,
SIGNAL_CLIENT_STARTED,
SIGNAL_CLIENT_STOPPED,
)
type ArcamFmjConfigEntry = ConfigEntry[Client]
from .const import DEFAULT_SCAN_INTERVAL
from .coordinator import ArcamFmjConfigEntry, ArcamFmjCoordinator, ArcamFmjRuntimeData
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.MEDIA_PLAYER]
PLATFORMS = [Platform.BINARY_SENSOR, Platform.MEDIA_PLAYER, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ArcamFmjConfigEntry) -> bool:
"""Set up config entry."""
entry.runtime_data = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
client = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
coordinators: dict[int, ArcamFmjCoordinator] = {}
for zone in (1, 2):
coordinator = ArcamFmjCoordinator(hass, entry, client, zone)
coordinators[zone] = coordinator
entry.runtime_data = ArcamFmjRuntimeData(client, coordinators)
entry.async_create_background_task(
hass, _run_client(hass, entry.runtime_data, DEFAULT_SCAN_INTERVAL), "arcam_fmj"
hass,
_run_client(hass, entry.runtime_data, DEFAULT_SCAN_INTERVAL),
"arcam_fmj",
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ArcamFmjConfigEntry) -> bool:
"""Cleanup before removing config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def _run_client(hass: HomeAssistant, client: Client, interval: float) -> None:
async def _run_client(
hass: HomeAssistant,
runtime_data: ArcamFmjRuntimeData,
interval: float,
) -> None:
client = runtime_data.client
coordinators = runtime_data.coordinators
def _listen(_: Any) -> None:
async_dispatcher_send(hass, SIGNAL_CLIENT_DATA, client.host)
for coordinator in coordinators.values():
coordinator.async_notify_data_updated()
while True:
try:
@@ -55,16 +64,21 @@ async def _run_client(hass: HomeAssistant, client: Client, interval: float) -> N
await client.start()
_LOGGER.debug("Client connected %s", client.host)
async_dispatcher_send(hass, SIGNAL_CLIENT_STARTED, client.host)
try:
for coordinator in coordinators.values():
await coordinator.state.start()
with client.listen(_listen):
for coordinator in coordinators.values():
coordinator.async_notify_connected()
await client.process()
finally:
await client.stop()
_LOGGER.debug("Client disconnected %s", client.host)
async_dispatcher_send(hass, SIGNAL_CLIENT_STOPPED, client.host)
for coordinator in coordinators.values():
coordinator.async_notify_disconnected()
except ConnectionFailed:
await asyncio.sleep(interval)
@@ -0,0 +1,68 @@
"""Arcam binary sensors for incoming stream info."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from arcam.fmj.state import State
from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ArcamFmjConfigEntry
from .entity import ArcamFmjEntity
@dataclass(frozen=True, kw_only=True)
class ArcamFmjBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes an Arcam FMJ binary sensor entity."""
value_fn: Callable[[State], bool | None]
BINARY_SENSORS: tuple[ArcamFmjBinarySensorEntityDescription, ...] = (
ArcamFmjBinarySensorEntityDescription(
key="incoming_video_interlaced",
translation_key="incoming_video_interlaced",
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda state: (
vp.interlaced
if (vp := state.get_incoming_video_parameters()) is not None
else None
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ArcamFmjConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Arcam FMJ binary sensors from a config entry."""
coordinators = config_entry.runtime_data.coordinators
entities: list[ArcamFmjBinarySensorEntity] = []
for coordinator in coordinators.values():
entities.extend(
ArcamFmjBinarySensorEntity(coordinator, description)
for description in BINARY_SENSORS
)
async_add_entities(entities)
class ArcamFmjBinarySensorEntity(ArcamFmjEntity, BinarySensorEntity):
"""Representation of an Arcam FMJ binary sensor."""
entity_description: ArcamFmjBinarySensorEntityDescription
@property
def is_on(self) -> bool | None:
"""Return the binary sensor value."""
return self.entity_description.value_fn(self.coordinator.state)
@@ -2,10 +2,6 @@
DOMAIN = "arcam_fmj"
SIGNAL_CLIENT_STARTED = "arcam.client_started"
SIGNAL_CLIENT_STOPPED = "arcam.client_stopped"
SIGNAL_CLIENT_DATA = "arcam.client_data"
EVENT_TURN_ON = "arcam_fmj.turn_on"
DEFAULT_PORT = 50000
@@ -0,0 +1,97 @@
"""Coordinator for Arcam FMJ integration."""
from __future__ import annotations
from dataclasses import dataclass
import logging
from arcam.fmj import ConnectionFailed
from arcam.fmj.client import Client
from arcam.fmj.state import State
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@dataclass
class ArcamFmjRuntimeData:
"""Runtime data for Arcam FMJ integration."""
client: Client
coordinators: dict[int, ArcamFmjCoordinator]
type ArcamFmjConfigEntry = ConfigEntry[ArcamFmjRuntimeData]
class ArcamFmjCoordinator(DataUpdateCoordinator[None]):
"""Coordinator for a single Arcam FMJ zone."""
config_entry: ArcamFmjConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: ArcamFmjConfigEntry,
client: Client,
zone: int,
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=f"Arcam FMJ zone {zone}",
)
self.client = client
self.state = State(client, zone)
self.last_update_success = False
name = config_entry.title
unique_id = config_entry.unique_id or config_entry.entry_id
unique_id_device = unique_id
if zone != 1:
unique_id_device += f"-{zone}"
name += f" Zone {zone}"
self.device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id_device)},
manufacturer="Arcam",
model="Arcam FMJ AVR",
name=name,
)
self.zone_unique_id = f"{unique_id}-{zone}"
if zone != 1:
self.device_info["via_device"] = (DOMAIN, unique_id)
async def _async_update_data(self) -> None:
"""Fetch data for manual refresh."""
try:
await self.state.update()
except ConnectionFailed as err:
raise UpdateFailed(
f"Connection failed during update for zone {self.state.zn}"
) from err
@callback
def async_notify_data_updated(self) -> None:
"""Notify that new data has been received from the device."""
self.async_set_updated_data(None)
@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()
@@ -0,0 +1,28 @@
"""Base entity for Arcam FMJ integration."""
from __future__ import annotations
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import ArcamFmjCoordinator
class ArcamFmjEntity(CoordinatorEntity[ArcamFmjCoordinator]):
"""Base entity for Arcam FMJ."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: ArcamFmjCoordinator,
description: EntityDescription | None = None,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._attr_device_info = coordinator.device_info
self._attr_entity_registry_enabled_default = coordinator.state.zn == 1
self._attr_unique_id = coordinator.zone_unique_id
if description is not None:
self._attr_unique_id = f"{self._attr_unique_id}-{description.key}"
self.entity_description = description
@@ -0,0 +1,35 @@
{
"entity": {
"binary_sensor": {
"incoming_video_interlaced": {
"default": "mdi:reorder-horizontal"
}
},
"sensor": {
"incoming_audio_config": {
"default": "mdi:surround-sound"
},
"incoming_audio_format": {
"default": "mdi:dolby"
},
"incoming_audio_sample_rate": {
"default": "mdi:waveform"
},
"incoming_video_aspect_ratio": {
"default": "mdi:aspect-ratio"
},
"incoming_video_colorspace": {
"default": "mdi:palette"
},
"incoming_video_horizontal_resolution": {
"default": "mdi:arrow-expand-horizontal"
},
"incoming_video_refresh_rate": {
"default": "mdi:animation"
},
"incoming_video_vertical_resolution": {
"default": "mdi:arrow-expand-vertical"
}
}
}
}
@@ -8,7 +8,6 @@ import logging
from typing import Any
from arcam.fmj import ConnectionFailed, SourceCodes
from arcam.fmj.state import State
from homeassistant.components.media_player import (
BrowseError,
@@ -20,20 +19,13 @@ from homeassistant.components.media_player import (
MediaType,
)
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import ArcamFmjConfigEntry
from .const import (
DOMAIN,
EVENT_TURN_ON,
SIGNAL_CLIENT_DATA,
SIGNAL_CLIENT_STARTED,
SIGNAL_CLIENT_STOPPED,
)
from .const import EVENT_TURN_ON
from .coordinator import ArcamFmjConfigEntry, ArcamFmjCoordinator
from .entity import ArcamFmjEntity
_LOGGER = logging.getLogger(__name__)
@@ -44,19 +36,10 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the configuration entry."""
client = config_entry.runtime_data
coordinators = config_entry.runtime_data.coordinators
async_add_entities(
[
ArcamFmj(
config_entry.title,
State(client, zone),
config_entry.unique_id or config_entry.entry_id,
)
for zone in (1, 2)
],
True,
[ArcamFmj(coordinators[zone]) for zone in (1, 2)],
)
@@ -77,21 +60,13 @@ def convert_exception[**_P, _R](
return _convert_exception
class ArcamFmj(MediaPlayerEntity):
class ArcamFmj(ArcamFmjEntity, MediaPlayerEntity):
"""Representation of a media device."""
_attr_should_poll = False
_attr_has_entity_name = True
def __init__(
self,
device_name: str,
state: State,
uuid: str,
) -> None:
def __init__(self, coordinator: ArcamFmjCoordinator) -> None:
"""Initialize device."""
self._state = state
self._attr_name = f"Zone {state.zn}"
super().__init__(coordinator)
self._state = coordinator.state
self._attr_supported_features = (
MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.PLAY_MEDIA
@@ -102,18 +77,8 @@ class ArcamFmj(MediaPlayerEntity):
| MediaPlayerEntityFeature.TURN_OFF
| MediaPlayerEntityFeature.TURN_ON
)
if state.zn == 1:
if self._state.zn == 1:
self._attr_supported_features |= MediaPlayerEntityFeature.SELECT_SOUND_MODE
self._attr_unique_id = f"{uuid}-{state.zn}"
self._attr_entity_registry_enabled_default = state.zn == 1
self._attr_device_info = DeviceInfo(
identifiers={
(DOMAIN, uuid),
},
manufacturer="Arcam",
model="Arcam FMJ AVR",
name=device_name,
)
@property
def state(self) -> MediaPlayerState:
@@ -122,49 +87,6 @@ class ArcamFmj(MediaPlayerEntity):
return MediaPlayerState.ON
return MediaPlayerState.OFF
async def async_added_to_hass(self) -> None:
"""Once registered, add listener for events."""
await self._state.start()
try:
await self._state.update()
except ConnectionFailed as connection:
_LOGGER.debug("Connection lost during addition: %s", connection)
@callback
def _data(host: str) -> None:
if host == self._state.client.host:
self.async_write_ha_state()
@callback
def _started(host: str) -> None:
if host == self._state.client.host:
self.async_schedule_update_ha_state(force_refresh=True)
@callback
def _stopped(host: str) -> None:
if host == self._state.client.host:
self.async_schedule_update_ha_state(force_refresh=True)
self.async_on_remove(
async_dispatcher_connect(self.hass, SIGNAL_CLIENT_DATA, _data)
)
self.async_on_remove(
async_dispatcher_connect(self.hass, SIGNAL_CLIENT_STARTED, _started)
)
self.async_on_remove(
async_dispatcher_connect(self.hass, SIGNAL_CLIENT_STOPPED, _stopped)
)
async def async_update(self) -> None:
"""Force update of state."""
_LOGGER.debug("Update state %s", self.name)
try:
await self._state.update()
except ConnectionFailed as connection:
_LOGGER.debug("Connection lost during update: %s", connection)
@convert_exception
async def async_mute_volume(self, mute: bool) -> None:
"""Send mute command."""
@@ -0,0 +1,162 @@
"""Arcam sensors for incoming stream info."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from arcam.fmj import IncomingVideoAspectRatio, IncomingVideoColorspace
from arcam.fmj.state import IncomingAudioConfig, IncomingAudioFormat, State
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import EntityCategory, UnitOfFrequency
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ArcamFmjConfigEntry
from .entity import ArcamFmjEntity
@dataclass(frozen=True, kw_only=True)
class ArcamFmjSensorEntityDescription(SensorEntityDescription):
"""Describes an Arcam FMJ sensor entity."""
value_fn: Callable[[State], int | float | str | None]
SENSORS: tuple[ArcamFmjSensorEntityDescription, ...] = (
ArcamFmjSensorEntityDescription(
key="incoming_video_horizontal_resolution",
translation_key="incoming_video_horizontal_resolution",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement="px",
suggested_display_precision=0,
value_fn=lambda state: (
vp.horizontal_resolution
if (vp := state.get_incoming_video_parameters()) is not None
else None
),
),
ArcamFmjSensorEntityDescription(
key="incoming_video_vertical_resolution",
translation_key="incoming_video_vertical_resolution",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement="px",
suggested_display_precision=0,
value_fn=lambda state: (
vp.vertical_resolution
if (vp := state.get_incoming_video_parameters()) is not None
else None
),
),
ArcamFmjSensorEntityDescription(
key="incoming_video_refresh_rate",
translation_key="incoming_video_refresh_rate",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.FREQUENCY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfFrequency.HERTZ,
suggested_display_precision=0,
value_fn=lambda state: (
vp.refresh_rate
if (vp := state.get_incoming_video_parameters()) is not None
else None
),
),
ArcamFmjSensorEntityDescription(
key="incoming_video_aspect_ratio",
translation_key="incoming_video_aspect_ratio",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.ENUM,
options=[member.name.lower() for member in IncomingVideoAspectRatio],
value_fn=lambda state: (
vp.aspect_ratio.name.lower()
if (vp := state.get_incoming_video_parameters()) is not None
else None
),
),
ArcamFmjSensorEntityDescription(
key="incoming_video_colorspace",
translation_key="incoming_video_colorspace",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.ENUM,
options=[member.name.lower() for member in IncomingVideoColorspace],
value_fn=lambda state: (
vp.colorspace.name.lower()
if (vp := state.get_incoming_video_parameters()) is not None
else None
),
),
ArcamFmjSensorEntityDescription(
key="incoming_audio_format",
translation_key="incoming_audio_format",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.ENUM,
options=[member.name.lower() for member in IncomingAudioFormat],
value_fn=lambda state: (
result.name.lower()
if (result := state.get_incoming_audio_format()[0]) is not None
else None
),
),
ArcamFmjSensorEntityDescription(
key="incoming_audio_config",
translation_key="incoming_audio_config",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.ENUM,
options=[member.name.lower() for member in IncomingAudioConfig],
value_fn=lambda state: (
result.name.lower()
if (result := state.get_incoming_audio_format()[1]) is not None
else None
),
),
ArcamFmjSensorEntityDescription(
key="incoming_audio_sample_rate",
translation_key="incoming_audio_sample_rate",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.FREQUENCY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfFrequency.HERTZ,
suggested_display_precision=0,
value_fn=lambda state: (
None
if (sample_rate := state.get_incoming_audio_sample_rate()) == 0
else sample_rate
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ArcamFmjConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Arcam FMJ sensors from a config entry."""
coordinators = config_entry.runtime_data.coordinators
entities: list[ArcamFmjSensorEntity] = []
for coordinator in coordinators.values():
entities.extend(
ArcamFmjSensorEntity(coordinator, description) for description in SENSORS
)
async_add_entities(entities)
class ArcamFmjSensorEntity(ArcamFmjEntity, SensorEntity):
"""Representation of an Arcam FMJ sensor."""
entity_description: ArcamFmjSensorEntityDescription
@property
def native_value(self) -> int | float | str | None:
"""Return the sensor value."""
return self.entity_description.value_fn(self.coordinator.state)
@@ -23,5 +23,121 @@
"trigger_type": {
"turn_on": "{entity_name} was requested to turn on"
}
},
"entity": {
"binary_sensor": {
"incoming_video_interlaced": {
"name": "Incoming video interlaced"
}
},
"sensor": {
"incoming_audio_config": {
"name": "Incoming audio configuration",
"state": {
"auro_10_1": "Auro 10.1",
"auro_11_1": "Auro 11.1",
"auro_13_1": "Auro 13.1",
"auro_2_2_2": "Auro 2.2.2",
"auro_5_0": "Auro 5.0",
"auro_5_1": "Auro 5.1",
"auro_8_0": "Auro 8.0",
"auro_9_1": "Auro 9.1",
"auro_quad": "Auro quad",
"dual_mono": "Dual mono",
"dual_mono_lfe": "Dual mono + LFE",
"mono": "Mono",
"mono_lfe": "Mono + LFE",
"stereo_center": "Stereo center",
"stereo_center_lfe": "Stereo center + LFE",
"stereo_center_surr_lr": "Stereo center surround L/R",
"stereo_center_surr_lr_back_lr": "Stereo center surround L/R back L/R",
"stereo_center_surr_lr_back_lr_lfe": "Stereo center surround L/R back L/R + LFE",
"stereo_center_surr_lr_back_matrix": "Stereo center surround L/R back matrix",
"stereo_center_surr_lr_back_matrix_lfe": "Stereo center surround L/R back matrix + LFE",
"stereo_center_surr_lr_back_mono": "Stereo center surround L/R back mono",
"stereo_center_surr_lr_back_mono_lfe": "Stereo center surround L/R back mono + LFE",
"stereo_center_surr_lr_lfe": "Stereo center surround L/R + LFE",
"stereo_center_surr_mono": "Stereo center surround mono",
"stereo_center_surr_mono_lfe": "Stereo center surround mono + LFE",
"stereo_downmix": "Stereo downmix",
"stereo_downmix_lfe": "Stereo downmix + LFE",
"stereo_lfe": "Stereo + LFE",
"stereo_only": "Stereo only",
"stereo_only_lo_ro": "Stereo only Lo/Ro",
"stereo_only_lo_ro_lfe": "Stereo only Lo/Ro + LFE",
"stereo_surr_lr": "Stereo surround L/R",
"stereo_surr_lr_back_lr": "Stereo surround L/R back L/R",
"stereo_surr_lr_back_lr_lfe": "Stereo surround L/R back L/R + LFE",
"stereo_surr_lr_back_matrix": "Stereo surround L/R back matrix",
"stereo_surr_lr_back_matrix_lfe": "Stereo surround L/R back matrix + LFE",
"stereo_surr_lr_back_mono": "Stereo surround L/R back mono",
"stereo_surr_lr_back_mono_lfe": "Stereo surround L/R back mono + LFE",
"stereo_surr_lr_lfe": "Stereo surround L/R + LFE",
"stereo_surr_mono": "Stereo surround mono",
"stereo_surr_mono_lfe": "Stereo surround mono + LFE",
"undetected": "Undetected",
"unknown": "Unknown"
}
},
"incoming_audio_format": {
"name": "Incoming audio format",
"state": {
"analogue_direct": "Analogue direct",
"auro_3d": "Auro-3D",
"dolby_atmos": "Dolby Atmos",
"dolby_digital": "Dolby Digital",
"dolby_digital_ex": "Dolby Digital EX",
"dolby_digital_plus": "Dolby Digital Plus",
"dolby_digital_surround": "Dolby Digital Surround",
"dolby_digital_true_hd": "Dolby TrueHD",
"dts": "DTS",
"dts_96_24": "DTS 96/24",
"dts_core": "DTS Core",
"dts_es_discrete": "DTS-ES Discrete",
"dts_es_discrete_96_24": "DTS-ES Discrete 96/24",
"dts_es_matrix": "DTS-ES Matrix",
"dts_es_matrix_96_24": "DTS-ES Matrix 96/24",
"dts_hd_high_res_audio": "DTS-HD High Resolution Audio",
"dts_hd_master_audio": "DTS-HD Master Audio",
"dts_low_bit_rate": "DTS Low Bit Rate",
"dts_x": "DTS:X",
"imax_enhanced": "IMAX Enhanced",
"pcm": "PCM",
"pcm_zero": "PCM zero",
"undetected": "Undetected",
"unsupported": "Unsupported"
}
},
"incoming_audio_sample_rate": {
"name": "Incoming audio sample rate"
},
"incoming_video_aspect_ratio": {
"name": "Incoming video aspect ratio",
"state": {
"aspect_16_9": "16:9",
"aspect_4_3": "4:3",
"undefined": "Undefined"
}
},
"incoming_video_colorspace": {
"name": "Incoming video colorspace",
"state": {
"dolby_vision": "Dolby Vision",
"hdr10": "HDR10",
"hdr10_plus": "HDR10+",
"hlg": "HLG",
"normal": "Normal"
}
},
"incoming_video_horizontal_resolution": {
"name": "Incoming video horizontal resolution"
},
"incoming_video_refresh_rate": {
"name": "Incoming video refresh rate"
},
"incoming_video_vertical_resolution": {
"name": "Incoming video vertical resolution"
}
}
}
}
@@ -78,19 +78,13 @@ class AssistPipelineSelect(SelectEntity, restore_state.RestoreEntity):
index: int = 0,
) -> None:
"""Initialize a pipeline selector."""
if index < 1:
# Keep compatibility
key_suffix = ""
placeholder = ""
else:
key_suffix = f"_{index + 1}"
placeholder = f" {index + 1}"
self.entity_description = replace(
self.entity_description,
key=f"pipeline{key_suffix}",
translation_placeholders={"index": placeholder},
)
if index >= 1:
self.entity_description = replace(
self.entity_description,
key=f"pipeline_{index + 1}",
translation_key="pipeline_n",
translation_placeholders={"index": str(index + 1)},
)
self._domain = domain
self._unique_id_prefix = unique_id_prefix
@@ -7,11 +7,17 @@
},
"select": {
"pipeline": {
"name": "Assistant{index}",
"name": "Assistant",
"state": {
"preferred": "Preferred"
}
},
"pipeline_n": {
"name": "Assistant {index}",
"state": {
"preferred": "[%key:component::assist_pipeline::entity::select::pipeline::state::preferred%]"
}
},
"vad_sensitivity": {
"name": "Finished speaking detection",
"state": {
+15 -3
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
from pathlib import Path
from typing import cast
from aiohttp import ClientResponseError
from aiohttp import ClientError
from yalexs.exceptions import AugustApiAIOHTTPError
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
from yalexs.manager.gateway import Config as YaleXSConfig
@@ -13,7 +13,12 @@ from yalexs.manager.gateway import Config as YaleXSConfig
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
OAuth2TokenRequestError,
OAuth2TokenRequestReauthError,
)
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
@@ -45,11 +50,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bo
august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session)
try:
await async_setup_august(hass, entry, august_gateway)
except OAuth2TokenRequestReauthError as err:
raise ConfigEntryAuthFailed from err
except (RequireValidation, InvalidAuth) as err:
raise ConfigEntryAuthFailed from err
except TimeoutError as err:
raise ConfigEntryNotReady("Timed out connecting to august api") from err
except (AugustApiAIOHTTPError, ClientResponseError, CannotConnect) as err:
except (
AugustApiAIOHTTPError,
OAuth2TokenRequestError,
ClientError,
CannotConnect,
) as err:
raise ConfigEntryNotReady from err
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -30,5 +30,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"]
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.3.0"]
}
@@ -121,40 +121,58 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
"alarm_control_panel",
"assist_satellite",
"climate",
"cover",
"device_tracker",
"door",
"fan",
"garage_door",
"gate",
"humidifier",
"lawn_mower",
"light",
"lock",
"media_player",
"motion",
"occupancy",
"person",
"schedule",
"siren",
"switch",
"vacuum",
"window",
}
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
"alarm_control_panel",
"assist_satellite",
"binary_sensor",
"button",
"climate",
"cover",
"device_tracker",
"door",
"fan",
"garage_door",
"gate",
"humidifier",
"humidity",
"input_boolean",
"lawn_mower",
"light",
"lock",
"media_player",
"motion",
"occupancy",
"person",
"remote",
"scene",
"schedule",
"select",
"siren",
"switch",
"text",
"update",
"vacuum",
"window",
}
@@ -0,0 +1,53 @@
"""The Autoskope integration."""
from __future__ import annotations
import aiohttp
from autoskope_client.api import AutoskopeApi
from autoskope_client.models import CannotConnect, InvalidAuth
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import DEFAULT_HOST
from .coordinator import AutoskopeConfigEntry, AutoskopeDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.DEVICE_TRACKER]
async def async_setup_entry(hass: HomeAssistant, entry: AutoskopeConfigEntry) -> bool:
"""Set up Autoskope from a config entry."""
session = async_create_clientsession(hass, cookie_jar=aiohttp.CookieJar())
api = AutoskopeApi(
host=entry.data.get(CONF_HOST, DEFAULT_HOST),
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
session=session,
)
try:
await api.connect()
except InvalidAuth as err:
# Raise ConfigEntryError until reauth flow is implemented (then ConfigEntryAuthFailed)
raise ConfigEntryError(
"Authentication failed, please check credentials"
) from err
except CannotConnect as err:
raise ConfigEntryNotReady("Could not connect to Autoskope API") from err
coordinator = AutoskopeDataUpdateCoordinator(hass, api, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: AutoskopeConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -0,0 +1,89 @@
"""Config flow for the Autoskope integration."""
from __future__ import annotations
from typing import Any
from autoskope_client.api import AutoskopeApi
from autoskope_client.models import CannotConnect, InvalidAuth
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import section
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.selector import (
TextSelector,
TextSelectorConfig,
TextSelectorType,
)
from .const import DEFAULT_HOST, DOMAIN, SECTION_ADVANCED_SETTINGS
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
vol.Required(SECTION_ADVANCED_SETTINGS): section(
vol.Schema(
{
vol.Required(CONF_HOST, default=DEFAULT_HOST): TextSelector(
TextSelectorConfig(type=TextSelectorType.URL)
),
}
),
{"collapsed": True},
),
}
)
class AutoskopeConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Autoskope."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
username = user_input[CONF_USERNAME].lower()
host = user_input[SECTION_ADVANCED_SETTINGS][CONF_HOST].lower()
try:
cv.url(host)
except vol.Invalid:
errors["base"] = "invalid_url"
if not errors:
await self.async_set_unique_id(f"{username}@{host}")
self._abort_if_unique_id_configured()
try:
async with AutoskopeApi(
host=host,
username=username,
password=user_input[CONF_PASSWORD],
):
pass
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
else:
return self.async_create_entry(
title=f"Autoskope ({username})",
data={
CONF_USERNAME: username,
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_HOST: host,
},
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
@@ -0,0 +1,9 @@
"""Constants for the Autoskope integration."""
from datetime import timedelta
DOMAIN = "autoskope"
DEFAULT_HOST = "https://portal.autoskope.de"
SECTION_ADVANCED_SETTINGS = "advanced_settings"
UPDATE_INTERVAL = timedelta(seconds=60)
@@ -0,0 +1,60 @@
"""Data update coordinator for the Autoskope integration."""
from __future__ import annotations
import logging
from autoskope_client.api import AutoskopeApi
from autoskope_client.models import CannotConnect, InvalidAuth, Vehicle
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, UPDATE_INTERVAL
_LOGGER = logging.getLogger(__name__)
type AutoskopeConfigEntry = ConfigEntry[AutoskopeDataUpdateCoordinator]
class AutoskopeDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Vehicle]]):
"""Class to manage fetching Autoskope data."""
config_entry: AutoskopeConfigEntry
def __init__(
self, hass: HomeAssistant, api: AutoskopeApi, entry: AutoskopeConfigEntry
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=UPDATE_INTERVAL,
config_entry=entry,
)
self.api = api
async def _async_update_data(self) -> dict[str, Vehicle]:
"""Fetch data from API endpoint."""
try:
vehicles = await self.api.get_vehicles()
return {vehicle.id: vehicle for vehicle in vehicles}
except InvalidAuth:
# Attempt to re-authenticate using stored credentials
try:
await self.api.authenticate()
# Retry the request after successful re-authentication
vehicles = await self.api.get_vehicles()
return {vehicle.id: vehicle for vehicle in vehicles}
except InvalidAuth as reauth_err:
raise ConfigEntryAuthFailed(
f"Authentication failed: {reauth_err}"
) from reauth_err
except CannotConnect as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
@@ -0,0 +1,145 @@
"""Support for Autoskope device tracking."""
from __future__ import annotations
from autoskope_client.constants import MANUFACTURER
from autoskope_client.models import Vehicle
from homeassistant.components.device_tracker import SourceType, TrackerEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import AutoskopeConfigEntry, AutoskopeDataUpdateCoordinator
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
entry: AutoskopeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Autoskope device tracker entities."""
coordinator: AutoskopeDataUpdateCoordinator = entry.runtime_data
tracked_vehicles: set[str] = set()
@callback
def update_entities() -> None:
"""Update entities based on coordinator data."""
current_vehicles = set(coordinator.data.keys())
vehicles_to_add = current_vehicles - tracked_vehicles
if vehicles_to_add:
new_entities = [
AutoskopeDeviceTracker(coordinator, vehicle_id)
for vehicle_id in vehicles_to_add
]
tracked_vehicles.update(vehicles_to_add)
async_add_entities(new_entities)
entry.async_on_unload(coordinator.async_add_listener(update_entities))
update_entities()
class AutoskopeDeviceTracker(
CoordinatorEntity[AutoskopeDataUpdateCoordinator], TrackerEntity
):
"""Representation of an Autoskope tracked device."""
_attr_has_entity_name = True
_attr_name: str | None = None
def __init__(
self, coordinator: AutoskopeDataUpdateCoordinator, vehicle_id: str
) -> None:
"""Initialize the TrackerEntity."""
super().__init__(coordinator)
self._vehicle_id = vehicle_id
self._attr_unique_id = vehicle_id
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
if (
self._vehicle_id in self.coordinator.data
and (device_entry := self.device_entry) is not None
and device_entry.name != self._vehicle_data.name
):
device_registry = dr.async_get(self.hass)
device_registry.async_update_device(
device_entry.id, name=self._vehicle_data.name
)
super()._handle_coordinator_update()
@property
def device_info(self) -> DeviceInfo:
"""Return device info for the vehicle."""
vehicle = self.coordinator.data[self._vehicle_id]
return DeviceInfo(
identifiers={(DOMAIN, str(vehicle.id))},
name=vehicle.name,
manufacturer=MANUFACTURER,
model=vehicle.model,
serial_number=vehicle.imei,
)
@property
def available(self) -> bool:
"""Return if entity is available."""
return (
super().available
and self.coordinator.data is not None
and self._vehicle_id in self.coordinator.data
)
@property
def _vehicle_data(self) -> Vehicle:
"""Return the vehicle data for the current entity."""
return self.coordinator.data[self._vehicle_id]
@property
def latitude(self) -> float | None:
"""Return latitude value of the device."""
if (vehicle := self._vehicle_data) and vehicle.position:
return float(vehicle.position.latitude)
return None
@property
def longitude(self) -> float | None:
"""Return longitude value of the device."""
if (vehicle := self._vehicle_data) and vehicle.position:
return float(vehicle.position.longitude)
return None
@property
def source_type(self) -> SourceType:
"""Return the source type of the device."""
return SourceType.GPS
@property
def location_accuracy(self) -> float:
"""Return the location accuracy of the device in meters."""
if (vehicle := self._vehicle_data) and vehicle.gps_quality:
if vehicle.gps_quality > 0:
# HDOP to estimated accuracy in meters
# HDOP of 1-2 = good (5-10m), 2-5 = moderate (10-25m), >5 = poor (>25m)
return float(max(5, int(vehicle.gps_quality * 5.0)))
return 0.0
@property
def icon(self) -> str:
"""Return the icon based on the vehicle's activity."""
if self._vehicle_id not in self.coordinator.data:
return "mdi:car-clock"
vehicle = self._vehicle_data
if vehicle.position:
if vehicle.position.park_mode:
return "mdi:car-brake-parking"
if vehicle.position.speed > 5: # Moving threshold: 5 km/h
return "mdi:car-arrow-right"
return "mdi:car"
return "mdi:car-clock"
@@ -0,0 +1,11 @@
{
"domain": "autoskope",
"name": "Autoskope",
"codeowners": ["@mcisk"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/autoskope",
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["autoskope_client==1.4.1"]
}
@@ -0,0 +1,88 @@
# + in comment indicates requirement for quality scale
# - in comment indicates issue to be fixed, not impacting quality scale
rules:
# Bronze
action-setup:
status: exempt
comment: |
Integration does not provide custom services.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
Integration does not provide custom services.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: |
Integration does not provide custom services.
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: todo
parallel-updates: done
reauthentication-flow:
status: todo
comment: |
Reauthentication flow removed for initial PR, will be added in follow-up.
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: |
Integration does not use discovery. Autoskope devices use NB-IoT/LTE-M (via IoT SIMs) and LoRaWAN.
discovery:
status: exempt
comment: |
Integration does not use discovery. Autoskope devices use NB-IoT/LTE-M (via IoT SIMs) and LoRaWAN.
docs-data-update: done
docs-examples: done
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: done
entity-category: done
entity-device-class: done
entity-disabled-by-default:
status: exempt
comment: |
Only one entity type (device_tracker) is created, making this not applicable.
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow:
status: todo
comment: |
Reconfiguration flow removed for initial PR, will be added in follow-up.
repair-issues: todo
stale-devices: done
# Platinum
async-dependency: done
inject-websession: done
strict-typing:
status: todo
comment: |
Integration needs to be added to .strict-typing file for full compliance.
@@ -0,0 +1,52 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_url": "Invalid URL",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"password": "The password for your Autoskope account.",
"username": "The username for your Autoskope account."
},
"description": "Enter your Autoskope credentials.",
"sections": {
"advanced_settings": {
"data": {
"host": "API endpoint"
},
"data_description": {
"host": "The URL of your Autoskope API endpoint. Only change this if you use a white-label portal."
},
"name": "Advanced settings"
}
},
"title": "Connect to Autoskope"
}
}
},
"issues": {
"cannot_connect": {
"description": "Home Assistant could not connect to the Autoskope API at {host}. Please check the connection details and ensure the API endpoint is reachable.\n\nError: {error}",
"title": "Failed to connect to Autoskope"
},
"invalid_auth": {
"description": "Authentication with Autoskope failed for user {username}. Please re-authenticate the integration with the correct password.",
"title": "Invalid Autoskope authentication"
},
"low_battery": {
"description": "The battery voltage for vehicle {vehicle_name} ({vehicle_id}) is low ({value}V). Consider checking or replacing the battery.",
"title": "Low vehicle battery ({vehicle_name})"
}
}
}
@@ -14,6 +14,7 @@ from homeassistant.components.backup import (
BackupAgent,
BackupAgentError,
BackupNotFound,
OnProgressCallback,
suggested_filename,
)
from homeassistant.core import HomeAssistant, callback
@@ -132,6 +133,7 @@ class S3BackupAgent(BackupAgent):
*,
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
backup: AgentBackup,
on_progress: OnProgressCallback,
**kwargs: Any,
) -> None:
"""Upload a backup.
+2 -2
View File
@@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from ..const import LOGGER
from ..errors import AuthenticationRequired, CannotConnect
@@ -26,7 +26,7 @@ async def get_axis_api(
config: Mapping[str, Any],
) -> axis.AxisDevice:
"""Create a Axis device API."""
session = get_async_client(hass, verify_ssl=False)
session = async_get_clientsession(hass, verify_ssl=False)
api = axis.AxisDevice(
Configuration(
+1 -1
View File
@@ -29,7 +29,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["axis"],
"requirements": ["axis==66"],
"requirements": ["axis==67"],
"ssdp": [
{
"manufacturer": "AXIS"
@@ -16,6 +16,7 @@ from homeassistant.components.backup import (
BackupAgent,
BackupAgentError,
BackupNotFound,
OnProgressCallback,
suggested_filename,
)
from homeassistant.core import HomeAssistant, callback
@@ -129,6 +130,7 @@ class AzureStorageBackupAgent(BackupAgent):
*,
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
backup: AgentBackup,
on_progress: OnProgressCallback,
**kwargs: Any,
) -> None:
"""Upload a backup."""
@@ -17,6 +17,7 @@ from homeassistant.components.backup import (
BackupAgent,
BackupAgentError,
BackupNotFound,
OnProgressCallback,
suggested_filename,
)
from homeassistant.core import HomeAssistant, callback
@@ -230,6 +231,7 @@ class BackblazeBackupAgent(BackupAgent):
*,
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
backup: AgentBackup,
on_progress: OnProgressCallback,
**kwargs: Any,
) -> None:
"""Upload a backup to Backblaze B2.
@@ -17,6 +17,7 @@ from .agent import (
BackupAgentError,
BackupAgentPlatformProtocol,
LocalBackupAgent,
OnProgressCallback,
)
from .config import BackupConfig, CreateBackupParametersDict
from .const import DATA_MANAGER, DOMAIN
@@ -41,6 +42,7 @@ from .manager import (
RestoreBackupEvent,
RestoreBackupStage,
RestoreBackupState,
UploadBackupEvent,
WrittenBackup,
)
from .models import AddonInfo, AgentBackup, BackupNotFound, Folder
@@ -72,9 +74,11 @@ __all__ = [
"LocalBackupAgent",
"ManagerBackup",
"NewBackup",
"OnProgressCallback",
"RestoreBackupEvent",
"RestoreBackupStage",
"RestoreBackupState",
"UploadBackupEvent",
"WrittenBackup",
"async_get_manager",
"suggested_filename",
+9
View File
@@ -14,6 +14,13 @@ from homeassistant.core import HomeAssistant, callback
from .models import AgentBackup, BackupAgentError
class OnProgressCallback(Protocol):
"""Protocol for on_progress callback."""
def __call__(self, *, bytes_uploaded: int, **kwargs: Any) -> None:
"""Report upload progress."""
class BackupAgentUnreachableError(BackupAgentError):
"""Raised when the agent can't reach its API."""
@@ -53,12 +60,14 @@ class BackupAgent(abc.ABC):
*,
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
backup: AgentBackup,
on_progress: OnProgressCallback,
**kwargs: Any,
) -> None:
"""Upload a backup.
:param open_stream: A function returning an async iterator that yields bytes.
:param backup: Metadata about the backup that should be uploaded.
:param on_progress: A callback to report the number of uploaded bytes.
"""
@abc.abstractmethod
+2 -1
View File
@@ -11,7 +11,7 @@ from typing import Any
from homeassistant.core import HomeAssistant
from homeassistant.helpers.hassio import is_hassio
from .agent import BackupAgent, LocalBackupAgent
from .agent import BackupAgent, LocalBackupAgent, OnProgressCallback
from .const import DOMAIN, LOGGER
from .models import AgentBackup, BackupNotFound
from .util import read_backup, suggested_filename
@@ -73,6 +73,7 @@ class CoreLocalBackupAgent(LocalBackupAgent):
*,
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
backup: AgentBackup,
on_progress: OnProgressCallback,
**kwargs: Any,
) -> None:
"""Upload a backup."""
+66 -4
View File
@@ -32,6 +32,7 @@ from homeassistant.helpers import (
issue_registry as ir,
start,
)
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.json import json_bytes
from homeassistant.util import dt as dt_util, json as json_util
from homeassistant.util.async_iterator import AsyncIteratorReader
@@ -78,6 +79,8 @@ from .util import (
validate_password_stream,
)
UPLOAD_PROGRESS_DEBOUNCE_SECONDS = 1
@dataclass(frozen=True, kw_only=True, slots=True)
class NewBackup:
@@ -141,6 +144,7 @@ class CreateBackupStage(StrEnum):
ADDONS = "addons"
AWAIT_ADDON_RESTARTS = "await_addon_restarts"
DOCKER_CONFIG = "docker_config"
CLEANING_UP = "cleaning_up"
FINISHING_FILE = "finishing_file"
FOLDERS = "folders"
HOME_ASSISTANT = "home_assistant"
@@ -252,6 +256,15 @@ class BlockedEvent(ManagerStateEvent):
manager_state: BackupManagerState = BackupManagerState.BLOCKED
@dataclass(frozen=True, kw_only=True, slots=True)
class UploadBackupEvent(ManagerStateEvent):
"""Backup agent upload progress event."""
agent_id: str
uploaded_bytes: int
total_bytes: int
class BackupPlatformProtocol(Protocol):
"""Define the format that backup platforms can have."""
@@ -579,9 +592,50 @@ class BackupManager:
_backup = replace(
backup, protected=should_encrypt, size=streamer.size()
)
await self.backup_agents[agent_id].async_upload_backup(
agent = self.backup_agents[agent_id]
latest_uploaded_bytes = 0
@callback
def _emit_upload_progress() -> None:
"""Emit the latest upload progress event."""
self.async_on_backup_event(
UploadBackupEvent(
manager_state=self.state,
agent_id=agent_id,
uploaded_bytes=latest_uploaded_bytes,
total_bytes=_backup.size,
)
)
upload_progress_debouncer: Debouncer[None] = Debouncer(
self.hass,
LOGGER,
cooldown=UPLOAD_PROGRESS_DEBOUNCE_SECONDS,
immediate=True,
function=_emit_upload_progress,
)
@callback
def on_upload_progress(*, bytes_uploaded: int, **kwargs: Any) -> None:
"""Handle upload progress."""
nonlocal latest_uploaded_bytes
latest_uploaded_bytes = bytes_uploaded
upload_progress_debouncer.async_schedule_call()
await agent.async_upload_backup(
open_stream=open_stream_func,
backup=_backup,
on_progress=on_upload_progress,
)
upload_progress_debouncer.async_cancel()
self.async_on_backup_event(
UploadBackupEvent(
manager_state=self.state,
agent_id=agent_id,
uploaded_bytes=_backup.size,
total_bytes=_backup.size,
)
)
if streamer:
await streamer.wait()
@@ -1237,6 +1291,13 @@ class BackupManager:
)
# delete old backups more numerous than copies
# try this regardless of agent errors above
self.async_on_backup_event(
CreateBackupEvent(
reason=None,
stage=CreateBackupStage.CLEANING_UP,
state=CreateBackupState.IN_PROGRESS,
)
)
await delete_backups_exceeding_configured_count(self)
finally:
@@ -1374,9 +1435,10 @@ class BackupManager:
"""Forward event to subscribers."""
if (current_state := self.state) != (new_state := event.manager_state):
LOGGER.debug("Backup state: %s -> %s", current_state, new_state)
self.last_event = event
if not isinstance(event, (BlockedEvent, IdleEvent)):
self.last_action_event = event
if not isinstance(event, UploadBackupEvent):
self.last_event = event
if not isinstance(event, (BlockedEvent, IdleEvent)):
self.last_action_event = event
for subscription in self._backup_event_subscriptions:
subscription(event)
+5 -1
View File
@@ -246,6 +246,8 @@ def decrypt_backup(
except (DecryptError, SecureTarError, tarfile.TarError) as err:
LOGGER.warning("Error decrypting backup: %s", err)
error = err
except Abort:
raise
except Exception as err: # noqa: BLE001
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
error = err
@@ -332,8 +334,10 @@ def encrypt_backup(
except (EncryptError, SecureTarError, tarfile.TarError) as err:
LOGGER.warning("Error encrypting backup: %s", err)
error = err
except Abort:
raise
except Exception as err: # noqa: BLE001
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
LOGGER.exception("Unexpected error when encrypting backup: %s", err)
error = err
else:
# Pad the output stream to the requested minimum size
@@ -174,13 +174,5 @@
"on": "mdi:window-open"
}
}
},
"triggers": {
"occupancy_cleared": {
"trigger": "mdi:home-outline"
},
"occupancy_detected": {
"trigger": "mdi:home"
}
}
}
@@ -1,8 +1,4 @@
{
"common": {
"trigger_behavior_description_occupancy": "The behavior of the targeted occupancy sensors to trigger on.",
"trigger_behavior_name": "Behavior"
},
"device_automation": {
"condition_type": {
"is_bat_low": "{entity_name} battery is low",
@@ -321,36 +317,5 @@
}
}
},
"selector": {
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"title": "Binary sensor",
"triggers": {
"occupancy_cleared": {
"description": "Triggers after one or more occupancy sensors stop detecting occupancy.",
"fields": {
"behavior": {
"description": "[%key:component::binary_sensor::common::trigger_behavior_description_occupancy%]",
"name": "[%key:component::binary_sensor::common::trigger_behavior_name%]"
}
},
"name": "Occupancy cleared"
},
"occupancy_detected": {
"description": "Triggers after one or more occupancy sensors start detecting occupancy.",
"fields": {
"behavior": {
"description": "[%key:component::binary_sensor::common::trigger_behavior_description_occupancy%]",
"name": "[%key:component::binary_sensor::common::trigger_behavior_name%]"
}
},
"name": "Occupancy detected"
}
}
"title": "Binary sensor"
}
@@ -1,67 +0,0 @@
"""Provides triggers for binary sensors."""
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import get_device_class
from homeassistant.helpers.trigger import EntityTargetStateTriggerBase, Trigger
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from . import DOMAIN, BinarySensorDeviceClass
def get_device_class_or_undefined(
hass: HomeAssistant, entity_id: str
) -> str | None | UndefinedType:
"""Get the device class of an entity or UNDEFINED if not found."""
try:
return get_device_class(hass, entity_id)
except HomeAssistantError:
return UNDEFINED
class BinarySensorOnOffTrigger(EntityTargetStateTriggerBase):
"""Class for binary sensor on/off triggers."""
_device_class: BinarySensorDeviceClass | None
_domains = {DOMAIN}
def entity_filter(self, entities: set[str]) -> set[str]:
"""Filter entities of this domain."""
entities = super().entity_filter(entities)
return {
entity_id
for entity_id in entities
if get_device_class_or_undefined(self._hass, entity_id)
== self._device_class
}
def make_binary_sensor_trigger(
device_class: BinarySensorDeviceClass | None,
to_state: str,
) -> type[BinarySensorOnOffTrigger]:
"""Create an entity state trigger class."""
class CustomTrigger(BinarySensorOnOffTrigger):
"""Trigger for entity state changes."""
_device_class = device_class
_to_states = {to_state}
return CustomTrigger
TRIGGERS: dict[str, type[Trigger]] = {
"occupancy_detected": make_binary_sensor_trigger(
BinarySensorDeviceClass.OCCUPANCY, STATE_ON
),
"occupancy_cleared": make_binary_sensor_trigger(
BinarySensorDeviceClass.OCCUPANCY, STATE_OFF
),
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for binary sensors."""
return TRIGGERS
@@ -16,11 +16,11 @@
"quality_scale": "internal",
"requirements": [
"bleak==2.1.1",
"bleak-retry-connector==4.4.3",
"bleak-retry-connector==4.6.0",
"bluetooth-adapters==2.1.0",
"bluetooth-auto-recovery==1.5.3",
"bluetooth-data-tools==1.28.4",
"dbus-fast==3.1.2",
"habluetooth==5.8.0"
"habluetooth==5.10.2"
]
}
@@ -1,177 +0,0 @@
"""Reads vehicle status from MyBMW portal."""
from __future__ import annotations
import logging
import voluptuous as vol
from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITY_ID, CONF_NAME, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
discovery,
entity_registry as er,
)
from .const import ATTR_VIN, CONF_READ_ONLY, DOMAIN
from .coordinator import BMWConfigEntry, BMWDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
SERVICE_SCHEMA = vol.Schema(
vol.Any(
{vol.Required(ATTR_VIN): cv.string},
{vol.Required(CONF_DEVICE_ID): cv.string},
)
)
DEFAULT_OPTIONS = {
CONF_READ_ONLY: False,
}
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.DEVICE_TRACKER,
Platform.LOCK,
Platform.NOTIFY,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
]
SERVICE_UPDATE_STATE = "update_state"
@callback
def _async_migrate_options_from_data_if_missing(
hass: HomeAssistant, entry: BMWConfigEntry
) -> None:
data = dict(entry.data)
options = dict(entry.options)
if CONF_READ_ONLY in data or list(options) != list(DEFAULT_OPTIONS):
options = dict(
DEFAULT_OPTIONS,
**{k: v for k, v in options.items() if k in DEFAULT_OPTIONS},
)
options[CONF_READ_ONLY] = data.pop(CONF_READ_ONLY, False)
hass.config_entries.async_update_entry(entry, data=data, options=options)
async def _async_migrate_entries(
hass: HomeAssistant, config_entry: BMWConfigEntry
) -> bool:
"""Migrate old entry."""
entity_registry = er.async_get(hass)
@callback
def update_unique_id(entry: er.RegistryEntry) -> dict[str, str] | None:
replacements = {
Platform.SENSOR.value: {
"charging_level_hv": "fuel_and_battery.remaining_battery_percent",
"fuel_percent": "fuel_and_battery.remaining_fuel_percent",
"ac_current_limit": "charging_profile.ac_current_limit",
"charging_start_time": "fuel_and_battery.charging_start_time",
"charging_end_time": "fuel_and_battery.charging_end_time",
"charging_status": "fuel_and_battery.charging_status",
"charging_target": "fuel_and_battery.charging_target",
"remaining_battery_percent": "fuel_and_battery.remaining_battery_percent",
"remaining_range_total": "fuel_and_battery.remaining_range_total",
"remaining_range_electric": "fuel_and_battery.remaining_range_electric",
"remaining_range_fuel": "fuel_and_battery.remaining_range_fuel",
"remaining_fuel": "fuel_and_battery.remaining_fuel",
"remaining_fuel_percent": "fuel_and_battery.remaining_fuel_percent",
"activity": "climate.activity",
}
}
if (key := entry.unique_id.split("-")[-1]) in replacements.get(
entry.domain, []
):
new_unique_id = entry.unique_id.replace(
key, replacements[entry.domain][key]
)
_LOGGER.debug(
"Migrating entity '%s' unique_id from '%s' to '%s'",
entry.entity_id,
entry.unique_id,
new_unique_id,
)
if existing_entity_id := entity_registry.async_get_entity_id(
entry.domain, entry.platform, new_unique_id
):
_LOGGER.debug(
"Cannot migrate to unique_id '%s', already exists for '%s'",
new_unique_id,
existing_entity_id,
)
return None
return {
"new_unique_id": new_unique_id,
}
return None
await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
return True
async def async_setup_entry(hass: HomeAssistant, entry: BMWConfigEntry) -> bool:
"""Set up BMW Connected Drive from a config entry."""
_async_migrate_options_from_data_if_missing(hass, entry)
await _async_migrate_entries(hass, entry)
# Set up one data coordinator per account/config entry
coordinator = BMWDataUpdateCoordinator(
hass,
config_entry=entry,
)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
# Set up all platforms except notify
await hass.config_entries.async_forward_entry_setups(
entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
)
# set up notify platform, no entry support for notify platform yet,
# have to use discovery to load platform.
hass.async_create_task(
discovery.async_load_platform(
hass,
Platform.NOTIFY,
DOMAIN,
{CONF_NAME: DOMAIN, CONF_ENTITY_ID: entry.entry_id},
{},
)
)
# Clean up vehicles which are not assigned to the account anymore
account_vehicles = {(DOMAIN, v.vin) for v in coordinator.account.vehicles}
device_registry = dr.async_get(hass)
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry_id=entry.entry_id
)
for device in device_entries:
if not device.identifiers.intersection(account_vehicles):
device_registry.async_update_device(
device.id, remove_config_entry_id=entry.entry_id
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: BMWConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(
entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
)
@@ -1,254 +0,0 @@
"""Reads vehicle status from BMW MyBMW portal."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Any
from bimmer_connected.vehicle import MyBMWVehicle
from bimmer_connected.vehicle.doors_windows import LockState
from bimmer_connected.vehicle.fuel_and_battery import ChargingState
from bimmer_connected.vehicle.reports import ConditionBasedService
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.unit_system import UnitSystem
from . import BMWConfigEntry
from .const import UNIT_MAP
from .coordinator import BMWDataUpdateCoordinator
from .entity import BMWBaseEntity
PARALLEL_UPDATES = 0
_LOGGER = logging.getLogger(__name__)
ALLOWED_CONDITION_BASED_SERVICE_KEYS = {
"BRAKE_FLUID",
"BRAKE_PADS_FRONT",
"BRAKE_PADS_REAR",
"EMISSION_CHECK",
"ENGINE_OIL",
"OIL",
"TIRE_WEAR_FRONT",
"TIRE_WEAR_REAR",
"VEHICLE_CHECK",
"VEHICLE_TUV",
}
LOGGED_CONDITION_BASED_SERVICE_WARNINGS: set[str] = set()
ALLOWED_CHECK_CONTROL_MESSAGE_KEYS = {
"ENGINE_OIL",
"TIRE_PRESSURE",
"WASHING_FLUID",
}
LOGGED_CHECK_CONTROL_MESSAGE_WARNINGS: set[str] = set()
def _condition_based_services(
vehicle: MyBMWVehicle, unit_system: UnitSystem
) -> dict[str, Any]:
extra_attributes = {}
for report in vehicle.condition_based_services.messages:
if (
report.service_type not in ALLOWED_CONDITION_BASED_SERVICE_KEYS
and report.service_type not in LOGGED_CONDITION_BASED_SERVICE_WARNINGS
):
_LOGGER.warning(
"'%s' not an allowed condition based service (%s)",
report.service_type,
report,
)
LOGGED_CONDITION_BASED_SERVICE_WARNINGS.add(report.service_type)
continue
extra_attributes.update(_format_cbs_report(report, unit_system))
return extra_attributes
def _check_control_messages(vehicle: MyBMWVehicle) -> dict[str, Any]:
extra_attributes: dict[str, Any] = {}
for message in vehicle.check_control_messages.messages:
if (
message.description_short not in ALLOWED_CHECK_CONTROL_MESSAGE_KEYS
and message.description_short not in LOGGED_CHECK_CONTROL_MESSAGE_WARNINGS
):
_LOGGER.warning(
"'%s' not an allowed check control message (%s)",
message.description_short,
message,
)
LOGGED_CHECK_CONTROL_MESSAGE_WARNINGS.add(message.description_short)
continue
extra_attributes[message.description_short.lower()] = message.state.value
return extra_attributes
def _format_cbs_report(
report: ConditionBasedService, unit_system: UnitSystem
) -> dict[str, Any]:
result: dict[str, Any] = {}
service_type = report.service_type.lower()
result[service_type] = report.state.value
if report.due_date is not None:
result[f"{service_type}_date"] = report.due_date.strftime("%Y-%m-%d")
if report.due_distance.value and report.due_distance.unit:
distance = round(
unit_system.length(
report.due_distance.value,
UNIT_MAP.get(report.due_distance.unit, report.due_distance.unit),
)
)
result[f"{service_type}_distance"] = f"{distance} {unit_system.length_unit}"
return result
@dataclass(frozen=True, kw_only=True)
class BMWBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes BMW binary_sensor entity."""
value_fn: Callable[[MyBMWVehicle], bool]
attr_fn: Callable[[MyBMWVehicle, UnitSystem], dict[str, Any]] | None = None
is_available: Callable[[MyBMWVehicle], bool] = lambda v: v.is_lsc_enabled
SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = (
BMWBinarySensorEntityDescription(
key="lids",
translation_key="lids",
device_class=BinarySensorDeviceClass.OPENING,
# device class opening: On means open, Off means closed
value_fn=lambda v: not v.doors_and_windows.all_lids_closed,
attr_fn=lambda v, u: {
lid.name: lid.state.value for lid in v.doors_and_windows.lids
},
),
BMWBinarySensorEntityDescription(
key="windows",
translation_key="windows",
device_class=BinarySensorDeviceClass.OPENING,
# device class opening: On means open, Off means closed
value_fn=lambda v: not v.doors_and_windows.all_windows_closed,
attr_fn=lambda v, u: {
window.name: window.state.value for window in v.doors_and_windows.windows
},
),
BMWBinarySensorEntityDescription(
key="door_lock_state",
translation_key="door_lock_state",
device_class=BinarySensorDeviceClass.LOCK,
# device class lock: On means unlocked, Off means locked
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
value_fn=lambda v: (
v.doors_and_windows.door_lock_state
not in {LockState.LOCKED, LockState.SECURED}
),
attr_fn=lambda v, u: {
"door_lock_state": v.doors_and_windows.door_lock_state.value
},
),
BMWBinarySensorEntityDescription(
key="condition_based_services",
translation_key="condition_based_services",
device_class=BinarySensorDeviceClass.PROBLEM,
# device class problem: On means problem detected, Off means no problem
value_fn=lambda v: v.condition_based_services.is_service_required,
attr_fn=_condition_based_services,
),
BMWBinarySensorEntityDescription(
key="check_control_messages",
translation_key="check_control_messages",
device_class=BinarySensorDeviceClass.PROBLEM,
# device class problem: On means problem detected, Off means no problem
value_fn=lambda v: v.check_control_messages.has_check_control_messages,
attr_fn=lambda v, u: _check_control_messages(v),
),
# electric
BMWBinarySensorEntityDescription(
key="charging_status",
translation_key="charging_status",
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
# device class power: On means power detected, Off means no power
value_fn=lambda v: v.fuel_and_battery.charging_status == ChargingState.CHARGING,
is_available=lambda v: v.has_electric_drivetrain,
),
BMWBinarySensorEntityDescription(
key="connection_status",
translation_key="connection_status",
device_class=BinarySensorDeviceClass.PLUG,
value_fn=lambda v: v.fuel_and_battery.is_charger_connected,
is_available=lambda v: v.has_electric_drivetrain,
),
BMWBinarySensorEntityDescription(
key="is_pre_entry_climatization_enabled",
translation_key="is_pre_entry_climatization_enabled",
value_fn=lambda v: (
v.charging_profile.is_pre_entry_climatization_enabled
if v.charging_profile
else False
),
is_available=lambda v: v.has_electric_drivetrain,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BMWConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the BMW binary sensors from config entry."""
coordinator = config_entry.runtime_data
entities = [
BMWBinarySensor(coordinator, vehicle, description, hass.config.units)
for vehicle in coordinator.account.vehicles
for description in SENSOR_TYPES
if description.is_available(vehicle)
]
async_add_entities(entities)
class BMWBinarySensor(BMWBaseEntity, BinarySensorEntity):
"""Representation of a BMW vehicle binary sensor."""
entity_description: BMWBinarySensorEntityDescription
def __init__(
self,
coordinator: BMWDataUpdateCoordinator,
vehicle: MyBMWVehicle,
description: BMWBinarySensorEntityDescription,
unit_system: UnitSystem,
) -> None:
"""Initialize sensor."""
super().__init__(coordinator, vehicle)
self.entity_description = description
self._unit_system = unit_system
self._attr_unique_id = f"{vehicle.vin}-{description.key}"
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
_LOGGER.debug(
"Updating binary sensor '%s' of %s",
self.entity_description.key,
self.vehicle.name,
)
self._attr_is_on = self.entity_description.value_fn(self.vehicle)
if self.entity_description.attr_fn:
self._attr_extra_state_attributes = self.entity_description.attr_fn(
self.vehicle, self._unit_system
)
super()._handle_coordinator_update()
@@ -1,127 +0,0 @@
"""Support for MyBMW button entities."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
import logging
from typing import TYPE_CHECKING, Any
from bimmer_connected.models import MyBMWAPIError
from bimmer_connected.vehicle import MyBMWVehicle
from bimmer_connected.vehicle.remote_services import RemoteServiceStatus
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN, BMWConfigEntry
from .entity import BMWBaseEntity
if TYPE_CHECKING:
from .coordinator import BMWDataUpdateCoordinator
PARALLEL_UPDATES = 1
_LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True, kw_only=True)
class BMWButtonEntityDescription(ButtonEntityDescription):
"""Class describing BMW button entities."""
remote_function: Callable[[MyBMWVehicle], Coroutine[Any, Any, RemoteServiceStatus]]
enabled_when_read_only: bool = False
is_available: Callable[[MyBMWVehicle], bool] = lambda _: True
BUTTON_TYPES: tuple[BMWButtonEntityDescription, ...] = (
BMWButtonEntityDescription(
key="light_flash",
translation_key="light_flash",
remote_function=lambda vehicle: (
vehicle.remote_services.trigger_remote_light_flash()
),
),
BMWButtonEntityDescription(
key="sound_horn",
translation_key="sound_horn",
remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_horn(),
),
BMWButtonEntityDescription(
key="activate_air_conditioning",
translation_key="activate_air_conditioning",
remote_function=lambda vehicle: (
vehicle.remote_services.trigger_remote_air_conditioning()
),
),
BMWButtonEntityDescription(
key="deactivate_air_conditioning",
translation_key="deactivate_air_conditioning",
remote_function=lambda vehicle: (
vehicle.remote_services.trigger_remote_air_conditioning_stop()
),
is_available=lambda vehicle: vehicle.is_remote_climate_stop_enabled,
),
BMWButtonEntityDescription(
key="find_vehicle",
translation_key="find_vehicle",
remote_function=lambda vehicle: (
vehicle.remote_services.trigger_remote_vehicle_finder()
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BMWConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the BMW buttons from config entry."""
coordinator = config_entry.runtime_data
entities: list[BMWButton] = []
for vehicle in coordinator.account.vehicles:
entities.extend(
[
BMWButton(coordinator, vehicle, description)
for description in BUTTON_TYPES
if (not coordinator.read_only and description.is_available(vehicle))
or (coordinator.read_only and description.enabled_when_read_only)
]
)
async_add_entities(entities)
class BMWButton(BMWBaseEntity, ButtonEntity):
"""Representation of a MyBMW button."""
entity_description: BMWButtonEntityDescription
def __init__(
self,
coordinator: BMWDataUpdateCoordinator,
vehicle: MyBMWVehicle,
description: BMWButtonEntityDescription,
) -> None:
"""Initialize BMW vehicle sensor."""
super().__init__(coordinator, vehicle)
self.entity_description = description
self._attr_unique_id = f"{vehicle.vin}-{description.key}"
async def async_press(self) -> None:
"""Press the button."""
try:
await self.entity_description.remote_function(self.vehicle)
except MyBMWAPIError as ex:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
self.coordinator.async_update_listeners()
@@ -1,277 +0,0 @@
"""Config flow for BMW ConnectedDrive integration."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from bimmer_connected.api.authentication import MyBMWAuthentication
from bimmer_connected.api.regions import get_region_from_name
from bimmer_connected.models import (
MyBMWAPIError,
MyBMWAuthError,
MyBMWCaptchaMissingError,
)
from httpx import RequestError
import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_REAUTH,
SOURCE_RECONFIGURE,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_SOURCE, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
from homeassistant.util.ssl import get_default_context
from . import DOMAIN
from .const import (
CONF_ALLOWED_REGIONS,
CONF_CAPTCHA_REGIONS,
CONF_CAPTCHA_TOKEN,
CONF_CAPTCHA_URL,
CONF_GCID,
CONF_READ_ONLY,
CONF_REFRESH_TOKEN,
)
from .coordinator import BMWConfigEntry
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_REGION): SelectSelector(
SelectSelectorConfig(
options=CONF_ALLOWED_REGIONS,
translation_key="regions",
)
),
},
extra=vol.REMOVE_EXTRA,
)
RECONFIGURE_SCHEMA = vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
},
extra=vol.REMOVE_EXTRA,
)
CAPTCHA_SCHEMA = vol.Schema(
{
vol.Required(CONF_CAPTCHA_TOKEN): str,
},
extra=vol.REMOVE_EXTRA,
)
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]:
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
auth = MyBMWAuthentication(
data[CONF_USERNAME],
data[CONF_PASSWORD],
get_region_from_name(data[CONF_REGION]),
hcaptcha_token=data.get(CONF_CAPTCHA_TOKEN),
verify=get_default_context(),
)
try:
await auth.login()
except MyBMWCaptchaMissingError as ex:
raise MissingCaptcha from ex
except MyBMWAuthError as ex:
raise InvalidAuth from ex
except (MyBMWAPIError, RequestError) as ex:
raise CannotConnect from ex
# Return info that you want to store in the config entry.
retval = {"title": f"{data[CONF_USERNAME]}{data.get(CONF_SOURCE, '')}"}
if auth.refresh_token:
retval[CONF_REFRESH_TOKEN] = auth.refresh_token
if auth.gcid:
retval[CONF_GCID] = auth.gcid
return retval
class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for MyBMW."""
VERSION = 1
def __init__(self) -> None:
"""Initialize the config flow."""
self.data: dict[str, Any] = {}
self._existing_entry_data: dict[str, Any] = {}
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = self.data.pop("errors", {})
if user_input is not None and not errors:
unique_id = f"{user_input[CONF_REGION]}-{user_input[CONF_USERNAME]}"
await self.async_set_unique_id(unique_id)
# Unique ID cannot change for reauth/reconfigure
if self.source not in {SOURCE_REAUTH, SOURCE_RECONFIGURE}:
self._abort_if_unique_id_configured()
# Store user input for later use
self.data.update(user_input)
# North America and Rest of World require captcha token
if (
self.data.get(CONF_REGION) in CONF_CAPTCHA_REGIONS
and CONF_CAPTCHA_TOKEN not in self.data
):
return await self.async_step_captcha()
info = None
try:
info = await validate_input(self.hass, self.data)
except MissingCaptcha:
errors["base"] = "missing_captcha"
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
finally:
self.data.pop(CONF_CAPTCHA_TOKEN, None)
if info:
entry_data = {
**self.data,
CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN),
CONF_GCID: info.get(CONF_GCID),
}
if self.source == SOURCE_REAUTH:
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data=entry_data
)
if self.source == SOURCE_RECONFIGURE:
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(),
data=entry_data,
)
return self.async_create_entry(
title=info["title"],
data=entry_data,
)
schema = self.add_suggested_values_to_schema(
DATA_SCHEMA,
self._existing_entry_data or self.data,
)
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
async def async_step_change_password(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Show the change password step."""
if user_input is not None:
return await self.async_step_user(self._existing_entry_data | user_input)
return self.async_show_form(
step_id="change_password",
data_schema=RECONFIGURE_SCHEMA,
description_placeholders={
CONF_USERNAME: self._existing_entry_data[CONF_USERNAME],
CONF_REGION: self._existing_entry_data[CONF_REGION],
},
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
self._existing_entry_data = dict(entry_data)
return await self.async_step_change_password()
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a reconfiguration flow initialized by the user."""
self._existing_entry_data = dict(self._get_reconfigure_entry().data)
return await self.async_step_change_password()
async def async_step_captcha(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Show captcha form."""
if user_input and user_input.get(CONF_CAPTCHA_TOKEN):
self.data[CONF_CAPTCHA_TOKEN] = user_input[CONF_CAPTCHA_TOKEN].strip()
return await self.async_step_user(self.data)
return self.async_show_form(
step_id="captcha",
data_schema=CAPTCHA_SCHEMA,
description_placeholders={
"captcha_url": CONF_CAPTCHA_URL.format(region=self.data[CONF_REGION])
},
)
@staticmethod
@callback
def async_get_options_flow(
config_entry: BMWConfigEntry,
) -> BMWOptionsFlow:
"""Return a MyBMW option flow."""
return BMWOptionsFlow()
class BMWOptionsFlow(OptionsFlow):
"""Handle a option flow for MyBMW."""
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the options."""
return await self.async_step_account_options()
async def async_step_account_options(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
if user_input is not None:
# Manually update & reload the config entry after options change.
# Required as each successful login will store the latest refresh_token
# using async_update_entry, which would otherwise trigger a full reload
# if the options would be refreshed using a listener.
changed = self.hass.config_entries.async_update_entry(
self.config_entry,
options=user_input,
)
if changed:
await self.hass.config_entries.async_reload(self.config_entry.entry_id)
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="account_options",
data_schema=vol.Schema(
{
vol.Optional(
CONF_READ_ONLY,
default=self.config_entry.options.get(CONF_READ_ONLY, False),
): bool,
}
),
)
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
class MissingCaptcha(HomeAssistantError):
"""Error to indicate the captcha token is missing."""
@@ -1,34 +0,0 @@
"""Const file for the MyBMW integration."""
from homeassistant.const import UnitOfLength, UnitOfVolume
DOMAIN = "bmw_connected_drive"
ATTR_DIRECTION = "direction"
ATTR_VIN = "vin"
CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"]
CONF_CAPTCHA_REGIONS = ["north_america", "rest_of_world"]
CONF_READ_ONLY = "read_only"
CONF_ACCOUNT = "account"
CONF_REFRESH_TOKEN = "refresh_token"
CONF_GCID = "gcid"
CONF_CAPTCHA_TOKEN = "captcha_token"
CONF_CAPTCHA_URL = (
"https://bimmer-connected.readthedocs.io/en/stable/captcha/{region}.html"
)
DATA_HASS_CONFIG = "hass_config"
UNIT_MAP = {
"KILOMETERS": UnitOfLength.KILOMETERS,
"MILES": UnitOfLength.MILES,
"LITERS": UnitOfVolume.LITERS,
"GALLONS": UnitOfVolume.GALLONS,
}
SCAN_INTERVALS = {
"china": 300,
"north_america": 600,
"rest_of_world": 300,
}
@@ -1,113 +0,0 @@
"""Coordinator for BMW."""
from __future__ import annotations
from datetime import timedelta
import logging
from bimmer_connected.account import MyBMWAccount
from bimmer_connected.api.regions import get_region_from_name
from bimmer_connected.models import (
GPSPosition,
MyBMWAPIError,
MyBMWAuthError,
MyBMWCaptchaMissingError,
)
from httpx import RequestError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.ssl import get_default_context
from .const import CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN, SCAN_INTERVALS
_LOGGER = logging.getLogger(__name__)
type BMWConfigEntry = ConfigEntry[BMWDataUpdateCoordinator]
class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
"""Class to manage fetching BMW data."""
account: MyBMWAccount
config_entry: BMWConfigEntry
def __init__(self, hass: HomeAssistant, *, config_entry: BMWConfigEntry) -> None:
"""Initialize account-wide BMW data updater."""
self.account = MyBMWAccount(
config_entry.data[CONF_USERNAME],
config_entry.data[CONF_PASSWORD],
get_region_from_name(config_entry.data[CONF_REGION]),
observer_position=GPSPosition(hass.config.latitude, hass.config.longitude),
verify=get_default_context(),
)
self.read_only: bool = config_entry.options[CONF_READ_ONLY]
if CONF_REFRESH_TOKEN in config_entry.data:
self.account.set_refresh_token(
refresh_token=config_entry.data[CONF_REFRESH_TOKEN],
gcid=config_entry.data.get(CONF_GCID),
)
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=f"{DOMAIN}-{config_entry.data[CONF_USERNAME]}",
update_interval=timedelta(
seconds=SCAN_INTERVALS[config_entry.data[CONF_REGION]]
),
)
# Default to false on init so _async_update_data logic works
self.last_update_success = False
async def _async_update_data(self) -> None:
"""Fetch data from BMW."""
old_refresh_token = self.account.refresh_token
try:
await self.account.get_vehicles()
except MyBMWCaptchaMissingError as err:
# If a captcha is required (user/password login flow), always trigger the reauth flow
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="missing_captcha",
) from err
except MyBMWAuthError as err:
# Allow one retry interval before raising AuthFailed to avoid flaky API issues
if self.last_update_success:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"exception": str(err)},
) from err
# Clear refresh token and trigger reauth if previous update failed as well
self._update_config_entry_refresh_token(None)
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
) from err
except (MyBMWAPIError, RequestError) as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"exception": str(err)},
) from err
if self.account.refresh_token != old_refresh_token:
self._update_config_entry_refresh_token(self.account.refresh_token)
def _update_config_entry_refresh_token(self, refresh_token: str | None) -> None:
"""Update or delete the refresh_token in the Config Entry."""
data = {
**self.config_entry.data,
CONF_REFRESH_TOKEN: refresh_token,
}
if not refresh_token:
data.pop(CONF_REFRESH_TOKEN)
self.hass.config_entries.async_update_entry(self.config_entry, data=data)
@@ -1,86 +0,0 @@
"""Device tracker for MyBMW vehicles."""
from __future__ import annotations
import logging
from typing import Any
from bimmer_connected.vehicle import MyBMWVehicle
from homeassistant.components.device_tracker import TrackerEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BMWConfigEntry
from .const import ATTR_DIRECTION
from .coordinator import BMWDataUpdateCoordinator
from .entity import BMWBaseEntity
PARALLEL_UPDATES = 0
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BMWConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the MyBMW tracker from config entry."""
coordinator = config_entry.runtime_data
entities: list[BMWDeviceTracker] = []
for vehicle in coordinator.account.vehicles:
entities.append(BMWDeviceTracker(coordinator, vehicle))
if not vehicle.is_vehicle_tracking_enabled:
_LOGGER.info(
(
"Tracking is (currently) disabled for vehicle %s (%s), defaulting"
" to unknown"
),
vehicle.name,
vehicle.vin,
)
async_add_entities(entities)
class BMWDeviceTracker(BMWBaseEntity, TrackerEntity):
"""MyBMW device tracker."""
_attr_force_update = False
_attr_translation_key = "car"
_attr_name = None
def __init__(
self,
coordinator: BMWDataUpdateCoordinator,
vehicle: MyBMWVehicle,
) -> None:
"""Initialize the Tracker."""
super().__init__(coordinator, vehicle)
self._attr_unique_id = vehicle.vin
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return entity specific state attributes."""
return {ATTR_DIRECTION: self.vehicle.vehicle_location.heading}
@property
def latitude(self) -> float | None:
"""Return latitude value of the device."""
return (
self.vehicle.vehicle_location.location[0]
if self.vehicle.is_vehicle_tracking_enabled
and self.vehicle.vehicle_location.location
else None
)
@property
def longitude(self) -> float | None:
"""Return longitude value of the device."""
return (
self.vehicle.vehicle_location.location[1]
if self.vehicle.is_vehicle_tracking_enabled
and self.vehicle.vehicle_location.location
else None
)
@@ -1,100 +0,0 @@
"""Diagnostics support for the BMW Connected Drive integration."""
from __future__ import annotations
from dataclasses import asdict
import json
from typing import TYPE_CHECKING, Any
from bimmer_connected.utils import MyBMWJSONEncoder
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntry
from . import BMWConfigEntry
from .const import CONF_REFRESH_TOKEN
PARALLEL_UPDATES = 1
if TYPE_CHECKING:
from bimmer_connected.vehicle import MyBMWVehicle
TO_REDACT_INFO = [CONF_USERNAME, CONF_PASSWORD, CONF_REFRESH_TOKEN]
TO_REDACT_DATA = [
"lat",
"latitude",
"lon",
"longitude",
"heading",
"vin",
"licensePlate",
"city",
"street",
"streetNumber",
"postalCode",
"phone",
"formatted",
"subtitle",
]
def vehicle_to_dict(vehicle: MyBMWVehicle | None) -> dict:
"""Convert a MyBMWVehicle to a dictionary using MyBMWJSONEncoder."""
retval: dict = json.loads(json.dumps(vehicle, cls=MyBMWJSONEncoder))
return retval
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: BMWConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = config_entry.runtime_data
coordinator.account.config.log_responses = True
await coordinator.account.get_vehicles(force_init=True)
diagnostics_data = {
"info": async_redact_data(config_entry.data, TO_REDACT_INFO),
"data": [
async_redact_data(vehicle_to_dict(vehicle), TO_REDACT_DATA)
for vehicle in coordinator.account.vehicles
],
"fingerprint": async_redact_data(
[asdict(r) for r in coordinator.account.get_stored_responses()],
TO_REDACT_DATA,
),
}
coordinator.account.config.log_responses = False
return diagnostics_data
async def async_get_device_diagnostics(
hass: HomeAssistant, config_entry: BMWConfigEntry, device: DeviceEntry
) -> dict[str, Any]:
"""Return diagnostics for a device."""
coordinator = config_entry.runtime_data
coordinator.account.config.log_responses = True
await coordinator.account.get_vehicles(force_init=True)
vin = next(iter(device.identifiers))[1]
vehicle = coordinator.account.get_vehicle(vin)
diagnostics_data = {
"info": async_redact_data(config_entry.data, TO_REDACT_INFO),
"data": async_redact_data(vehicle_to_dict(vehicle), TO_REDACT_DATA),
# Always have to get the full fingerprint as the VIN is redacted beforehand by the library
"fingerprint": async_redact_data(
[asdict(r) for r in coordinator.account.get_stored_responses()],
TO_REDACT_DATA,
),
}
coordinator.account.config.log_responses = False
return diagnostics_data
@@ -1,40 +0,0 @@
"""Base for all BMW entities."""
from __future__ import annotations
from bimmer_connected.vehicle import MyBMWVehicle
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import BMWDataUpdateCoordinator
class BMWBaseEntity(CoordinatorEntity[BMWDataUpdateCoordinator]):
"""Common base for BMW entities."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: BMWDataUpdateCoordinator,
vehicle: MyBMWVehicle,
) -> None:
"""Initialize entity."""
super().__init__(coordinator)
self.vehicle = vehicle
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, vehicle.vin)},
manufacturer=vehicle.brand.name,
model=vehicle.name,
name=vehicle.name,
serial_number=vehicle.vin,
)
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self._handle_coordinator_update()
@@ -1,102 +0,0 @@
{
"entity": {
"binary_sensor": {
"charging_status": {
"default": "mdi:ev-station"
},
"check_control_messages": {
"default": "mdi:car-tire-alert"
},
"condition_based_services": {
"default": "mdi:wrench"
},
"connection_status": {
"default": "mdi:car-electric"
},
"door_lock_state": {
"default": "mdi:car-key"
},
"is_pre_entry_climatization_enabled": {
"default": "mdi:car-seat-heater"
},
"lids": {
"default": "mdi:car-door-lock"
},
"windows": {
"default": "mdi:car-door"
}
},
"button": {
"activate_air_conditioning": {
"default": "mdi:hvac"
},
"deactivate_air_conditioning": {
"default": "mdi:hvac-off"
},
"find_vehicle": {
"default": "mdi:crosshairs-question"
},
"light_flash": {
"default": "mdi:car-light-alert"
},
"sound_horn": {
"default": "mdi:bullhorn"
}
},
"device_tracker": {
"car": {
"default": "mdi:car"
}
},
"number": {
"target_soc": {
"default": "mdi:battery-charging-medium"
}
},
"select": {
"ac_limit": {
"default": "mdi:current-ac"
},
"charging_mode": {
"default": "mdi:vector-point-select"
}
},
"sensor": {
"charging_status": {
"default": "mdi:ev-station"
},
"charging_target": {
"default": "mdi:battery-charging-high"
},
"climate_status": {
"default": "mdi:fan"
},
"mileage": {
"default": "mdi:speedometer"
},
"remaining_fuel": {
"default": "mdi:gas-station"
},
"remaining_fuel_percent": {
"default": "mdi:gas-station"
},
"remaining_range_electric": {
"default": "mdi:map-marker-distance"
},
"remaining_range_fuel": {
"default": "mdi:map-marker-distance"
},
"remaining_range_total": {
"default": "mdi:map-marker-distance"
}
},
"switch": {
"charging": {
"default": "mdi:ev-station"
},
"climate": {
"default": "mdi:fan"
}
}
}
}
@@ -1,121 +0,0 @@
"""Support for BMW car locks with BMW ConnectedDrive."""
from __future__ import annotations
import logging
from typing import Any
from bimmer_connected.models import MyBMWAPIError
from bimmer_connected.vehicle import MyBMWVehicle
from bimmer_connected.vehicle.doors_windows import LockState
from homeassistant.components.lock import LockEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN, BMWConfigEntry
from .coordinator import BMWDataUpdateCoordinator
from .entity import BMWBaseEntity
PARALLEL_UPDATES = 1
DOOR_LOCK_STATE = "door_lock_state"
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BMWConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the MyBMW lock from config entry."""
coordinator = config_entry.runtime_data
if not coordinator.read_only:
async_add_entities(
BMWLock(coordinator, vehicle) for vehicle in coordinator.account.vehicles
)
class BMWLock(BMWBaseEntity, LockEntity):
"""Representation of a MyBMW vehicle lock."""
_attr_translation_key = "lock"
def __init__(
self,
coordinator: BMWDataUpdateCoordinator,
vehicle: MyBMWVehicle,
) -> None:
"""Initialize the lock."""
super().__init__(coordinator, vehicle)
self._attr_unique_id = f"{vehicle.vin}-lock"
self.door_lock_state_available = vehicle.is_lsc_enabled
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the car."""
_LOGGER.debug("%s: locking doors", self.vehicle.name)
# Only update the HA state machine if the vehicle reliably reports its lock state
if self.door_lock_state_available:
# Optimistic state set here because it takes some time before the
# update callback response
self._attr_is_locked = True
self.async_write_ha_state()
try:
await self.vehicle.remote_services.trigger_remote_door_lock()
except MyBMWAPIError as ex:
# Set the state to unknown if the command fails
self._attr_is_locked = None
self.async_write_ha_state()
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
finally:
# Always update the listeners to get the latest state
self.coordinator.async_update_listeners()
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the car."""
_LOGGER.debug("%s: unlocking doors", self.vehicle.name)
# Only update the HA state machine if the vehicle reliably reports its lock state
if self.door_lock_state_available:
# Optimistic state set here because it takes some time before the
# update callback response
self._attr_is_locked = False
self.async_write_ha_state()
try:
await self.vehicle.remote_services.trigger_remote_door_unlock()
except MyBMWAPIError as ex:
# Set the state to unknown if the command fails
self._attr_is_locked = None
self.async_write_ha_state()
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
finally:
# Always update the listeners to get the latest state
self.coordinator.async_update_listeners()
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
_LOGGER.debug("Updating lock data of %s", self.vehicle.name)
# Only update the HA state machine if the vehicle reliably reports its lock state
if self.door_lock_state_available:
self._attr_is_locked = self.vehicle.doors_and_windows.door_lock_state in {
LockState.LOCKED,
LockState.SECURED,
}
self._attr_extra_state_attributes = {
DOOR_LOCK_STATE: self.vehicle.doors_and_windows.door_lock_state.value
}
super()._handle_coordinator_update()

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