Compare commits

...

444 Commits

Author SHA1 Message Date
abmantis
b3fd7bc0cf Test refs 2026-03-03 15:24:51 +00:00
James
713b7cf36d Check Daikin zone temp keys before represent (#164297)
Co-authored-by: barneyonline <barneyonline@users.noreply.github.com>
2026-03-02 19:48:39 +00:00
Bram Kragten
cb016b014b Update frontend to 20260302.0 (#164612) 2026-03-02 18:53:01 +01:00
Michael Hansen
afb4523f63 Add device_id and satellite_id to conversation HTTP/websocket APIs (#164414) 2026-03-02 17:01:51 +01:00
Alex Brown
05ad4986ac Fix Matter clear lock user (#164493) 2026-03-02 16:28:49 +01:00
epenet
42dbd5f98f Migrate moat to runtime_data (#164605) 2026-03-02 16:14:25 +01:00
epenet
f58a514ce7 Migrate monzo to runtime_data (#164603) 2026-03-02 16:14:10 +01:00
Artur Pragacz
8fb384a5e1 Raise on vacuum area mapping not configured (#164595) 2026-03-02 15:36:48 +01:00
Samuel Xiao
c24302b5ce Switchbot Cloud: Fixed Smart Radiator Thermostat off line (#162714)
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-03-02 14:44:34 +01:00
Jan-Philipp Benecke
999ad9b642 Bump aiotankerkoenig to 0.5.1 (#164590) 2026-03-02 14:44:29 +01:00
Pierre Sassoulas
36d6b4dafe Use clearer number notation for very small and very large literals (#164521) 2026-03-02 14:06:19 +01:00
Norbert Rittel
06870a2e25 Replace "the lock" with "a lock" in matter action descriptions (#164585) 2026-03-02 12:56:45 +01:00
willemstuursma
85eba2bb15 Bump DSMR parser to 1.5.0 (#164484) 2026-03-02 12:52:37 +01:00
Joost Lekkerkerker
5dd6dcc215 Add select for SmartThings Water spray level (#164520) 2026-03-02 12:17:31 +01:00
epenet
8bf894a514 Migrate microbees to runtime_data (#164564) 2026-03-02 12:04:34 +01:00
epenet
d3c67f2ae1 Migrate medcom_ble to runtime_data (#164557) 2026-03-02 12:03:35 +01:00
epenet
b60a282b60 Move motioneye coordinator to separate module (#164568) 2026-03-02 11:57:19 +01:00
epenet
0da1d40a19 Migrate meteoclimatic to runtime_data (#164559) 2026-03-02 11:50:46 +01:00
Robert Resch
aa3be915a0 Bump aiogithubapi to 26.0.0 (#164579) 2026-03-02 11:49:32 +01:00
Manu
0d97bfbc59 Bump pyloadapi to 2.0.0 (#164495) 2026-03-02 11:47:13 +01:00
epenet
fe830337c9 Migrate modem_callerid to runtime_data (#164566) 2026-03-02 11:45:58 +01:00
epenet
5210b7d847 Migrate moehlenhoff_alpha2 to runtime_data (#164571) 2026-03-02 11:45:10 +01:00
Mike Ryan
2f7ed4040b Bump python-fullykiosk from 0.0.14 to 0.0.15 (#164511) 2026-03-02 11:42:56 +01:00
Simone Chemelli
6376ba93a7 Bump aioamazondevices to 12.0.2 (#164518) 2026-03-02 11:37:39 +01:00
J. Nick Koston
fd3a1cc9f4 Bump yalexs-ble to 3.2.7 (#164555) 2026-03-02 11:36:05 +01:00
epenet
208013ab76 Move metoffice coordinators to separate module (#164562)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 11:31:57 +01:00
Alex Brown
770b3f910e Fix Matter lock credential slot iteration bound (#164478)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:56:03 +01:00
Norbert Rittel
5dce4a8eda Change one remaining string from "Overseerr" to "Seerr" (#164569) 2026-03-02 10:22:49 +01:00
Jan-Philipp Benecke
6fcc9da948 Fix large WebDAV backup metadata download (#164563)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 10:17:18 +01:00
epenet
bf93580ff9 Migrate modern_forms to runtime_data (#164570) 2026-03-02 10:10:03 +01:00
Jan-Philipp Benecke
0c2fe045d5 Bump aiowebdav2 to 0.6.1 (#164560) 2026-03-02 10:09:33 +01:00
Joost Lekkerkerker
e14a3a6b0e Fix SmartThings EHS power (#164395) 2026-03-02 08:35:37 +01:00
Joost Lekkerkerker
e032740e90 Add time platform to SmartThings (#164451) 2026-03-02 08:34:53 +01:00
Joost Lekkerkerker
78ad1e102d Add binary sensor for full dust bag in SmartThings (#164457) 2026-03-02 08:34:19 +01:00
Joost Lekkerkerker
4f97cc7b68 Add sound detection sensitivity select to SmartThings (#164466) 2026-03-02 08:33:47 +01:00
dependabot[bot]
df8f135532 Bump github/codeql-action from 4.32.3 to 4.32.4 (#164554) 2026-03-02 07:30:23 +01:00
J. Nick Koston
0066801b0f Bump yarl to 1.23.0 (#164542) 2026-03-02 07:22:37 +01:00
Joost Lekkerkerker
0aa66ed6cb Add select for SmartThings driving mode (#164522) 2026-03-01 19:11:58 +01:00
HadiAyache
6903463f14 Fix AccuWeather daily forecast crash when humidity average is missing (#163968)
Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-01 17:19:15 +01:00
Brett Adams
a473010fee Update Tessie quality scale to silver (#164104)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-01 16:53:39 +01:00
Robin Lintermann
ddf7a783a8 Bump smarla quality scale to silver (#164325) 2026-03-01 11:52:11 +01:00
Joost Lekkerkerker
513e4d52fe Add button to reset HEPA filter to SmartThings (#164464) 2026-03-01 07:33:10 +01:00
Klaas Schoute
17bb14e260 Update error handling messages for Powerfox Local integration (#164465) 2026-03-01 07:32:36 +01:00
Brett Adams
cd1258464b Fix OAuth token type narrowing in Teslemetry (#164505) 2026-03-01 07:31:34 +01:00
Allen Porter
d3f5e0e6d7 Update nest access token error handling to use specific OAuth2 token request exceptions (#164506) 2026-03-01 07:26:07 +01:00
Joost Lekkerkerker
e124829364 Rename Overseerr integration to Seerr (#164060) 2026-02-28 23:07:31 +01:00
Jan Bouwhuis
87b83dcc1b Remove the MQTT object_id option after 6 months of deprecation (#164460)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-28 20:12:23 +01:00
Erik Montnemery
be9b47539d Revert "Remove unnecessary volume_up/volume_down overrides from frontier_silicon media player" (#164463) 2026-02-28 20:11:52 +01:00
Joost Lekkerkerker
be6ddc314c Add sound detection switch to SmartThings (#164470) 2026-02-28 20:11:13 +01:00
David Bonnes
c6f8a7b7e4 Harden test of an invalid service call for Evohome (#164458) 2026-02-28 20:10:11 +01:00
Joost Lekkerkerker
53da5612e9 Add fan speed to SmartThings vacuum (#164452)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-28 20:09:43 +01:00
Michael Davie
6cc56b76f9 Bump env-canada to 0.13.2 (#164480)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 20:08:17 +01:00
Tom Matheussen
03cb65d555 Require user code to be set when toggling Satel Integra switches (#164483) 2026-02-28 20:06:56 +01:00
Abílio Costa
73dd024933 Add merged PR count sensor to Github integration (#164405) 2026-02-28 15:13:17 +01:00
Barry vd. Heuvel
1c8c92bf8f Bump weheat to 2026.2.28 (#164456) 2026-02-28 14:40:58 +01:00
Khole
7e041a6759 Hive - Bump pyhive-integration to v1.0.8 (#164453) 2026-02-28 12:32:37 +00:00
Alex Brown
ee05f14530 Add Matter lock user and credential management services (#161936)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-28 10:43:09 +01:00
Simone Chemelli
f0ba5178b7 Fix RpcSensorDescription for Shelly (#150719) 2026-02-28 09:28:53 +01:00
Denis Shulyaka
df51ac932b Improve Anthropic service exceptions (#164418)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-28 09:20:17 +01:00
Paulus Schoutsen
e96b5f2eb1 Remove unnecessary volume_up/volume_down overrides from mpd media player (#164428)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-28 09:16:53 +01:00
Paulus Schoutsen
4e59c89327 Remove unnecessary volume_up/volume_down overrides from bluesound media player (#164426)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-28 08:57:53 +01:00
Paulus Schoutsen
15676021a9 Remove unnecessary volume_up/volume_down overrides from demo media player (#164424)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-28 08:57:30 +01:00
Paulus Schoutsen
d3197a0d1e Remove unnecessary volume_up/volume_down overrides from aquostv media player (#164431)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-28 08:56:09 +01:00
Paulus Schoutsen
35692b335c Remove unnecessary volume_up/volume_down overrides from frontier_silicon media player (#164430)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-28 08:49:47 +01:00
Paulus Schoutsen
cc5c810501 Remove unnecessary volume_up/volume_down overrides from NADtcp media player (#164434)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-28 08:47:08 +01:00
Paulus Schoutsen
f2681f2dc8 Remove unnecessary volume_up/volume_down overrides from monoprice media player (#164429)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-28 08:45:43 +01:00
Brett Adams
fe0a22c790 Complete strict typing for Teslemetry integration (#164416) 2026-02-28 08:33:45 +01:00
Norman Yee
186ab50458 Bump govee-ble to 1.2.0 (#164438) 2026-02-28 08:24:38 +01:00
mettolen
b524c40176 Remove error translation placeholders from Airobot (#164436) 2026-02-28 06:18:19 +01:00
Klaas Schoute
642864959a Update translatable exceptions for Powerfox integration (#164322) 2026-02-28 01:57:02 +00:00
Franck Nijhof
7ef6c34149 Reject relative paths in SFTP storage backup location config flow (#164408) 2026-02-27 19:25:04 -05:00
Franck Nijhof
5b32e42b8c Add aioclient_mock to ssdp tests to prevent real HTTP requests (#164403) 2026-02-27 19:24:13 -05:00
Franck Nijhof
1be8b8e525 Add discovery mocks to tplink init tests (#164386) 2026-02-27 19:23:47 -05:00
Franck Nijhof
3fae15c430 Fix fixture ordering in esphome dashboard tests (#164367) 2026-02-27 19:23:13 -05:00
Franck Nijhof
c7e78568d0 Enable real sockets in default_config setup test (#164366) 2026-02-27 19:22:29 -05:00
Stefan Agner
492b542136 Fix Matter vacuum crash on nullable ServiceArea location info (#164411) 2026-02-28 00:11:32 +01:00
Franck Nijhof
0f4852d8c2 Enable sockets for http integration tests (#164404) 2026-02-27 22:22:15 +01:00
nopoz
737c0c1823 Google Cast: detect state and attributes when device is doing active non-media casting (#160819)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-02-27 22:07:09 +01:00
Petro31
5fadcb01e9 Fix int vs float template sensor issue (#164339) 2026-02-27 22:06:37 +01:00
TheJulianJES
2b4f46a739 Fix ZHA update entities not working after reload (#164290) 2026-02-27 22:04:51 +01:00
Franck Nijhof
44fe37da1f Mock ConnectionContextBuilder in homematicip_cloud tests (#164356) 2026-02-27 22:00:37 +01:00
Joost Lekkerkerker
abd4e89577 Sync SmartThings vacuum fixture (#164360) 2026-02-27 21:43:30 +01:00
Franck Nijhof
033798835a Refactor adguard tests to use proper fixtures for mocking (#164402)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-27 21:34:10 +01:00
Franck Nijhof
83c77957c1 Add missing mock fixtures to telegram_bot polling init test (#164398) 2026-02-27 21:29:10 +01:00
dependabot[bot]
b1bc1dc102 Bump actions/dependency-review-action from 4.8.2 to 4.8.3 (#164296)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 21:21:15 +01:00
Jason Hunter
40b8a2c380 Remove Duke Energy (#164282)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-02-27 20:19:03 +00:00
Glenn de Haan
fb23a6fbf8 Add HDFury audio offset numbers (#164315) 2026-02-27 21:02:34 +01:00
Joost Lekkerkerker
faad3de02c Bump pySmartThings to 3.6.0 (#164397) 2026-02-27 21:00:33 +01:00
Franck Nijhof
5f30f532e5 Mock async_setup_entry in unifiprotect reauth tests (#164375) 2026-02-27 20:53:52 +01:00
Franck Nijhof
667e8c4d38 Mock async_setup_entry in jvc_projector config flow tests (#164401) 2026-02-27 20:53:38 +01:00
Franck Nijhof
74240ecd26 Mock async_setup_entry in lametric DHCP discovery test (#164400) 2026-02-27 20:50:11 +01:00
Franck Nijhof
c81ee53265 Mock TodoistAPIAsync in todoist failed coordinator update test (#164390) 2026-02-27 20:49:02 +01:00
Franck Nijhof
8835f1d5e6 Mock async_setup_entry in youless config flow test (#164399) 2026-02-27 20:46:48 +01:00
Franck Nijhof
2ca84182d8 Patch discovery in elkm1 invalid auth and reconfigure tests (#164396) 2026-02-27 20:46:45 +01:00
Franck Nijhof
3f0d1bc071 Mock PyMochad controller in mochad tests (#164394) 2026-02-27 20:43:08 +01:00
Franck Nijhof
350f462bdf Prevent real setup during DHCP discovery test in fully_kiosk tests (#164342) 2026-02-27 20:42:32 +01:00
Franck Nijhof
2f98e68ed8 Mock async_setup_entry in arcam_fmj config flow tests (#164351) 2026-02-27 20:42:12 +01:00
Franck Nijhof
5b7fac94e5 Mock async_setup_entry in ccm15 config flow tests (#164352) 2026-02-27 20:42:02 +01:00
Franck Nijhof
c32ce3da5c Add missing rest_api fixture in samsungtv setup test (#164353) 2026-02-27 20:41:39 +01:00
Franck Nijhof
0e1d1fbaed Fix fixture ordering in jvc_projector integration setup (#164354) 2026-02-27 20:41:17 +01:00
Franck Nijhof
57d7f364f4 Mock async_setup_entry in wilight SSDP flow test (#164393) 2026-02-27 20:40:35 +01:00
Franck Nijhof
7cc5777b47 Fix fixture ordering in madVR tests to ensure proper mocking (#164350) 2026-02-27 20:38:42 +01:00
Franck Nijhof
5e3f23b6a2 Fix mock target for Met Office config flow error test (#164391) 2026-02-27 20:37:24 +01:00
Franck Nijhof
6873a40407 Mock async_setup_entry in forked_daapd config flow tests (#164370) 2026-02-27 20:36:33 +01:00
Franck Nijhof
ddaa2fb293 Mock async_setup_entry in daikin config flow tests (#164371) 2026-02-27 20:36:23 +01:00
Franck Nijhof
53b6223459 Mock async_setup_entry in emulated_roku config flow tests (#164368) 2026-02-27 20:35:50 +01:00
Franck Nijhof
7329cfb927 Mock async_setup_entry in home_connect migration tests (#164357) 2026-02-27 20:33:54 +01:00
Franck Nijhof
44b80dde0c Mock async_setup_entry in radarr config flow tests (#164359) 2026-02-27 20:33:19 +01:00
Joost Lekkerkerker
8c125e4e4f Add do not disturb switch to SmartThings (#164364) 2026-02-27 20:31:56 +01:00
Franck Nijhof
227a258382 Add missing client mocks to tplink_omada service tests (#164389) 2026-02-27 20:30:54 +01:00
Franck Nijhof
addc2a6766 Mock async_setup_entry in speedtestdotnet config flow test (#164387) 2026-02-27 20:30:47 +01:00
Franck Nijhof
97bcea9727 Mock async_setup_entry in tautulli config flow tests (#164388) 2026-02-27 20:30:38 +01:00
Franck Nijhof
4f05c807b0 Mock async_setup_entry in panasonic_viera config flow tests (#164385) 2026-02-27 20:30:25 +01:00
Franck Nijhof
177a918c26 Mock async_setup_entry in onvif DHCP host update test (#164384) 2026-02-27 20:30:15 +01:00
Franck Nijhof
9705770c6c Remove unnecessary config entry from velux validation error test (#164383) 2026-02-27 20:30:12 +01:00
Franck Nijhof
7309351165 Mock async_setup_entry in lunatone config flow tests (#164382) 2026-02-27 20:29:22 +01:00
Franck Nijhof
d0401de70d Mock HMConnection in homematic notify tests (#164381) 2026-02-27 20:29:14 +01:00
Franck Nijhof
6b89359a73 Mock async_setup_entry in sharkiq setup test (#164380) 2026-02-27 20:27:40 +01:00
Franck Nijhof
b31bafab99 Mock async_setup_entry in roku options flow test (#164377) 2026-02-27 20:27:13 +01:00
Franck Nijhof
84c556bb63 Mock setup and client in sma config flow tests (#164374) 2026-02-27 20:26:59 +01:00
Franck Nijhof
225ea02d9a Fix axis setup failure test to mock at correct layer (#164373) 2026-02-27 20:26:46 +01:00
Franck Nijhof
ebd1cc994c Add missing mock_transmission_client to transmission init tests (#164369) 2026-02-27 20:26:33 +01:00
Franck Nijhof
9ec22ba158 Mock async_setup_entry in kostal_plenticore reconfigure test (#164372) 2026-02-27 20:26:18 +01:00
Paulus Schoutsen
2ff85d2134 Add missing volume supported features to dunehd (#164343)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 19:50:42 +01:00
reneboer
3eb7f04510 Add tests for Megane e-Tech (#164358)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-27 19:47:22 +01:00
Kamil Breguła
54613ac8d9 Add mik-laj as codeowner to WLED (#164349)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
2026-02-27 18:31:37 +01:00
Joost Lekkerkerker
044522a8ab Add state for washing mop in SmartThings (#164348) 2026-02-27 18:26:20 +01:00
Willem-Jan van Rootselaar
19bf41496a Set entity_registry_enabled_default to False for total energy sensor (#164197) 2026-02-27 18:03:17 +01:00
Johnny Willemsen
a7efba098d Update state labels to use common keys in indevolt (#164308) 2026-02-27 17:57:02 +01:00
Arie Catsman
042ad3b759 Add missing production ct data, total-consumption and new CT to enphase_envoy (#164270) 2026-02-27 17:43:46 +01:00
Franck Nijhof
4270e4c793 Mock firmware data during reauth flow init in airos tests (#164341) 2026-02-27 17:21:22 +01:00
Erwin Douna
cb11c22e76 SMA add data descriptions (#164331) 2026-02-27 16:34:45 +01:00
Norbert Rittel
c6e23fec93 Replace "service" with "action" in evohome exception string (#164333) 2026-02-27 16:32:15 +01:00
epenet
553cecb397 Ensure future is marked as retrieved in frontend storage (#164320) 2026-02-27 15:51:34 +02:00
Erwin Douna
bb7d5897d1 Portainer redact CONF_HOST in diagnostics (#164301) 2026-02-27 13:54:12 +01:00
7eaves
3e050ebe59 Bump PySwitchBot to 1.1.0 (#164298) 2026-02-27 13:11:14 +01:00
Ye Zhiling
856a9e695a Pass encoding to AtomicWriter in write_utf8_file_atomic (#164015) 2026-02-27 11:40:58 +01:00
Artur Pragacz
1944a8bd3a Remove vacuum area mapping not configured issue (#164259) 2026-02-27 11:20:46 +01:00
epenet
3f11af8084 Drop single-use service name constants in bsblan (#164311) 2026-02-27 10:59:02 +01:00
David Bonnes
46a87cd9dd Migrate evohome's zone services to entity-level services (#164105)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-02-27 10:16:35 +01:00
Erwin Douna
f8a657cf01 Proxmox expand data descriptions (#164304) 2026-02-27 09:59:43 +01:00
Norbert Rittel
75ed7b2fa2 Improve descriptions of schlage actions (#164299) 2026-02-27 08:46:08 +01:00
hanwg
e63e54820c Remove redundant exception messages from Telegram bot (#164289) 2026-02-27 08:19:10 +01:00
Kamil Breguła
37d2c946e8 Add diagnostics platform to AWS S3 (#164118)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2026-02-27 08:16:58 +01:00
James
e8a35ea69d Handle missing Daikin zone temperature keys (#164170)
Co-authored-by: barneyonline <barneyonline@users.noreply.github.com>
2026-02-26 22:15:55 +00:00
Erwin Douna
28b950c64a Simplify entity init in Proxmox (#164265) 2026-02-26 21:26:29 +01:00
Denis Shulyaka
e7cf6cbe72 Create reauth flow for Anthropic for auth errors during conversation (#164267)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-26 21:16:11 +01:00
Raphael Hehl
5ad71453b8 Bump uiprotect to version 10.2.2 (#164269)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-02-26 21:12:30 +01:00
Andrew Grimberg
ab9c8093c3 Add services for managing Schlage door codes (#151014)
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-02-26 20:54:57 +01:00
peteS-UK
51acdeb563 Add config flow support to Orvibo legacy integration (#155115)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-26 19:59:13 +01:00
Johnny Willemsen
bf60d57cc2 Update state labels to use common keys in compit (#164261) 2026-02-26 18:56:11 +01:00
Kamil Breguła
d94f15b985 Update IQS for AWS S3 (#164117)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
2026-02-26 18:54:04 +01:00
Erwin Douna
8a621e6570 Remove kw arg for Portainer (#164260) 2026-02-26 18:43:56 +01:00
Bram Kragten
dd44b15b7b Update frontend to 20260226.0 (#164262) 2026-02-26 18:42:48 +01:00
epenet
23ec28bbbf Simplify portainer entity initialisation (#164256) 2026-02-26 17:00:35 +01:00
epenet
7a6a479b53 Rename local constants in device_automation test (#164143) 2026-02-26 16:41:02 +01:00
epenet
f9ffaad7f1 Drop single-use service name constants in abode (#164146) 2026-02-26 16:40:43 +01:00
epenet
d4aa52ecc3 Drop single-use service name constants in alarmdecoder (#164150) 2026-02-26 16:40:28 +01:00
epenet
1b5eea5fae Drop single-use service name constants in amberelectric (#164152) 2026-02-26 16:40:13 +01:00
epenet
39dce8eb31 Drop single-use service name constants in androidtv (#164153) 2026-02-26 16:39:53 +01:00
epenet
b651e62c7f Drop single-use service name constants in advantage_air (#164148) 2026-02-26 16:39:32 +01:00
Denis Shulyaka
1e807dc9da Update reasoning options for gpt-5.3-codex (#164179) 2026-02-26 16:39:04 +01:00
epenet
cba69e7e69 Drop single-use service name constants in agent_dvr (#164149) 2026-02-26 16:37:58 +01:00
Denis Shulyaka
802a7aafec Disable code interpreter with minimal reasoning for OpenAI (#164254) 2026-02-26 16:37:31 +01:00
Joost Lekkerkerker
db5e7b3e3b Remove invalid color mode from philips_js (#164204) 2026-02-26 16:33:35 +01:00
Erwin Douna
75798bfb5e Fix stack devices merging with container devices in Portainer (#164135)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-02-26 16:14:50 +01:00
Kevin Stillhammer
06a25de0d5 Remove redundant DEFAULT_TIME_DELTA in waze_travel_time (#164227) 2026-02-26 15:43:16 +01:00
AlCalzone
892da4a03e Rename "Z-Wave Supervisor app" to "Z-Wave JS app" (#164147) 2026-02-26 15:38:03 +01:00
epenet
91e8e3da7a Use constants in default_config tests (#164144) 2026-02-26 15:31:48 +01:00
Kevin Stillhammer
144b8768a1 Add time_delta option to waze_travel_time (#161803) 2026-02-26 14:28:05 +01:00
Brett Adams
cb6d86f86d Add energy price calendar platform to Teslemetry (#145848)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-26 13:42:45 +01:00
epenet
422007577e Use constant in diagnostics test (#164139) 2026-02-26 12:58:21 +01:00
Norbert Rittel
7c2904bf48 Replace "add-ons" with "apps" in backup issues (#164129) 2026-02-26 12:57:09 +01:00
epenet
3240fd7fc8 Drop single-use service name constants in amcrest (#164156) 2026-02-26 12:54:21 +01:00
epenet
7dc2dff4e7 Drop single-use service name constants in alexa_devices (#164151) 2026-02-26 12:53:18 +01:00
Abílio Costa
7e8de9bb9c Add infrared entity integration (#162251)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2026-02-26 11:45:21 +00:00
Luca Angemi
9eff12605c Add minimum state duration variable to history_stats (#151643) 2026-02-26 11:21:37 +01:00
Erik Montnemery
784ac85759 Require full coverage for backup platforms (#164137) 2026-02-26 11:16:32 +01:00
Amit Finkelstein
31f7961437 Add HassOS "mount_reload" action (#155996)
Co-authored-by: Shay Levy <levyshay1@gmail.com>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-02-26 08:04:58 +01:00
mettolen
eaae64fa12 Remove error translation placeholders from Saunum (#164121) 2026-02-26 07:44:19 +01:00
Paulus Schoutsen
88b276f3a4 Simplify Anthropic integration name (#164124)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-26 07:43:44 +01:00
Kamil Breguła
f5c996e243 Add support for S3 prefix in AWS S3 integration (#162836)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-26 07:39:50 +01:00
Michael Hansen
4863df00a1 Avoid invalid cache future state (#164081) 2026-02-25 22:36:53 -05:00
Maciej Bieniek
9fadfecf14 Bump accuweather to 5.1.0 (#164034) 2026-02-26 02:00:10 +01:00
Liquidmasl
dae7f73f53 Sonarr post merge changes (#164112) 2026-02-26 01:57:14 +01:00
Jamie Magee
c46d0382c3 Add diagnostics to aladdin_connect for easier troubleshooting (#164110) 2026-02-26 00:17:38 +01:00
Artur Pragacz
c21e9cb24c Fix Matter vacuum clean area status check (#164108) 2026-02-25 23:49:14 +01:00
David Bonnes
928732af40 Clean up evohome constants (#164102) 2026-02-25 20:23:17 +00:00
Franck Nijhof
51dc6d7c26 Bump version to 2026.4.0dev0 (#164101) 2026-02-25 21:08:17 +01:00
Przemko92
02972579aa Add Compit fan (#164049)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-25 20:52:01 +01:00
Denis Shulyaka
80574f7ae0 Change icon for Anthropic entities to mdi:asterisk (#164099)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-25 20:33:33 +01:00
Klaas Schoute
390b62551d Add PowerfoxPrivacyError handling for Powerfox integration (#164100) 2026-02-25 20:28:56 +01:00
Denis Shulyaka
17e0fd1885 Add Code execution tool to Anthropic (#164065) 2026-02-25 20:01:34 +01:00
Brett Adams
4eb3e77891 Remove redundant get_status call from Tessie coordinator (#163219)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 19:58:35 +01:00
Willem-Jan van Rootselaar
324ed65999 add codeowner to homevolt (#164097) 2026-02-25 19:46:41 +01:00
Maikel Punie
42428b91bb Bump velbusaio to 2026.2.0 (#164093) 2026-02-25 19:41:17 +01:00
Glenn de Haan
c41dd3e3a8 Bump hdfury to 1.6.0 (#164088) 2026-02-25 19:40:11 +01:00
Joost Lekkerkerker
02171a1da0 Add Zinvolt power sensor (#164092) 2026-02-25 18:58:25 +01:00
konsulten
19c7f663ca Add diagnostic to systemnexa2 integration (#164090) 2026-02-25 18:51:51 +01:00
Matthias Alphart
87bd04af5a Update knx-frontend to 2026.2.25.165736 (#164089) 2026-02-25 18:50:21 +01:00
Jamie Magee
5af6227ad7 Add action exceptions for cover commands in aladdin_connect (#164087) 2026-02-25 18:45:04 +01:00
Robert Resch
9b56f936fd Bump uv to 0.10.6 (#164086) 2026-02-25 18:36:07 +01:00
Joost Lekkerkerker
f2afd324d9 Make Zinvolt battery state a non diagnostic sensor (#164071) 2026-02-25 18:22:23 +01:00
Joost Lekkerkerker
173aab5233 Refresh coordinator in Zinvolt after setting value (#164069) 2026-02-25 18:19:58 +01:00
Joost Lekkerkerker
1d97729547 Use different name source in Zinvolt (#164072) 2026-02-25 18:18:52 +01:00
konsulten
91ca674a36 Add sensor platform to systemnexa2 (#163961) 2026-02-25 18:18:12 +01:00
Joost Lekkerkerker
6157802fb5 Set initiate flow for Zinvolt (#164054) 2026-02-25 18:18:10 +01:00
Joost Lekkerkerker
7e3b7a0c02 Add integration_type device to zerproc (#163998) 2026-02-25 18:17:56 +01:00
Joost Lekkerkerker
6a5455d7a5 Add integration_type device to wiffi (#163978) 2026-02-25 18:17:23 +01:00
Kamil Breguła
09765fe53d Fix AWS S3 config flow endpoint URL validation (#164085)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
2026-02-25 18:17:04 +01:00
Felix Eckhofer
2fccbd6e47 dwd_weather_warnings: Filter expired warnings (#163096) 2026-02-25 18:16:44 +01:00
Jamie Magee
ef7cccbe3f Handle coordinator update errors in aladdin_connect (#164084) 2026-02-25 18:15:40 +01:00
Jamie Magee
a704c2d44b Add parallel updates to aladdin_connect (#164082) 2026-02-25 18:06:43 +01:00
Robert Resch
f12c5b627d Remove building wheels for Python 3.13 (#164083) 2026-02-25 18:05:32 +01:00
Bram Kragten
b241054a96 Update frontend to 20260225.0 (#164076) 2026-02-25 17:55:00 +01:00
Erik Montnemery
0fd515404d Fix smarla test snapshots (#164078) 2026-02-25 17:50:06 +01:00
Erik Montnemery
52382b7fe5 Fix ntfy test snapshots (#164079) 2026-02-25 17:49:46 +01:00
Thomas D
209af5dccc Adjust service description for Volvo integration (#164073) 2026-02-25 17:46:34 +01:00
Liquidmasl
227d2e8de6 Sonarr coordinator refactor (#164077)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-25 17:46:18 +01:00
Erwin Douna
96d50565f9 Portainer optimize switch (#163520)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Robert Resch <robert@resch.dev>
2026-02-25 17:39:49 +01:00
Tom
80fc3691d8 Align airOS add_entities consumption in sensor (#164055)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-25 17:25:51 +01:00
Christian Lackas
15e00f6ffa Add siren support for HmIP-MP3P (Combination Signalling Device) (#161634)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-25 17:16:56 +01:00
Brett Adams
f25b437832 Add quality scale to Tessie integration (#160499)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Tom <CoMPaTech@users.noreply.github.com>
2026-02-25 17:10:41 +01:00
Franck Nijhof
2e34d4d3a6 Add brands system integration to proxy brand images through local API (#163960)
Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-25 17:10:28 +01:00
Liquidmasl
b81b12f094 Sonarr service calls instead of sensor attributes (#161199)
Co-authored-by: Joostlek <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-25 17:09:06 +01:00
Erwin Douna
7446d5ea7c Add reconfigure flow to Fully Kiosk (#161840) 2026-02-25 17:08:43 +01:00
Matt Zimmerman
7b811cddce Use has_entity_name in SmartTub entities (#162374)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-25 16:45:48 +01:00
Paul Bottein
19545f29dc Use show in sidebar property instead of removing panel title and icon (#164025) 2026-02-25 16:37:15 +01:00
Jamie Magee
e591291cbe Add platform tests for aladdin_connect cover and sensor (#164011) 2026-02-25 16:20:19 +01:00
Joost Lekkerkerker
cb990823cd Improve platforms pylint plugin (#164067) 2026-02-25 16:15:28 +01:00
Willem-Jan van Rootselaar
2cfafc04ce Bump python-bsblan to 5.1.0 (#164064) 2026-02-25 15:57:07 +01:00
Ludovic BOUÉ
0563037c5a Fix MatterValve state handling and allow None values for attributes (#164066) 2026-02-25 15:57:05 +01:00
Joost Lekkerkerker
70f5f2c1ee Add binary sensor platform to Zinvolt (#164050) 2026-02-25 15:38:53 +01:00
Robin Lintermann
c5b31d6782 Add Update Platform to Smarla Integration (#163255) 2026-02-25 15:36:48 +01:00
Joost Lekkerkerker
925bcea1c0 Add number platform to Zinvolt (#164058) 2026-02-25 15:30:45 +01:00
Manu
01f0e4fe48 Add update platform to ntfy integration (#164018)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-25 15:28:47 +01:00
mettolen
f9a61e5412 Mark docs-examples done for Liebherr integration (#163034) 2026-02-25 15:26:08 +01:00
Andreas Jakl
caf40f9d25 Add diagnostics to NRGkick integration (#164047) 2026-02-25 15:20:34 +01:00
Manu
89c5511558 Improve configuration url in Uptime Kuma (#164057)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-25 15:02:05 +01:00
Joost Lekkerkerker
fc79e0cbfa Bump zinvolt to 0.3.0 (#164046) 2026-02-25 14:56:21 +01:00
Thomas D
317f95ff0f Add a service to retrieve images for the Volvo integration (#159603)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-02-25 14:41:03 +01:00
Manu
0cb34d2888 Categorize update entity as diagnostic in Uptime Kuma (#164022) 2026-02-25 14:14:03 +01:00
Manu
b8df61fc5f Categorize update entity as diagnostic in IronOS integration (#164023) 2026-02-25 14:13:40 +01:00
epenet
44a4be012d Use constants in counter tests (#164020) 2026-02-25 14:13:24 +01:00
Joost Lekkerkerker
8dcaed62b5 Add base entity to Zinvolt (#164051) 2026-02-25 14:12:32 +01:00
epenet
195e55097b Drop single-use service name constants in Renault (#164043) 2026-02-25 13:16:20 +01:00
Tom Quist
910f501194 Fix ingress compression breaking SSE and streaming responses (#160704) 2026-02-25 12:58:12 +01:00
kang
f0edfbf053 Enrich DeviceInfo with meter metadata in route_b_smart_meter (#164006)
Co-authored-by: Robert Resch <robert@resch.dev>
2026-02-25 11:49:52 +01:00
epenet
834227a762 Use constants in calendar test (#164021) 2026-02-25 10:51:58 +01:00
Ludovic BOUÉ
3426846361 Add CLEAN_AREA feature to Matter vacuum entity (#163570)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
Co-authored-by: Artur Pragacz <artur@pragacz.com>
2026-02-25 10:47:47 +01:00
Artur Pragacz
50f39621e9 Add vacuum area mapping not configured issue (#163965) 2026-02-25 10:45:44 +01:00
epenet
dc133bf7cc Move Tuya helpers to external library (#158791) 2026-02-25 10:35:12 +01:00
TheJulianJES
3219417a7d Bump ZHA to 1.0.0 (#164013) 2026-02-25 09:51:30 +01:00
Joost Lekkerkerker
9a23a518ed Add integration_type device to ws66i (#163987) 2026-02-25 09:50:16 +01:00
Joost Lekkerkerker
7e62852723 Add integration_type hub to watts (#163973) 2026-02-25 09:45:33 +01:00
Zhephyr
0a1027391f Add pet last seen flap device id and user id sensors to Sure Petcare (#160215) 2026-02-25 08:59:22 +01:00
Allen Porter
7644fc4325 Update MCP client integration to use new OAuth spec (#161611)
Co-authored-by: Robert Resch <robert@resch.dev>
2026-02-24 23:18:25 -08:00
Yangqian Yan
2f80720730 Add Full support for roborock Zeo washing/drying machines (#159575)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-24 23:17:56 -08:00
Joost Lekkerkerker
644c74f311 Add integration_type hub to zwave_me (#164000) 2026-02-25 07:29:36 +01:00
Joost Lekkerkerker
29370add66 Add integration_type service to zamg (#163997) 2026-02-25 07:28:32 +01:00
Joost Lekkerkerker
fc4680ad86 Add integration_type device to youless (#163996) 2026-02-25 07:28:11 +01:00
Joost Lekkerkerker
174076ba76 Add integration_type hub to yolink (#163995) 2026-02-25 07:27:37 +01:00
Joost Lekkerkerker
f3590bd9cf Add integration_type device to yeelight (#163994) 2026-02-25 07:27:08 +01:00
Joost Lekkerkerker
ae7f71219f Add integration_type device to yardian (#163993) 2026-02-25 07:26:27 +01:00
Joost Lekkerkerker
e1529620db Add integration_type hub to yale (#163989) 2026-02-25 07:25:20 +01:00
Joost Lekkerkerker
9a56d30924 Add integration_type hub to yale_smart_alarm (#163990) 2026-02-25 07:24:54 +01:00
Joost Lekkerkerker
d6df2b3c4c Add integration_type device to yamaha_musiccast (#163992) 2026-02-25 07:24:28 +01:00
Joost Lekkerkerker
9740dc65aa Add integration_type device to yalexs_ble (#163991) 2026-02-25 07:23:47 +01:00
Joost Lekkerkerker
b914971531 Add integration_type device to wolflink (#163982) 2026-02-25 07:23:14 +01:00
Joost Lekkerkerker
9007c65b50 Add integration_type hub to wilight (#163979) 2026-02-25 07:22:35 +01:00
Joost Lekkerkerker
a4a2847b03 Add integration_type hub to weheat (#163977) 2026-02-25 07:21:04 +01:00
Joost Lekkerkerker
9a11db2ad5 Add integration_type service to weatherkit (#163976) 2026-02-25 07:20:32 +01:00
Joost Lekkerkerker
2d445f8f53 Add integration_type hub to weatherflow_cloud (#163975) 2026-02-25 07:20:06 +01:00
Joost Lekkerkerker
f07c386529 Add integration_type device to watergate (#163972) 2026-02-25 07:18:54 +01:00
Joost Lekkerkerker
3cd79581dc Add integration_type hub to zimi (#163999) 2026-02-25 07:07:50 +01:00
Joost Lekkerkerker
e82df86dda Add integration_type hub to xiaomi_aqara (#163988) 2026-02-25 07:07:25 +01:00
Joost Lekkerkerker
1629d2b204 Add integration_type service to worldclock (#163986) 2026-02-25 07:07:05 +01:00
Joost Lekkerkerker
a6e60d8b73 Add integration_type hub to withings (#163980) 2026-02-25 07:06:45 +01:00
Joost Lekkerkerker
ef6650548e Add integration_type service to waze_travel_time (#163974) 2026-02-25 07:06:14 +01:00
Manu
52a2e94fc4 Bump aiontfy to 0.8.1 (#164010) 2026-02-25 07:05:36 +01:00
Klaas Schoute
6bba7e7583 Bump powerfox to v2.1.1 (#164004) 2026-02-25 02:14:27 +01:00
MizterB
58e8a8d398 Ecobee username/password authentication (#161716)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-02-25 01:41:36 +01:00
Klaas Schoute
6b0303a1ef Set quality scale to platinum for Powerfox Local integration (#164003) 2026-02-25 01:22:27 +01:00
Klaas Schoute
249e6c2f3d Add reconfiguration flow for Powerfox Local integration (#164002) 2026-02-25 01:05:30 +01:00
Simone Chemelli
7ae0380b33 Update IQS to gold for UptimeRobot (#162926) 2026-02-25 01:05:17 +01:00
Tom
889faa5a5c Add v6 firmware support to airOS (#163889)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-02-25 01:02:26 +01:00
Klaas Schoute
9b810c64d9 Add diagnostics support for Powerfox Local integration (#163985) 2026-02-25 00:24:33 +01:00
Joost Lekkerkerker
1e3bed9864 Add integration_type device to wiz (#163981) 2026-02-25 00:04:34 +01:00
Tom
eac3fb651e Update airOS quality_scale (#163895)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-24 23:47:18 +01:00
Karl Beecken
8b285239f0 Update Teltonika IQS to silver (#163943) 2026-02-24 23:37:46 +01:00
Andreas Jakl
d0a74ad539 Update quality scale to silver for nrgkick integration (#163964) 2026-02-24 23:35:06 +01:00
Andreas Jakl
0f071c1ae5 Fix accessing optional username and password for nrgkick integration (#163963) 2026-02-24 23:33:40 +01:00
mettolen
e671e4408b Implement dynamic devices for Liebherr integration (#163951)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-24 23:32:51 +01:00
Klaas Schoute
697441969b Add reauthentication flow for Powerfox Local integration (#163966) 2026-02-24 23:29:19 +01:00
nic
bc324a1a6e Add ZoneMinder integration test suite (#163115)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-02-24 23:27:13 +01:00
Robin Lintermann
e505ad9003 Update availability of entities when connection changes (#163252) 2026-02-24 23:25:57 +01:00
Jan Čermák
6a91771f04 Use native ARM runner for builder action, update to builder 2026.02.1 (#163942) 2026-02-24 23:14:50 +01:00
Christian Lackas
e7df4356f4 Fix HmIP-RGBW monochrome mode FEATURE_NOT_SUPPORTED error (#161917) 2026-02-24 22:38:47 +01:00
Luke Lashley
a41207d369 Implement changes for Clean area for Roborock. (#163956) 2026-02-24 22:34:56 +01:00
cdheiser
28e8d7c3eb Add tests to lutron (#162055)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-02-24 22:30:31 +01:00
mettolen
e514faf0bc Fix Saunum session parameters to use timedelta (#163962) 2026-02-24 22:14:09 +01:00
Erwin Douna
7894a80728 Proxmox separate errors and patch tests (#163922) 2026-02-24 22:08:50 +01:00
Przemko92
6751f6f4a2 Add sensor for compit integration (#161527) 2026-02-24 21:49:47 +01:00
Erwin Douna
ce0dd0eb7b Fix small typo in Portainer containers (#163957) 2026-02-24 21:45:34 +01:00
Erwin Douna
7cb595f768 Add sensor platform to Proxmox (#163404) 2026-02-24 21:45:07 +01:00
Kamil Breguła
dfbd4ffb2d Add diagnostics to met (#157805)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-24 21:34:54 +01:00
Willem-Jan van Rootselaar
6abefc852d Add quality scale to bsblan integration (#146323)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-24 21:30:41 +01:00
konsulten
9ba28150e9 Add light platform to systemnexa2 (#163710)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-24 21:29:46 +01:00
Erwin Douna
adfe4f2b62 Add stack management to Portainer (#163612)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-24 21:28:16 +01:00
Christian Lackas
dc3dc116d2 Handle 403 authentication errors in HomematicIP Cloud (#162579) 2026-02-24 21:21:29 +01:00
Willem-Jan van Rootselaar
f16e7aaec4 bugfix tests to use model_validate_json for device time (#163950) 2026-02-24 21:20:03 +01:00
A. Gideonse
ea68152f32 Add select platform to Indevolt integration (#163955) 2026-02-24 21:18:43 +01:00
wollew
c75c9d9dd8 Add diagnostics to Velux integration (#163896) 2026-02-24 21:17:56 +01:00
rlippmann
4760f9b8eb Restart SimpliSafe websocket after request failures (#160974)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-02-24 21:11:12 +01:00
Denis Shulyaka
9bb879e061 Fix API key check during config flow for openai_conversation (#163025) 2026-02-24 20:53:19 +01:00
Blake Messer
f2c87f96a2 Bump pyrainbird to 6.1.0 (#163919) 2026-02-24 20:48:33 +01:00
Denis Shulyaka
30fffafceb Add STT support for OpenAI (#162931)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-02-24 19:32:13 +01:00
Thomas55555
ff916a783b Disable seconds in Husqvarna Automower services (#163948) 2026-02-24 19:24:10 +01:00
Maciej Bieniek
0fcfc3f070 Bump imgw_pib to 2.0.2 (#163940) 2026-02-24 19:15:41 +01:00
Przemko92
413506276c Add binary sensor for Compit (#161709) 2026-02-24 18:58:15 +01:00
Willem-Jan van Rootselaar
4a4e077d40 Add button platform for BSB-Lan integration (#160243) 2026-02-24 18:52:33 +01:00
Robin Lintermann
8f824b566e Add reauthentication flow to smarla (#163250) 2026-02-24 18:52:03 +01:00
Willem-Jan van Rootselaar
610aaa6eee Update BSB-LAN strings, error handling, and code cleanup (#163480) 2026-02-24 18:09:32 +01:00
Martin Arndt
ecb7ab238c Allow worxlandroid PIN to contain letters (#163266) 2026-02-24 18:07:15 +01:00
Simone Chemelli
9013b7835e Resolve pylance complaints for Fritz (#163313) 2026-02-24 18:06:19 +01:00
Erwin Douna
5363638c7e OAuth helper enhance response text logger (#163371)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-02-24 16:50:40 +01:00
Andreas Jakl
164b1cbb8c Add reconfiguration flow to NRGkick (#163828) 2026-02-24 16:46:23 +01:00
Mattias Michaux
b5a55ec032 Fix Sonos browse album art lookup for multi-segment A:ALBUM IDs (#163786) 2026-02-24 16:45:27 +01:00
Karl Beecken
0c6d635e83 Teltonika quality scale: mark unavailable rules done (#163705) 2026-02-24 16:43:48 +01:00
Christian Lackas
9259db0b85 Centralize ViCare error handling in base entity class (#162619) 2026-02-24 16:43:16 +01:00
Denis Shulyaka
6f1a021197 Add IQS to Anthropic (#163891) 2026-02-24 16:27:51 +01:00
Christian Lackas
8dbf7f7ad7 Add diagnostics support to homematicip_cloud (#163829) 2026-02-24 16:25:04 +01:00
Jamie Magee
3854c8e261 Econet friedrich support (#163904)
Co-authored-by: w1ll1am23 <6432770+w1ll1am23@users.noreply.github.com>
2026-02-24 16:20:35 +01:00
On Freund
7adfb0a40b Add bus support to MTA integration (#163220)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-02-24 16:11:13 +01:00
Zoltán Farkasdi
b4705e4a45 Fix flaky netatmo test (#163941) 2026-02-24 16:02:00 +01:00
Tom
a0176d18cf Add DHCP ip_addresses update to airOS (#163936) 2026-02-24 15:36:52 +01:00
Kevin Stillhammer
5543107f6c Allow to disable seconds in DurationSelector (#163803) 2026-02-24 15:11:26 +01:00
Klaas Schoute
6dc8840932 Rename Powerfox integration to Powerfox Cloud (#163723) 2026-02-24 14:42:43 +01:00
Stefan Agner
76902aa7fa Avoid adding Content-Type to non-body responses (#163885) 2026-02-24 14:31:04 +01:00
Erwin Douna
07b9877f64 Add button platform to Proxmox (#163791)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-02-24 14:24:20 +01:00
Erik Montnemery
40e2f79e60 Add support for reading backups using securetar v3 (#163920) 2026-02-24 14:23:00 +01:00
Christopher Fenner
aa707fcf41 Add gateway discovery via USB for EnOcean integration (#162756)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-24 11:58:01 +01:00
Willem-Jan van Rootselaar
4b53bc243d Add energy sensor to bsblan (#163879) 2026-02-24 11:56:27 +01:00
Robert Resch
220e94d029 Fix nightlies by reverting the builder to a version instead of a sha (#163935) 2026-02-24 11:48:19 +01:00
Erik Montnemery
b1f943ccda Replace discovery with user flow in Philips Hue BLE (#163924) 2026-02-24 11:06:31 +01:00
Brett Adams
e37d84049a Update Splunk integration to bronze quality scale (#163616)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-24 10:56:05 +01:00
Marc Mueller
209473e376 Remove myself as codeowner for fritzbox_callmonitor (#163927) 2026-02-24 10:45:58 +01:00
MoonDevLT
334c3af448 Bump lunatone-rest-api-client to 0.7.0 (#163594) 2026-02-24 10:10:04 +01:00
hanwg
5560139d24 Clean up duplicated code in Telegram bot (#163917) 2026-02-24 10:04:21 +01:00
Erik Montnemery
d4dec5d1d3 Improve backup_restore tests (#163921) 2026-02-24 10:03:42 +01:00
J. Nick Koston
6cb63a60bc Skip unknown entity types in ESPHome integration (#163887) 2026-02-24 08:48:27 +01:00
Franck Nijhof
991301e79e Merge branch 'master' into dev 2026-02-24 07:07:39 +00:00
andreimoraru
06e2b4633a Bump yt-dlp to 2026.2.21 (#163916) 2026-02-24 07:30:54 +01:00
Manu
048d8d217c Update strings in ntfy integration (#163912) 2026-02-24 06:24:18 +01:00
Kyle Johnson
3693bc5878 Make Google Assistant fan speed percent and step speeds mutually exclusive (#162770) 2026-02-23 22:26:09 +00:00
Franck Nijhof
9c640fe0fa 2026.2.3 (#163683) 2026-02-20 21:43:32 +01:00
Sid
62145e5f9e Bump eheimdigital to 1.6.0 (#161961) 2026-02-20 19:51:10 +00:00
Franck Nijhof
c0fc414bb9 Fix nrgkick tests for rc 2026-02-20 19:49:27 +00:00
Franck Nijhof
69411a05ff Bump version to 2026.2.3 2026-02-20 19:39:05 +00:00
Marc Mueller
06c9ec861d Fix hassfest requirements check (#163681) 2026-02-20 19:38:58 +00:00
Joost Lekkerkerker
946df1755f Bump pySmartThings to 3.5.3 (#163375)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-02-20 19:38:56 +00:00
Thomas Sejr Madsen
d0678e0641 Fix touchline_sl zone availability when alarm state is set (#163338) 2026-02-20 19:38:55 +00:00
Allen Porter
ec56f183da Bump pyrainbird to 6.0.5 (#163333) 2026-02-20 19:38:53 +00:00
Åke Strandberg
033005e0de Add Miele dishwasher program code (#163308) 2026-02-20 19:38:52 +00:00
Andreas Jakl
91f9f5a826 NRGkick: do not update vehicle connected timestamp when vehicle is not connected (#163292) 2026-02-20 19:38:51 +00:00
David Recordon
ac4fcab827 Fix Control4 HVAC action mapping for multi-stage and idle states (#163222) 2026-02-20 19:38:49 +00:00
Allen Porter
d0eea77178 Fix remote calendar event handling of events within the same update period (#163186)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-20 19:38:48 +00:00
Markus Adrario
fb38fa3844 Add Lux to homee units (#163180) 2026-02-20 19:38:47 +00:00
Allen Porter
440efb953e Bump ical to 13.2.0 (#163123) 2026-02-20 19:38:45 +00:00
Manu
7ce47cca0d Fix blocking call in Xbox config flow (#163122) 2026-02-20 19:38:44 +00:00
Andre Lengwenus
a5f607bb91 Bump pypck to 0.9.11 (#163043) 2026-02-20 19:38:42 +00:00
Andre Lengwenus
b03043aa6f Bump pypck to 0.9.10 (#162333) 2026-02-20 19:38:41 +00:00
Robert Resch
0f3c7ca277 Block redirect to localhost (#162941) 2026-02-20 19:37:03 +00:00
Martin Hjelmare
3abf7c22f3 Fix Z-Wave climate set preset (#162728) 2026-02-20 19:37:01 +00:00
hbludworth
292e1de126 Show progress indicator during backup stage of Core/App update (#162683) 2026-02-20 19:37:00 +00:00
Christian Lackas
2d776a8193 Fix HomematicIP entity recovery after access point cloud reconnect (#162575) 2026-02-20 19:36:58 +00:00
Sid
039bbbb48c Fix dynamic entity creation in eheimdigital (#161155) 2026-02-20 19:36:56 +00:00
Luke Lashley
ad5565df95 Add the ability to select region for Roborock (#160898) 2026-02-20 19:36:55 +00:00
Franck Nijhof
3e6bc29a6a 2026.2.2 (#162950) 2026-02-13 21:05:06 +01:00
Franck Nijhof
ec8067a5a8 Bump version to 2026.2.2 2026-02-13 19:25:16 +00:00
Josef Zweck
6f47716d0a Log remaining token duration in onedrive (#162933) 2026-02-13 19:24:25 +00:00
puddly
efba5c6bcc Bump ZHA to 0.0.90 (#162894) 2026-02-13 19:24:24 +00:00
Sammy [Andrei Marinache]
d10e78079f Add Miele TQ1000WP tumble dryer programs and program phases (#162871)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Åke Strandberg <ake@strandberg.eu>
2026-02-13 19:24:23 +00:00
Jon Seager
6d4581580f Bump pytouchlinesl to 0.6.0 (#162856) 2026-02-13 19:24:21 +00:00
Yoshi Walsh
0d9a41a540 Bump pydaikin to 2.17.2 (#162846) 2026-02-13 19:24:20 +00:00
Vicx
cd69e6db73 Bump slixmpp to 1.13.2 (#162837) 2026-02-13 19:24:19 +00:00
Xitee
1320367d0d Filter out transient zero values from qBittorrent alltime stats (#162821)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 19:24:18 +00:00
Joost Lekkerkerker
dfa4698887 Bump pySmartThings to 3.5.2 (#162809)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-02-13 19:24:17 +00:00
Robert Resch
b426115de7 Bump cryptography to 46.0.5 (#162783) 2026-02-13 19:24:15 +00:00
hanwg
fb79fa37f8 Fix bug in edit_message_media action for Telegram bot (#162762) 2026-02-13 19:24:14 +00:00
Simone Chemelli
6a5f7bf424 Fix image platform state for Vodafone Station (#162747)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-02-13 19:24:13 +00:00
Simone Chemelli
142ca6dec1 Fix alarm refresh warning for Comelit SimpleHome (#162710) 2026-02-13 19:24:12 +00:00
epenet
0f986c24d0 Fix unavailable status in Tuya (#162709) 2026-02-13 19:24:11 +00:00
Josef Zweck
01f2b7b6f6 Bump onedrive-personal-sdk to 0.1.2 (#162689) 2026-02-13 19:24:09 +00:00
Michael
b9469027f5 Fix handling when FRITZ!Box reboots in FRITZ!Box Tools (#162679) 2026-02-13 19:24:08 +00:00
Tomás Correia
fbb94af748 fix to cloudflare r2 setup screen info (#162677) 2026-02-13 19:24:07 +00:00
Michael
148bdf6e3a Fix handling when FRITZ!Box reboots in FRITZ!Smarthome (#162676) 2026-02-13 19:24:05 +00:00
starkillerOG
91999f8871 Bump reolink-aio to 0.19.0 (#162672) 2026-02-13 19:24:04 +00:00
Jeef
aecca4eb99 Bump intellifire4py to 4.3.1 (#162659) 2026-02-13 19:24:03 +00:00
Allen Porter
bf8aa49bae Improve MCP SSE fallback error handling (#162655) 2026-02-13 19:24:02 +00:00
Joost Lekkerkerker
4423425683 Pin setuptools to 81.0.0 (#162589)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-13 19:24:01 +00:00
Aaron Godfrey
44202da53d Increase max tasks retrieved per page to prevent timeout (#162587) 2026-02-13 19:23:59 +00:00
Thomas55555
9f7dfb72c4 Bump aioautomower to 2.7.3 (#162583)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-13 19:23:58 +00:00
Michael
de07a69e4f Bump aioimmich to 0.12.0 (#162573)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-13 19:23:57 +00:00
Maikel Punie
bbf4c38115 migrate velbus config entries (#162565) 2026-02-13 19:23:56 +00:00
ElCruncharino
e1bb5d52ef Add timeout to B2 metadata downloads to prevent backup hang (#162562) 2026-02-13 19:23:54 +00:00
hanwg
eb64b6bdee Fix config flow bug for Telegram bot (#162555) 2026-02-13 19:23:53 +00:00
Andrea Turri
ecb288b735 Add new Miele mappings (#162544) 2026-02-13 19:23:52 +00:00
Norbert Rittel
a419c9c420 Sentence-case "speech-to-text" in google_cloud (#162534) 2026-02-13 19:23:51 +00:00
Brett Adams
dd29133324 Fix Tesla Fleet partner registration to use all regions (#162525)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 19:23:50 +00:00
Allen Porter
90f22ea516 Bump grpc to 1.78.0 (#162520) 2026-02-13 19:23:48 +00:00
Peter Grauvogel
9db1428265 Fix Green Planet Energy price unit conversion (#162511) 2026-02-13 19:23:47 +00:00
Denis Shulyaka
a696b05b0d Fix JSON serialization of time objects in Cloud conversation tool results (#162506) 2026-02-13 19:23:46 +00:00
Denis Shulyaka
77ddb63b73 Fix JSON serialization of time objects in Open Router tool results (#162505) 2026-02-13 19:23:44 +00:00
Denis Shulyaka
4180a6e176 Fix JSON serialization of time objects in Ollama tool results (#162502)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-13 19:23:43 +00:00
Denis Shulyaka
6d74c912d2 Fix JSON serialization of datetime objects in Google Generative AI tool results (#162495) 2026-02-13 19:23:42 +00:00
Denis Shulyaka
8a01dfcc00 Fix JSON serialization of time objects in OpenAI tool results (#162490) 2026-02-13 19:23:40 +00:00
Brett Adams
9722898dc6 Fix device_class of backup reserve sensor in Tessie (#162459) 2026-02-13 19:23:39 +00:00
Brett Adams
7438c71fcb Fix device_class of backup reserve sensor in teslemetry (#162458) 2026-02-13 19:23:38 +00:00
Christian Lackas
0b5e55b923 Fix absolute humidity sensor on HmIP-WGT glass thermostats (#162455) 2026-02-13 19:23:37 +00:00
ElCruncharino
61ed959e8e Fix AsyncIteratorReader blocking after stream exhaustion (#161731) 2026-02-13 19:17:20 +00:00
Jaap Pieroen
3989532465 Bump essent-dynamic-pricing to 0.3.1 (#160958)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-02-13 19:17:18 +00:00
Franck Nijhof
28027ddca4 2026.2.1 (#162450) 2026-02-06 22:44:07 +01:00
Franck Nijhof
fe0d7b3cca Bump version to 2026.2.1 2026-02-06 20:49:26 +00:00
jameson_uk
0dcc4e9527 dep: bump aioamazondevices to 11.1.3 (#162437) 2026-02-06 20:47:38 +00:00
Artur Pragacz
b13b189703 Make bad entity ID detection more lenient (#162425) 2026-02-06 20:47:37 +00:00
epenet
150829f599 Fix invalid yardian snaphots (#162422) 2026-02-06 20:47:36 +00:00
Joost Lekkerkerker
57dd9d9c23 Remove double unit of measurement for yardian (#162412) 2026-02-06 20:47:34 +00:00
Sab44
e2056cb12c Bump librehardwaremonitor-api to version 1.9.1 (#162409) 2026-02-06 20:47:33 +00:00
Joost Lekkerkerker
fa2c8992cf Remove entity id overwrite for ambient station (#162403) 2026-02-06 20:47:32 +00:00
Matt Zimmerman
ddf5c7fe3a Add missing config flow strings to SmartTub (#162375)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-06 20:47:31 +00:00
Matt Zimmerman
7034ed6d3f Bump python-smarttub to 0.0.47 (#162367)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-06 20:47:29 +00:00
Aaron Godfrey
9015b53c1b Fix conversion of data for todo.* actions (#162366) 2026-02-06 20:47:28 +00:00
Jordan Harvey
1cfa6561f7 Update pynintendoparental requirement to version 2.3.2.1 (#162362) 2026-02-06 20:47:27 +00:00
Shay Levy
eead02dcca Fix Shelly Linkedgo Thermostat status update (#162339) 2026-02-06 20:47:26 +00:00
Arie Catsman
456e51a221 Bump pyenphase to 2.4.5 (#162324) 2026-02-06 20:47:25 +00:00
Luo Chen
5d984ce186 Fix unicode escaping in MCP server tool response (#162319)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-02-06 20:47:24 +00:00
Oliver
61f45489ac Add mapping for stopped state to denonavr media player (#162283) 2026-02-06 20:47:23 +00:00
Tomás Correia
f72c643b38 Fix multipart upload to use consistent part sizes for R2/S3 (#162278) 2026-02-06 20:47:22 +00:00
Oliver
27bc26e886 Bump denonavr to 1.3.2 (#162271) 2026-02-06 20:47:20 +00:00
Thomas55555
0e9f03cbc1 Bump google_air_quality_api to 3.0.1 (#162233) 2026-02-06 20:47:19 +00:00
David Bonnes
9480c33fb0 Bump evohome-async to 1.1.3 (#162232) 2026-02-06 20:47:18 +00:00
Jonathan
3e6b8663e8 Fix device_class of backup reserve sensor (#161178) 2026-02-06 20:47:17 +00:00
epenet
1c69a83793 Fix redundant off preset in Tuya climate (#161040) 2026-02-06 20:47:16 +00:00
1018 changed files with 78047 additions and 16395 deletions

View File

@@ -0,0 +1,8 @@
---
name: ban-word-list
description: Find words that are not allowed
---
# Ban Word List
If any of the words listed in the `list.md` file are found on new code, warn the user and ask them to change it.

View File

@@ -0,0 +1 @@
- potato

View File

@@ -34,6 +34,7 @@ base_platforms: &base_platforms
- homeassistant/components/humidifier/**
- homeassistant/components/image/**
- homeassistant/components/image_processing/**
- homeassistant/components/infrared/**
- homeassistant/components/lawn_mower/**
- homeassistant/components/light/**
- homeassistant/components/lock/**

File diff suppressed because it is too large Load Diff

View File

@@ -272,7 +272,7 @@ jobs:
name: Build ${{ matrix.machine }} machine core image
if: github.repository_owner == 'home-assistant'
needs: ["init", "build_base"]
runs-on: ubuntu-latest
runs-on: ${{ matrix.runs-on }}
permissions:
contents: read # To check out the repository
packages: write # To push to GHCR
@@ -294,6 +294,21 @@ jobs:
- raspberrypi5-64
- yellow
- green
include:
# Default: aarch64 on native ARM runner
- arch: aarch64
runs-on: ubuntu-24.04-arm
# Overrides for amd64 machines
- machine: generic-x86-64
arch: amd64
runs-on: ubuntu-24.04
- machine: qemux86-64
arch: amd64
runs-on: ubuntu-24.04
# TODO: remove, intel-nuc is a legacy name for x86-64, renamed in 2021
- machine: intel-nuc
arch: amd64
runs-on: ubuntu-24.04
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -321,8 +336,9 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@21bc64d76dad7a5184c67826aab41c6b6f89023a # 2025.11.0
uses: home-assistant/builder@6cb4fd3d1338b6e22d0958a4bcb53e0965ea63b4 # 2026.02.1
with:
image: ${{ matrix.arch }}
args: |
$BUILD_ARGS \
--target /data/machine \

View File

@@ -40,7 +40,7 @@ env:
CACHE_VERSION: 3
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2026.3"
HA_SHORT_VERSION: "2026.4"
DEFAULT_PYTHON: "3.14.2"
ALL_PYTHON_VERSIONS: "['3.14.2']"
# 10.3 is the oldest supported version
@@ -605,7 +605,7 @@ jobs:
with:
persist-credentials: false
- name: Dependency review
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3
with:
license-check: false # We use our own license audit checks

View File

@@ -28,11 +28,11 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
category: "/language:python"

View File

@@ -110,7 +110,7 @@ jobs:
strategy:
fail-fast: false
matrix:
abi: ["cp313", "cp314"]
abi: ["cp314"]
arch: ["amd64", "aarch64"]
include:
- arch: amd64
@@ -161,7 +161,7 @@ jobs:
strategy:
fail-fast: false
matrix:
abi: ["cp313", "cp314"]
abi: ["cp314"]
arch: ["amd64", "aarch64"]
include:
- arch: amd64

View File

@@ -289,6 +289,7 @@ homeassistant.components.imgw_pib.*
homeassistant.components.immich.*
homeassistant.components.incomfort.*
homeassistant.components.inels.*
homeassistant.components.infrared.*
homeassistant.components.input_button.*
homeassistant.components.input_select.*
homeassistant.components.input_text.*
@@ -544,6 +545,7 @@ homeassistant.components.tcp.*
homeassistant.components.technove.*
homeassistant.components.tedee.*
homeassistant.components.telegram_bot.*
homeassistant.components.teslemetry.*
homeassistant.components.text.*
homeassistant.components.thethingsnetwork.*
homeassistant.components.threshold.*

17
CODEOWNERS generated
View File

@@ -242,6 +242,8 @@ build.json @home-assistant/supervisor
/tests/components/bosch_alarm/ @mag1024 @sanjay900
/homeassistant/components/bosch_shc/ @tschamm
/tests/components/bosch_shc/ @tschamm
/homeassistant/components/brands/ @home-assistant/core
/tests/components/brands/ @home-assistant/core
/homeassistant/components/braviatv/ @bieniu @Drafteed
/tests/components/braviatv/ @bieniu @Drafteed
/homeassistant/components/bring/ @miaucl @tr4nt0r
@@ -399,8 +401,6 @@ build.json @home-assistant/supervisor
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
/homeassistant/components/duckdns/ @tr4nt0r
/tests/components/duckdns/ @tr4nt0r
/homeassistant/components/duke_energy/ @hunterjm
/tests/components/duke_energy/ @hunterjm
/homeassistant/components/duotecno/ @cereal2nd
/tests/components/duotecno/ @cereal2nd
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192
@@ -555,8 +555,6 @@ build.json @home-assistant/supervisor
/tests/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
/homeassistant/components/fritzbox/ @mib1185 @flabbamann
/tests/components/fritzbox/ @mib1185 @flabbamann
/homeassistant/components/fritzbox_callmonitor/ @cdce8p
/tests/components/fritzbox_callmonitor/ @cdce8p
/homeassistant/components/fronius/ @farmio
/tests/components/fronius/ @farmio
/homeassistant/components/frontend/ @home-assistant/frontend
@@ -719,8 +717,8 @@ build.json @home-assistant/supervisor
/tests/components/homematic/ @pvizeli
/homeassistant/components/homematicip_cloud/ @hahn-th @lackas
/tests/components/homematicip_cloud/ @hahn-th @lackas
/homeassistant/components/homevolt/ @danielhiversen
/tests/components/homevolt/ @danielhiversen
/homeassistant/components/homevolt/ @danielhiversen @liudger
/tests/components/homevolt/ @danielhiversen @liudger
/homeassistant/components/homewizard/ @DCSBL
/tests/components/homewizard/ @DCSBL
/homeassistant/components/honeywell/ @rdfurman @mkmer
@@ -794,6 +792,8 @@ build.json @home-assistant/supervisor
/tests/components/inels/ @epdevlab
/homeassistant/components/influxdb/ @mdegat01 @Robbie1221
/tests/components/influxdb/ @mdegat01 @Robbie1221
/homeassistant/components/infrared/ @home-assistant/core
/tests/components/infrared/ @home-assistant/core
/homeassistant/components/inkbird/ @bdraco
/tests/components/inkbird/ @bdraco
/homeassistant/components/input_boolean/ @home-assistant/core
@@ -1899,8 +1899,8 @@ build.json @home-assistant/supervisor
/tests/components/withings/ @joostlek
/homeassistant/components/wiz/ @sbidy @arturpragacz
/tests/components/wiz/ @sbidy @arturpragacz
/homeassistant/components/wled/ @frenck
/tests/components/wled/ @frenck
/homeassistant/components/wled/ @frenck @mik-laj
/tests/components/wled/ @frenck @mik-laj
/homeassistant/components/wmspro/ @mback2k
/tests/components/wmspro/ @mback2k
/homeassistant/components/wolflink/ @adamkrol93 @mtielen
@@ -1968,6 +1968,7 @@ build.json @home-assistant/supervisor
/homeassistant/components/zone/ @home-assistant/core
/tests/components/zone/ @home-assistant/core
/homeassistant/components/zoneminder/ @rohankapoorcom @nabbi
/tests/components/zoneminder/ @rohankapoorcom @nabbi
/homeassistant/components/zwave_js/ @home-assistant/z-wave
/tests/components/zwave_js/ @home-assistant/z-wave
/homeassistant/components/zwave_me/ @lawfulchaos @Z-Wave-Me @PoltoS

2
Dockerfile generated
View File

@@ -30,7 +30,7 @@ RUN \
# Verify go2rtc can be executed
go2rtc --version \
# Install uv
&& pip3 install uv==0.9.26
&& pip3 install uv==0.10.6
WORKDIR /usr/src

View File

@@ -10,6 +10,7 @@ coverage:
target: auto
threshold: 1
paths:
- homeassistant/components/*/backup.py
- homeassistant/components/*/config_flow.py
- homeassistant/components/*/device_action.py
- homeassistant/components/*/device_condition.py
@@ -28,6 +29,7 @@ coverage:
target: 100
threshold: 0
paths:
- homeassistant/components/*/backup.py
- homeassistant/components/*/config_flow.py
- homeassistant/components/*/device_action.py
- homeassistant/components/*/device_condition.py

View File

@@ -210,6 +210,7 @@ DEFAULT_INTEGRATIONS = {
"analytics", # Needed for onboarding
"application_credentials",
"backup",
"brands",
"frontend",
"hardware",
"labs",

View File

@@ -12,10 +12,6 @@ from homeassistant.helpers.dispatcher import dispatcher_send
from .const import DOMAIN, DOMAIN_DATA, LOGGER
SERVICE_SETTINGS = "change_setting"
SERVICE_CAPTURE_IMAGE = "capture_image"
SERVICE_TRIGGER_AUTOMATION = "trigger_automation"
ATTR_SETTING = "setting"
ATTR_VALUE = "value"
@@ -75,16 +71,13 @@ def async_setup_services(hass: HomeAssistant) -> None:
"""Home Assistant services."""
hass.services.async_register(
DOMAIN, SERVICE_SETTINGS, _change_setting, schema=CHANGE_SETTING_SCHEMA
DOMAIN, "change_setting", _change_setting, schema=CHANGE_SETTING_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_CAPTURE_IMAGE, _capture_image, schema=CAPTURE_IMAGE_SCHEMA
DOMAIN, "capture_image", _capture_image, schema=CAPTURE_IMAGE_SCHEMA
)
hass.services.async_register(
DOMAIN,
SERVICE_TRIGGER_AUTOMATION,
_trigger_automation,
schema=AUTOMATION_SCHEMA,
DOMAIN, "trigger_automation", _trigger_automation, schema=AUTOMATION_SCHEMA
)

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["accuweather"],
"requirements": ["accuweather==5.0.0"]
"requirements": ["accuweather==5.1.0"]
}

View File

@@ -30,6 +30,8 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
)
return {
"can_reach_server": system_health.async_check_can_reach_url(hass, ENDPOINT),
"can_reach_server": system_health.async_check_can_reach_url(
hass, str(ENDPOINT)
),
"remaining_requests": remaining_requests,
}

View File

@@ -191,7 +191,7 @@ class AccuWeatherEntity(
{
ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(),
ATTR_FORECAST_CLOUD_COVERAGE: item["CloudCoverDay"],
ATTR_FORECAST_HUMIDITY: item["RelativeHumidityDay"]["Average"],
ATTR_FORECAST_HUMIDITY: item["RelativeHumidityDay"].get("Average"),
ATTR_FORECAST_NATIVE_TEMP: item["TemperatureMax"][ATTR_VALUE],
ATTR_FORECAST_NATIVE_TEMP_LOW: item["TemperatureMin"][ATTR_VALUE],
ATTR_FORECAST_NATIVE_APPARENT_TEMP: item["RealFeelTemperatureMax"][

View File

@@ -10,8 +10,6 @@ from homeassistant.helpers import config_validation as cv, service
from .const import DOMAIN
ADVANTAGE_AIR_SERVICE_SET_TIME_TO = "set_time_to"
@callback
def async_setup_services(hass: HomeAssistant) -> None:
@@ -20,7 +18,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
service.async_register_platform_entity_service(
hass,
DOMAIN,
ADVANTAGE_AIR_SERVICE_SET_TIME_TO,
"set_time_to",
entity_domain=SENSOR_DOMAIN,
schema={vol.Required("minutes"): cv.positive_int},
func="set_time_to",

View File

@@ -8,18 +8,12 @@ from homeassistant.helpers import service
from .const import DOMAIN
_DEV_EN_ALT = "enable_alerts"
_DEV_DS_ALT = "disable_alerts"
_DEV_EN_REC = "start_recording"
_DEV_DS_REC = "stop_recording"
_DEV_SNAP = "snapshot"
CAMERA_SERVICES = {
_DEV_EN_ALT: "async_enable_alerts",
_DEV_DS_ALT: "async_disable_alerts",
_DEV_EN_REC: "async_start_recording",
_DEV_DS_REC: "async_stop_recording",
_DEV_SNAP: "async_snapshot",
"enable_alerts": "async_enable_alerts",
"disable_alerts": "async_disable_alerts",
"start_recording": "async_start_recording",
"stop_recording": "async_stop_recording",
"snapshot": "async_snapshot",
}

View File

@@ -93,7 +93,6 @@ class AirobotNumber(AirobotEntity, NumberEntity):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="set_value_failed",
translation_placeholders={"error": str(err)},
) from err
else:
await self.coordinator.async_request_refresh()

View File

@@ -112,7 +112,7 @@
"message": "Failed to set temperature to {temperature}."
},
"set_value_failed": {
"message": "Failed to set value: {error}"
"message": "Failed to set value."
},
"switch_turn_off_failed": {
"message": "Failed to turn off {switch}."

View File

@@ -4,7 +4,16 @@ from __future__ import annotations
import logging
from airos.airos6 import AirOS6
from airos.airos8 import AirOS8
from airos.exceptions import (
AirOSConnectionAuthenticationError,
AirOSConnectionSetupError,
AirOSDataMissingError,
AirOSDeviceConnectionError,
AirOSKeyDataMissingError,
)
from airos.helpers import DetectDeviceData, async_get_firmware_data
from homeassistant.const import (
CONF_HOST,
@@ -15,6 +24,11 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryError,
ConfigEntryNotReady,
)
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -39,15 +53,40 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
hass, verify_ssl=entry.data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL]
)
airos_device = AirOS8(
host=entry.data[CONF_HOST],
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
session=session,
use_ssl=entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL],
conn_data = {
CONF_HOST: entry.data[CONF_HOST],
CONF_USERNAME: entry.data[CONF_USERNAME],
CONF_PASSWORD: entry.data[CONF_PASSWORD],
"use_ssl": entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL],
"session": session,
}
# Determine firmware version before creating the device instance
try:
device_data: DetectDeviceData = await async_get_firmware_data(**conn_data)
except (
AirOSConnectionSetupError,
AirOSDeviceConnectionError,
TimeoutError,
) as err:
raise ConfigEntryNotReady from err
except (
AirOSConnectionAuthenticationError,
AirOSDataMissingError,
) as err:
raise ConfigEntryAuthFailed from err
except AirOSKeyDataMissingError as err:
raise ConfigEntryError("key_data_missing") from err
except Exception as err:
raise ConfigEntryError("unknown") from err
airos_class: type[AirOS8 | AirOS6] = (
AirOS8 if device_data["fw_major"] == 8 else AirOS6
)
coordinator = AirOSDataUpdateCoordinator(hass, entry, airos_device)
airos_device = airos_class(**conn_data)
coordinator = AirOSDataUpdateCoordinator(hass, entry, device_data, airos_device)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator

View File

@@ -4,7 +4,9 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Generic, TypeVar
from airos.data import AirOSDataBaseClass
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
@@ -18,25 +20,24 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AirOS8Data, AirOSConfigEntry, AirOSDataUpdateCoordinator
from .entity import AirOSEntity
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
AirOSDataModel = TypeVar("AirOSDataModel", bound=AirOSDataBaseClass)
@dataclass(frozen=True, kw_only=True)
class AirOSBinarySensorEntityDescription(BinarySensorEntityDescription):
class AirOSBinarySensorEntityDescription(
BinarySensorEntityDescription,
Generic[AirOSDataModel],
):
"""Describe an AirOS binary sensor."""
value_fn: Callable[[AirOS8Data], bool]
value_fn: Callable[[AirOSDataModel], bool]
BINARY_SENSORS: tuple[AirOSBinarySensorEntityDescription, ...] = (
AirOSBinarySensorEntityDescription(
key="portfw",
translation_key="port_forwarding",
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.portfw,
),
AirOS8BinarySensorEntityDescription = AirOSBinarySensorEntityDescription[AirOS8Data]
COMMON_BINARY_SENSORS: tuple[AirOSBinarySensorEntityDescription, ...] = (
AirOSBinarySensorEntityDescription(
key="dhcp_client",
translation_key="dhcp_client",
@@ -52,14 +53,6 @@ BINARY_SENSORS: tuple[AirOSBinarySensorEntityDescription, ...] = (
value_fn=lambda data: data.services.dhcpd,
entity_registry_enabled_default=False,
),
AirOSBinarySensorEntityDescription(
key="dhcp6_server",
translation_key="dhcp6_server",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.services.dhcp6d_stateful,
entity_registry_enabled_default=False,
),
AirOSBinarySensorEntityDescription(
key="pppoe",
translation_key="pppoe",
@@ -70,6 +63,23 @@ BINARY_SENSORS: tuple[AirOSBinarySensorEntityDescription, ...] = (
),
)
AIROS8_BINARY_SENSORS: tuple[AirOS8BinarySensorEntityDescription, ...] = (
AirOS8BinarySensorEntityDescription(
key="portfw",
translation_key="port_forwarding",
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.portfw,
),
AirOS8BinarySensorEntityDescription(
key="dhcp6_server",
translation_key="dhcp6_server",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.services.dhcp6d_stateful,
entity_registry_enabled_default=False,
),
)
async def async_setup_entry(
hass: HomeAssistant,
@@ -79,9 +89,18 @@ async def async_setup_entry(
"""Set up the AirOS binary sensors from a config entry."""
coordinator = config_entry.runtime_data
async_add_entities(
AirOSBinarySensor(coordinator, description) for description in BINARY_SENSORS
)
entities = [
AirOSBinarySensor(coordinator, description)
for description in COMMON_BINARY_SENSORS
]
if coordinator.device_data["fw_major"] == 8:
entities.extend(
AirOSBinarySensor(coordinator, description)
for description in AIROS8_BINARY_SENSORS
)
async_add_entities(entities)
class AirOSBinarySensor(AirOSEntity, BinarySensorEntity):

View File

@@ -2,8 +2,6 @@
from __future__ import annotations
import logging
from airos.exceptions import AirOSException
from homeassistant.components.button import (
@@ -18,8 +16,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import DOMAIN, AirOSConfigEntry, AirOSDataUpdateCoordinator
from .entity import AirOSEntity
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
REBOOT_BUTTON = ButtonEntityDescription(

View File

@@ -7,6 +7,8 @@ from collections.abc import Mapping
import logging
from typing import Any
from airos.airos6 import AirOS6
from airos.airos8 import AirOS8
from airos.discovery import airos_discover_devices
from airos.exceptions import (
AirOSConnectionAuthenticationError,
@@ -17,6 +19,7 @@ from airos.exceptions import (
AirOSKeyDataMissingError,
AirOSListenerError,
)
from airos.helpers import DetectDeviceData, async_get_firmware_data
import voluptuous as vol
from homeassistant.config_entries import (
@@ -34,11 +37,13 @@ from homeassistant.const import (
)
from homeassistant.data_entry_flow import section
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.selector import (
TextSelector,
TextSelectorConfig,
TextSelectorType,
)
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from .const import (
DEFAULT_SSL,
@@ -51,10 +56,11 @@ from .const import (
MAC_ADDRESS,
SECTION_ADVANCED_SETTINGS,
)
from .coordinator import AirOS8
_LOGGER = logging.getLogger(__name__)
AirOSDeviceDetect = AirOS8 | AirOS6
# Discovery duration in seconds, airOS announces every 20 seconds
DISCOVER_INTERVAL: int = 30
@@ -90,7 +96,7 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize the config flow."""
super().__init__()
self.airos_device: AirOS8
self.airos_device: AirOSDeviceDetect
self.errors: dict[str, str] = {}
self.discovered_devices: dict[str, dict[str, Any]] = {}
self.discovery_abort_reason: str | None = None
@@ -133,16 +139,14 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
verify_ssl=config_data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL],
)
airos_device = AirOS8(
host=config_data[CONF_HOST],
username=config_data[CONF_USERNAME],
password=config_data[CONF_PASSWORD],
session=session,
use_ssl=config_data[SECTION_ADVANCED_SETTINGS][CONF_SSL],
)
try:
await airos_device.login()
airos_data = await airos_device.status()
device_data: DetectDeviceData = await async_get_firmware_data(
host=config_data[CONF_HOST],
username=config_data[CONF_USERNAME],
password=config_data[CONF_PASSWORD],
session=session,
use_ssl=config_data[SECTION_ADVANCED_SETTINGS][CONF_SSL],
)
except (
AirOSConnectionSetupError,
@@ -157,14 +161,14 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception during credential validation")
self.errors["base"] = "unknown"
else:
await self.async_set_unique_id(airos_data.derived.mac)
await self.async_set_unique_id(device_data["mac"])
if self.source in [SOURCE_REAUTH, SOURCE_RECONFIGURE]:
self._abort_if_unique_id_mismatch()
else:
self._abort_if_unique_id_configured()
return {"title": airos_data.host.hostname, "data": config_data}
return {"title": device_data["hostname"], "data": config_data}
return None
@@ -392,6 +396,18 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
except asyncio.CancelledError:
pass
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Automatically handle a DHCP discovered IP change."""
ip_address = discovery_info.ip
# python-airos defaults to upper for derived mac_address
normalized_mac = format_mac(discovery_info.macaddress).upper()
await self.async_set_unique_id(normalized_mac)
self._abort_if_unique_id_configured(updates={CONF_HOST: ip_address})
return self.async_abort(reason="unreachable")
async def async_step_discovery_no_devices(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import logging
from airos.airos6 import AirOS6, AirOS6Data
from airos.airos8 import AirOS8, AirOS8Data
from airos.exceptions import (
AirOSConnectionAuthenticationError,
@@ -11,6 +12,7 @@ from airos.exceptions import (
AirOSDataMissingError,
AirOSDeviceConnectionError,
)
from airos.helpers import DetectDeviceData
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -21,19 +23,28 @@ from .const import DOMAIN, SCAN_INTERVAL
_LOGGER = logging.getLogger(__name__)
AirOSDeviceDetect = AirOS8 | AirOS6
AirOSDataDetect = AirOS8Data | AirOS6Data
type AirOSConfigEntry = ConfigEntry[AirOSDataUpdateCoordinator]
class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOS8Data]):
class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSDataDetect]):
"""Class to manage fetching AirOS data from single endpoint."""
airos_device: AirOSDeviceDetect
config_entry: AirOSConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: AirOSConfigEntry, airos_device: AirOS8
self,
hass: HomeAssistant,
config_entry: AirOSConfigEntry,
device_data: DetectDeviceData,
airos_device: AirOSDeviceDetect,
) -> None:
"""Initialize the coordinator."""
self.airos_device = airos_device
self.device_data = device_data
super().__init__(
hass,
_LOGGER,
@@ -42,7 +53,7 @@ class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOS8Data]):
update_interval=SCAN_INTERVAL,
)
async def _async_update_data(self) -> AirOS8Data:
async def _async_update_data(self) -> AirOSDataDetect:
"""Fetch data from AirOS."""
try:
await self.airos_device.login()
@@ -62,7 +73,7 @@ class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOS8Data]):
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err
except (AirOSDataMissingError,) as err:
except AirOSDataMissingError as err:
_LOGGER.error("Expected data not returned by airOS device: %s", err)
raise UpdateFailed(
translation_domain=DOMAIN,

View File

@@ -3,9 +3,10 @@
"name": "Ubiquiti airOS",
"codeowners": ["@CoMPaTech"],
"config_flow": true,
"dhcp": [{ "registered_devices": true }],
"documentation": "https://www.home-assistant.io/integrations/airos",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"quality_scale": "platinum",
"requirements": ["airos==0.6.4"]
}

View File

@@ -42,16 +42,20 @@ rules:
# Gold
devices: done
diagnostics: done
discovery-update-info: todo
discovery: todo
discovery-update-info: done
discovery:
status: exempt
comment: No way to detect device on the network
docs-data-update: done
docs-examples: todo
docs-examples: done
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: todo
dynamic-devices:
status: exempt
comment: single airOS device per config entry; peer/remote endpoints are not modeled as child devices/entities at this time
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
@@ -61,8 +65,10 @@ rules:
status: exempt
comment: no (custom) icons used or envisioned
reconfiguration-flow: done
repair-issues: todo
stale-devices: todo
repair-issues: done
stale-devices:
status: exempt
comment: single airOS device per config entry; peer/remote endpoints are not modeled as child devices/entities at this time
# Platinum
async-dependency: done

View File

@@ -5,8 +5,14 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Generic, TypeVar
from airos.data import DerivedWirelessMode, DerivedWirelessRole, NetRole
from airos.data import (
AirOSDataBaseClass,
DerivedWirelessMode,
DerivedWirelessRole,
NetRole,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -37,15 +43,19 @@ WIRELESS_ROLE_OPTIONS = [mode.value for mode in DerivedWirelessRole]
PARALLEL_UPDATES = 0
AirOSDataModel = TypeVar("AirOSDataModel", bound=AirOSDataBaseClass)
@dataclass(frozen=True, kw_only=True)
class AirOSSensorEntityDescription(SensorEntityDescription):
class AirOSSensorEntityDescription(SensorEntityDescription, Generic[AirOSDataModel]):
"""Describe an AirOS sensor."""
value_fn: Callable[[AirOS8Data], StateType]
value_fn: Callable[[AirOSDataModel], StateType]
SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
AirOS8SensorEntityDescription = AirOSSensorEntityDescription[AirOS8Data]
COMMON_SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
AirOSSensorEntityDescription(
key="host_cpuload",
translation_key="host_cpuload",
@@ -75,54 +85,6 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
translation_key="wireless_essid",
value_fn=lambda data: data.wireless.essid,
),
AirOSSensorEntityDescription(
key="wireless_antenna_gain",
translation_key="wireless_antenna_gain",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data.wireless.antenna_gain,
),
AirOSSensorEntityDescription(
key="wireless_throughput_tx",
translation_key="wireless_throughput_tx",
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
value_fn=lambda data: data.wireless.throughput.tx,
),
AirOSSensorEntityDescription(
key="wireless_throughput_rx",
translation_key="wireless_throughput_rx",
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
value_fn=lambda data: data.wireless.throughput.rx,
),
AirOSSensorEntityDescription(
key="wireless_polling_dl_capacity",
translation_key="wireless_polling_dl_capacity",
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
value_fn=lambda data: data.wireless.polling.dl_capacity,
),
AirOSSensorEntityDescription(
key="wireless_polling_ul_capacity",
translation_key="wireless_polling_ul_capacity",
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
value_fn=lambda data: data.wireless.polling.ul_capacity,
),
AirOSSensorEntityDescription(
key="host_uptime",
translation_key="host_uptime",
@@ -158,6 +120,57 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
options=WIRELESS_ROLE_OPTIONS,
entity_registry_enabled_default=False,
),
AirOSSensorEntityDescription(
key="wireless_antenna_gain",
translation_key="wireless_antenna_gain",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data.wireless.antenna_gain,
),
AirOSSensorEntityDescription(
key="wireless_polling_dl_capacity",
translation_key="wireless_polling_dl_capacity",
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
value_fn=lambda data: data.wireless.polling.dl_capacity,
),
AirOSSensorEntityDescription(
key="wireless_polling_ul_capacity",
translation_key="wireless_polling_ul_capacity",
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
value_fn=lambda data: data.wireless.polling.ul_capacity,
),
)
AIROS8_SENSORS: tuple[AirOS8SensorEntityDescription, ...] = (
AirOS8SensorEntityDescription(
key="wireless_throughput_tx",
translation_key="wireless_throughput_tx",
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
value_fn=lambda data: data.wireless.throughput.tx,
),
AirOS8SensorEntityDescription(
key="wireless_throughput_rx",
translation_key="wireless_throughput_rx",
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
value_fn=lambda data: data.wireless.throughput.rx,
),
)
@@ -169,7 +182,14 @@ async def async_setup_entry(
"""Set up the AirOS sensors from a config entry."""
coordinator = config_entry.runtime_data
async_add_entities(AirOSSensor(coordinator, description) for description in SENSORS)
entities = [AirOSSensor(coordinator, description) for description in COMMON_SENSORS]
if coordinator.device_data["fw_major"] == 8:
entities.extend(
AirOSSensor(coordinator, description) for description in AIROS8_SENSORS
)
async_add_entities(entities)
class AirOSSensor(AirOSEntity, SensorEntity):

View File

@@ -5,12 +5,13 @@ from __future__ import annotations
from datetime import timedelta
import logging
import aiohttp
from genie_partner_sdk.client import AladdinConnectClient
from genie_partner_sdk.model import GarageDoor
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
_LOGGER = logging.getLogger(__name__)
type AladdinConnectConfigEntry = ConfigEntry[dict[str, AladdinConnectCoordinator]]
@@ -40,7 +41,10 @@ class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
async def _async_update_data(self) -> GarageDoor:
"""Fetch data from the Aladdin Connect API."""
await self.client.update_door(self.data.device_id, self.data.door_number)
try:
await self.client.update_door(self.data.device_id, self.data.door_number)
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
)

View File

@@ -4,14 +4,19 @@ from __future__ import annotations
from typing import Any
import aiohttp
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import SUPPORTED_FEATURES
from .const import DOMAIN, SUPPORTED_FEATURES
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
from .entity import AladdinConnectEntity
PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant,
@@ -40,11 +45,23 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
async def async_open_cover(self, **kwargs: Any) -> None:
"""Issue open command to cover."""
await self.client.open_door(self._device_id, self._number)
try:
await self.client.open_door(self._device_id, self._number)
except aiohttp.ClientError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="open_door_failed",
) from err
async def async_close_cover(self, **kwargs: Any) -> None:
"""Issue close command to cover."""
await self.client.close_door(self._device_id, self._number)
try:
await self.client.close_door(self._device_id, self._number)
except aiohttp.ClientError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="close_door_failed",
) from err
@property
def is_closed(self) -> bool | None:

View File

@@ -0,0 +1,32 @@
"""Diagnostics support for Aladdin Connect."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.core import HomeAssistant
from .coordinator import AladdinConnectConfigEntry
TO_REDACT = {"access_token", "refresh_token"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: AladdinConnectConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
return {
"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,
}
for uid, coordinator in config_entry.runtime_data.items()
},
}

View File

@@ -26,24 +26,26 @@ rules:
unique-config-entry: done
# Silver
action-exceptions: todo
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: Integration does not have an options flow.
docs-installation-parameters: done
entity-unavailable: todo
entity-unavailable:
status: done
comment: Handled by the coordinator.
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
log-when-unavailable:
status: done
comment: Handled by the coordinator.
parallel-updates: done
reauthentication-flow: done
test-coverage:
status: todo
comment: Platform tests for cover and sensor need to be implemented to reach 95% coverage.
test-coverage: done
# Gold
devices: done
diagnostics: todo
diagnostics: done
discovery: done
discovery-update-info:
status: exempt

View File

@@ -20,6 +20,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
from .entity import AladdinConnectEntity
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class AladdinConnectSensorEntityDescription(SensorEntityDescription):

View File

@@ -32,5 +32,13 @@
"title": "[%key:common::config_flow::title::reauth%]"
}
}
},
"exceptions": {
"close_door_failed": {
"message": "Failed to close the garage door"
},
"open_door_failed": {
"message": "Failed to open the garage door"
}
}
}

View File

@@ -13,9 +13,6 @@ from homeassistant.helpers import config_validation as cv, service
from .const import DOMAIN
SERVICE_ALARM_TOGGLE_CHIME = "alarm_toggle_chime"
SERVICE_ALARM_KEYPRESS = "alarm_keypress"
ATTR_KEYPRESS = "keypress"
@@ -26,7 +23,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_ALARM_TOGGLE_CHIME,
"alarm_toggle_chime",
entity_domain=ALARM_CONTROL_PANEL_DOMAIN,
schema={
vol.Required(ATTR_CODE): cv.string,
@@ -37,7 +34,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_ALARM_KEYPRESS,
"alarm_keypress",
entity_domain=ALARM_CONTROL_PANEL_DOMAIN,
schema={
vol.Required(ATTR_KEYPRESS): cv.string,

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==12.0.0"]
"requirements": ["aioamazondevices==12.0.2"]
}

View File

@@ -16,9 +16,6 @@ from .coordinator import AmazonConfigEntry
ATTR_TEXT_COMMAND = "text_command"
ATTR_SOUND = "sound"
ATTR_INFO_SKILL = "info_skill"
SERVICE_TEXT_COMMAND = "send_text_command"
SERVICE_SOUND_NOTIFICATION = "send_sound"
SERVICE_INFO_SKILL = "send_info_skill"
SCHEMA_SOUND_SERVICE = vol.Schema(
{
@@ -128,17 +125,17 @@ def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the services for the Amazon Devices integration."""
for service_name, method, schema in (
(
SERVICE_SOUND_NOTIFICATION,
"send_sound",
async_send_sound_notification,
SCHEMA_SOUND_SERVICE,
),
(
SERVICE_TEXT_COMMAND,
"send_text_command",
async_send_text_command,
SCHEMA_CUSTOM_COMMAND,
),
(
SERVICE_INFO_SKILL,
"send_info_skill",
async_send_info_skill,
SCHEMA_INFO_SKILL,
),

View File

@@ -16,8 +16,6 @@ ATTRIBUTION = "Data provided by Amber Electric"
LOGGER = logging.getLogger(__package__)
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
SERVICE_GET_FORECASTS = "get_forecasts"
GENERAL_CHANNEL = "general"
CONTROLLED_LOAD_CHANNEL = "controlled_load"
FEED_IN_CHANNEL = "feed_in"

View File

@@ -22,7 +22,6 @@ from .const import (
DOMAIN,
FEED_IN_CHANNEL,
GENERAL_CHANNEL,
SERVICE_GET_FORECASTS,
)
from .coordinator import AmberConfigEntry
from .helpers import format_cents_to_dollars, normalize_descriptor
@@ -101,7 +100,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
hass.services.async_register(
DOMAIN,
SERVICE_GET_FORECASTS,
"get_forecasts",
handle_get_forecasts,
GET_FORECASTS_SCHEMA,
supports_response=SupportsResponse.ONLY,

View File

@@ -49,18 +49,6 @@ SCAN_INTERVAL = timedelta(seconds=15)
STREAM_SOURCE_LIST = ["snapshot", "mjpeg", "rtsp"]
_SRV_EN_REC = "enable_recording"
_SRV_DS_REC = "disable_recording"
_SRV_EN_AUD = "enable_audio"
_SRV_DS_AUD = "disable_audio"
_SRV_EN_MOT_REC = "enable_motion_recording"
_SRV_DS_MOT_REC = "disable_motion_recording"
_SRV_GOTO = "goto_preset"
_SRV_CBW = "set_color_bw"
_SRV_TOUR_ON = "start_tour"
_SRV_TOUR_OFF = "stop_tour"
_SRV_PTZ_CTRL = "ptz_control"
_ATTR_PTZ_TT = "travel_time"
_ATTR_PTZ_MOV = "movement"
_MOV = [
@@ -103,17 +91,17 @@ _SRV_PTZ_SCHEMA = _SRV_SCHEMA.extend(
)
CAMERA_SERVICES = {
_SRV_EN_REC: (_SRV_SCHEMA, "async_enable_recording", ()),
_SRV_DS_REC: (_SRV_SCHEMA, "async_disable_recording", ()),
_SRV_EN_AUD: (_SRV_SCHEMA, "async_enable_audio", ()),
_SRV_DS_AUD: (_SRV_SCHEMA, "async_disable_audio", ()),
_SRV_EN_MOT_REC: (_SRV_SCHEMA, "async_enable_motion_recording", ()),
_SRV_DS_MOT_REC: (_SRV_SCHEMA, "async_disable_motion_recording", ()),
_SRV_GOTO: (_SRV_GOTO_SCHEMA, "async_goto_preset", (_ATTR_PRESET,)),
_SRV_CBW: (_SRV_CBW_SCHEMA, "async_set_color_bw", (_ATTR_COLOR_BW,)),
_SRV_TOUR_ON: (_SRV_SCHEMA, "async_start_tour", ()),
_SRV_TOUR_OFF: (_SRV_SCHEMA, "async_stop_tour", ()),
_SRV_PTZ_CTRL: (
"enable_recording": (_SRV_SCHEMA, "async_enable_recording", ()),
"disable_recording": (_SRV_SCHEMA, "async_disable_recording", ()),
"enable_audio": (_SRV_SCHEMA, "async_enable_audio", ()),
"disable_audio": (_SRV_SCHEMA, "async_disable_audio", ()),
"enable_motion_recording": (_SRV_SCHEMA, "async_enable_motion_recording", ()),
"disable_motion_recording": (_SRV_SCHEMA, "async_disable_motion_recording", ()),
"goto_preset": (_SRV_GOTO_SCHEMA, "async_goto_preset", (_ATTR_PRESET,)),
"set_color_bw": (_SRV_CBW_SCHEMA, "async_set_color_bw", (_ATTR_COLOR_BW,)),
"start_tour": (_SRV_SCHEMA, "async_start_tour", ()),
"stop_tour": (_SRV_SCHEMA, "async_stop_tour", ()),
"ptz_control": (
_SRV_PTZ_SCHEMA,
"async_ptz_control",
(_ATTR_PTZ_MOV, _ATTR_PTZ_TT),

View File

@@ -36,7 +36,7 @@ from .const import (
SIGNAL_CONFIG_ENTITY,
)
from .entity import AndroidTVEntity, adb_decorator
from .services import ATTR_ADB_RESPONSE, ATTR_HDMI_INPUT, SERVICE_LEARN_SENDEVENT
from .services import ATTR_ADB_RESPONSE, ATTR_HDMI_INPUT
_LOGGER = logging.getLogger(__name__)
@@ -271,7 +271,7 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
self.async_write_ha_state()
msg = (
f"Output from service '{SERVICE_LEARN_SENDEVENT}' from"
f"Output from service 'learn_sendevent' from"
f" {self.entity_id}: '{output}'"
)
persistent_notification.async_create(

View File

@@ -16,11 +16,6 @@ ATTR_DEVICE_PATH = "device_path"
ATTR_HDMI_INPUT = "hdmi_input"
ATTR_LOCAL_PATH = "local_path"
SERVICE_ADB_COMMAND = "adb_command"
SERVICE_DOWNLOAD = "download"
SERVICE_LEARN_SENDEVENT = "learn_sendevent"
SERVICE_UPLOAD = "upload"
@callback
def async_setup_services(hass: HomeAssistant) -> None:
@@ -29,7 +24,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_ADB_COMMAND,
"adb_command",
entity_domain=MEDIA_PLAYER_DOMAIN,
schema={vol.Required(ATTR_COMMAND): cv.string},
func="adb_command",
@@ -37,7 +32,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_LEARN_SENDEVENT,
"learn_sendevent",
entity_domain=MEDIA_PLAYER_DOMAIN,
schema=None,
func="learn_sendevent",
@@ -45,7 +40,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_DOWNLOAD,
"download",
entity_domain=MEDIA_PLAYER_DOMAIN,
schema={
vol.Required(ATTR_DEVICE_PATH): cv.string,
@@ -56,7 +51,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_UPLOAD,
"upload",
entity_domain=MEDIA_PLAYER_DOMAIN,
schema={
vol.Required(ATTR_DEVICE_PATH): cv.string,

View File

@@ -46,6 +46,7 @@ class AnthropicTaskEntity(
ai_task.AITaskEntityFeature.GENERATE_DATA
| ai_task.AITaskEntityFeature.SUPPORT_ATTACHMENTS
)
_attr_translation_key = "ai_task_data"
async def _async_generate_data(
self,

View File

@@ -43,7 +43,9 @@ from homeassistant.helpers.selector import (
from homeassistant.helpers.typing import VolDictType
from .const import (
CODE_EXECUTION_UNSUPPORTED_MODELS,
CONF_CHAT_MODEL,
CONF_CODE_EXECUTION,
CONF_MAX_TOKENS,
CONF_PROMPT,
CONF_RECOMMENDED,
@@ -415,6 +417,16 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
else:
self.options.pop(CONF_THINKING_EFFORT, None)
if not model.startswith(tuple(CODE_EXECUTION_UNSUPPORTED_MODELS)):
step_schema[
vol.Optional(
CONF_CODE_EXECUTION,
default=DEFAULT[CONF_CODE_EXECUTION],
)
] = bool
else:
self.options.pop(CONF_CODE_EXECUTION, None)
if not model.startswith(tuple(WEB_SEARCH_UNSUPPORTED_MODELS)):
step_schema.update(
{

View File

@@ -11,6 +11,7 @@ DEFAULT_AI_TASK_NAME = "Claude AI Task"
CONF_RECOMMENDED = "recommended"
CONF_PROMPT = "prompt"
CONF_CHAT_MODEL = "chat_model"
CONF_CODE_EXECUTION = "code_execution"
CONF_MAX_TOKENS = "max_tokens"
CONF_TEMPERATURE = "temperature"
CONF_THINKING_BUDGET = "thinking_budget"
@@ -25,6 +26,7 @@ CONF_WEB_SEARCH_TIMEZONE = "timezone"
DEFAULT = {
CONF_CHAT_MODEL: "claude-haiku-4-5",
CONF_CODE_EXECUTION: False,
CONF_MAX_TOKENS: 3000,
CONF_TEMPERATURE: 1.0,
CONF_THINKING_BUDGET: 0,
@@ -65,6 +67,10 @@ WEB_SEARCH_UNSUPPORTED_MODELS = [
"claude-3-haiku",
]
CODE_EXECUTION_UNSUPPORTED_MODELS = [
"claude-3-haiku",
]
DEPRECATED_MODELS = [
"claude-3",
]

View File

@@ -37,6 +37,7 @@ class AnthropicConversationEntity(
"""Anthropic conversation agent."""
_attr_supports_streaming = True
_attr_translation_key = "conversation"
def __init__(self, entry: AnthropicConfigEntry, subentry: ConfigSubentry) -> None:
"""Initialize the agent."""

View File

@@ -3,19 +3,23 @@
import base64
from collections.abc import AsyncGenerator, Callable, Iterable
from dataclasses import dataclass, field
from datetime import UTC, datetime
import json
from mimetypes import guess_file_type
from pathlib import Path
from typing import Any
from typing import Any, Literal, cast
import anthropic
from anthropic import AsyncStream
from anthropic.types import (
Base64ImageSourceParam,
Base64PDFSourceParam,
BashCodeExecutionToolResultBlock,
CitationsDelta,
CitationsWebSearchResultLocation,
CitationWebSearchResultLocationParam,
CodeExecutionTool20250825Param,
Container,
ContentBlockParam,
DocumentBlockParam,
ImageBlockParam,
@@ -41,6 +45,7 @@ from anthropic.types import (
TextCitation,
TextCitationParam,
TextDelta,
TextEditorCodeExecutionToolResultBlock,
ThinkingBlock,
ThinkingBlockParam,
ThinkingConfigAdaptiveParam,
@@ -51,18 +56,21 @@ from anthropic.types import (
ToolChoiceAutoParam,
ToolChoiceToolParam,
ToolParam,
ToolResultBlockParam,
ToolUnionParam,
ToolUseBlock,
ToolUseBlockParam,
Usage,
WebSearchTool20250305Param,
WebSearchToolRequestErrorParam,
WebSearchToolResultBlock,
WebSearchToolResultBlockParam,
WebSearchToolResultError,
WebSearchToolResultBlockParamContentParam,
)
from anthropic.types.bash_code_execution_tool_result_block_param import (
Content as BashCodeExecutionToolResultContentParam,
)
from anthropic.types.message_create_params import MessageCreateParamsStreaming
from anthropic.types.text_editor_code_execution_tool_result_block_param import (
Content as TextEditorCodeExecutionToolResultContentParam,
)
import voluptuous as vol
from voluptuous_openapi import convert
@@ -74,10 +82,12 @@ from homeassistant.helpers import device_registry as dr, llm
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.json import json_dumps
from homeassistant.util import slugify
from homeassistant.util.json import JsonObjectType
from . import AnthropicConfigEntry
from .const import (
CONF_CHAT_MODEL,
CONF_CODE_EXECUTION,
CONF_MAX_TOKENS,
CONF_TEMPERATURE,
CONF_THINKING_BUDGET,
@@ -134,6 +144,7 @@ class ContentDetails:
citation_details: list[CitationDetails] = field(default_factory=list)
thinking_signature: str | None = None
redacted_thinking: str | None = None
container: Container | None = None
def has_content(self) -> bool:
"""Check if there is any text content."""
@@ -144,6 +155,7 @@ class ContentDetails:
return (
self.thinking_signature is not None
or self.redacted_thinking is not None
or self.container is not None
or self.has_citations()
)
@@ -188,30 +200,53 @@ class ContentDetails:
def _convert_content(
chat_content: Iterable[conversation.Content],
) -> list[MessageParam]:
) -> tuple[list[MessageParam], str | None]:
"""Transform HA chat_log content into Anthropic API format."""
messages: list[MessageParam] = []
container_id: str | None = None
for content in chat_content:
if isinstance(content, conversation.ToolResultContent):
external_tool = True
if content.tool_name == "web_search":
tool_result_block: ContentBlockParam = WebSearchToolResultBlockParam(
type="web_search_tool_result",
tool_use_id=content.tool_call_id,
content=content.tool_result["content"]
if "content" in content.tool_result
else WebSearchToolRequestErrorParam(
type="web_search_tool_result_error",
error_code=content.tool_result.get("error_code", "unavailable"), # type: ignore[typeddict-item]
tool_result_block: ContentBlockParam = {
"type": "web_search_tool_result",
"tool_use_id": content.tool_call_id,
"content": cast(
WebSearchToolResultBlockParamContentParam,
content.tool_result["content"]
if "content" in content.tool_result
else {
"type": "web_search_tool_result_error",
"error_code": content.tool_result.get(
"error_code", "unavailable"
),
},
),
)
external_tool = True
}
elif content.tool_name == "bash_code_execution":
tool_result_block = {
"type": "bash_code_execution_tool_result",
"tool_use_id": content.tool_call_id,
"content": cast(
BashCodeExecutionToolResultContentParam, content.tool_result
),
}
elif content.tool_name == "text_editor_code_execution":
tool_result_block = {
"type": "text_editor_code_execution_tool_result",
"tool_use_id": content.tool_call_id,
"content": cast(
TextEditorCodeExecutionToolResultContentParam,
content.tool_result,
),
}
else:
tool_result_block = ToolResultBlockParam(
type="tool_result",
tool_use_id=content.tool_call_id,
content=json_dumps(content.tool_result),
)
tool_result_block = {
"type": "tool_result",
"tool_use_id": content.tool_call_id,
"content": json_dumps(content.tool_result),
}
external_tool = False
if not messages or messages[-1]["role"] != (
"assistant" if external_tool else "user"
@@ -277,6 +312,11 @@ def _convert_content(
data=content.native.redacted_thinking,
)
)
if (
content.native.container is not None
and content.native.container.expires_at > datetime.now(UTC)
):
container_id = content.native.container.id
if content.content:
current_index = 0
@@ -325,10 +365,23 @@ def _convert_content(
ServerToolUseBlockParam(
type="server_tool_use",
id=tool_call.id,
name="web_search",
name=cast(
Literal[
"web_search",
"bash_code_execution",
"text_editor_code_execution",
],
tool_call.tool_name,
),
input=tool_call.tool_args,
)
if tool_call.external and tool_call.tool_name == "web_search"
if tool_call.external
and tool_call.tool_name
in [
"web_search",
"bash_code_execution",
"text_editor_code_execution",
]
else ToolUseBlockParam(
type="tool_use",
id=tool_call.id,
@@ -347,10 +400,10 @@ def _convert_content(
# If there is only one text block, simplify the content to a string
messages[-1]["content"] = messages[-1]["content"][0]["text"]
else:
# Note: We don't pass SystemContent here as its passed to the API as the prompt
raise TypeError(f"Unexpected content type: {type(content)}")
# Note: We don't pass SystemContent here as it's passed to the API as the prompt
raise HomeAssistantError("Unexpected content type in chat log")
return messages
return messages, container_id
async def _transform_stream( # noqa: C901 - This is complex, but better to have it in one place
@@ -389,8 +442,8 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
Each message could contain multiple blocks of the same type.
"""
if stream is None:
raise TypeError("Expected a stream of messages")
if stream is None or not hasattr(stream, "__aiter__"):
raise HomeAssistantError("Expected a stream of messages")
current_tool_block: ToolUseBlockParam | ServerToolUseBlockParam | None = None
current_tool_args: str
@@ -403,8 +456,6 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
LOGGER.debug("Received response: %s", response)
if isinstance(response, RawMessageStartEvent):
if response.message.role != "assistant":
raise ValueError("Unexpected message role")
input_usage = response.message.usage
first_block = True
elif isinstance(response, RawContentBlockStartEvent):
@@ -478,7 +529,14 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
input={},
)
current_tool_args = ""
elif isinstance(response.content_block, WebSearchToolResultBlock):
elif isinstance(
response.content_block,
(
WebSearchToolResultBlock,
BashCodeExecutionToolResultBlock,
TextEditorCodeExecutionToolResultBlock,
),
):
if content_details:
content_details.delete_empty()
yield {"native": content_details}
@@ -487,26 +545,16 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
yield {
"role": "tool_result",
"tool_call_id": response.content_block.tool_use_id,
"tool_name": "web_search",
"tool_name": response.content_block.type.removesuffix(
"_tool_result"
),
"tool_result": {
"type": "web_search_tool_result_error",
"error_code": response.content_block.content.error_code,
"content": cast(
JsonObjectType, response.content_block.to_dict()["content"]
)
}
if isinstance(
response.content_block.content, WebSearchToolResultError
)
else {
"content": [
{
"type": "web_search_result",
"encrypted_content": block.encrypted_content,
"page_age": block.page_age,
"title": block.title,
"url": block.url,
}
for block in response.content_block.content
]
},
if isinstance(response.content_block.content, list)
else cast(JsonObjectType, response.content_block.content.to_dict()),
}
first_block = True
elif isinstance(response, RawContentBlockDeltaEvent):
@@ -555,6 +603,7 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
elif isinstance(response, RawMessageDeltaEvent):
if (usage := response.usage) is not None:
chat_log.async_trace(_create_token_stats(input_usage, usage))
content_details.container = response.delta.container
if response.delta.stop_reason == "refusal":
raise HomeAssistantError("Potential policy violation detected")
elif isinstance(response, RawMessageStopEvent):
@@ -615,7 +664,7 @@ class AnthropicBaseLLMEntity(Entity):
system = chat_log.content[0]
if not isinstance(system, conversation.SystemContent):
raise TypeError("First message must be a system message")
raise HomeAssistantError("First message must be a system message")
# System prompt with caching enabled
system_prompt: list[TextBlockParam] = [
@@ -626,7 +675,7 @@ class AnthropicBaseLLMEntity(Entity):
)
]
messages = _convert_content(chat_log.content[1:])
messages, container_id = _convert_content(chat_log.content[1:])
model = options.get(CONF_CHAT_MODEL, DEFAULT[CONF_CHAT_MODEL])
@@ -636,6 +685,7 @@ class AnthropicBaseLLMEntity(Entity):
max_tokens=options.get(CONF_MAX_TOKENS, DEFAULT[CONF_MAX_TOKENS]),
system=system_prompt,
stream=True,
container=container_id,
)
if not model.startswith(tuple(NON_ADAPTIVE_THINKING_MODELS)):
@@ -674,6 +724,14 @@ class AnthropicBaseLLMEntity(Entity):
for tool in chat_log.llm_api.tools
]
if options.get(CONF_CODE_EXECUTION):
tools.append(
CodeExecutionTool20250825Param(
name="code_execution",
type="code_execution_20250825",
),
)
if options.get(CONF_WEB_SEARCH):
web_search = WebSearchTool20250305Param(
name="web_search",
@@ -784,21 +842,25 @@ class AnthropicBaseLLMEntity(Entity):
try:
stream = await client.messages.create(**model_args)
messages.extend(
_convert_content(
[
content
async for content in chat_log.async_add_delta_content_stream(
self.entity_id,
_transform_stream(
chat_log,
stream,
output_tool=structure_name or None,
),
)
]
)
new_messages, model_args["container"] = _convert_content(
[
content
async for content in chat_log.async_add_delta_content_stream(
self.entity_id,
_transform_stream(
chat_log,
stream,
output_tool=structure_name or None,
),
)
]
)
messages.extend(new_messages)
except anthropic.AuthenticationError as err:
self.entry.async_start_reauth(self.hass)
raise HomeAssistantError(
"Authentication error with Anthropic API, reauthentication required"
) from err
except anthropic.AnthropicError as err:
raise HomeAssistantError(
f"Sorry, I had a problem talking to Anthropic: {err}"

View File

@@ -0,0 +1,14 @@
{
"entity": {
"ai_task": {
"ai_task_data": {
"default": "mdi:asterisk"
}
},
"conversation": {
"conversation": {
"default": "mdi:asterisk"
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"domain": "anthropic",
"name": "Anthropic Conversation",
"name": "Anthropic",
"after_dependencies": ["assist_pipeline", "intent"],
"codeowners": ["@Shulyaka"],
"config_flow": true,
@@ -8,5 +8,6 @@
"documentation": "https://www.home-assistant.io/integrations/anthropic",
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["anthropic==0.83.0"]
}

View File

@@ -31,10 +31,7 @@ rules:
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: todo
comment: |
Reevaluate exceptions for entity services.
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
@@ -92,7 +89,7 @@ rules:
No entities disabled by default.
entity-translations: todo
exception-translations: todo
icon-translations: todo
icon-translations: done
reconfiguration-flow: done
repair-issues: done
stale-devices:

View File

@@ -69,6 +69,7 @@
},
"model": {
"data": {
"code_execution": "[%key:component::anthropic::config_subentries::conversation::step::model::data::code_execution%]",
"thinking_budget": "[%key:component::anthropic::config_subentries::conversation::step::model::data::thinking_budget%]",
"thinking_effort": "[%key:component::anthropic::config_subentries::conversation::step::model::data::thinking_effort%]",
"user_location": "[%key:component::anthropic::config_subentries::conversation::step::model::data::user_location%]",
@@ -76,6 +77,7 @@
"web_search_max_uses": "[%key:component::anthropic::config_subentries::conversation::step::model::data::web_search_max_uses%]"
},
"data_description": {
"code_execution": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::code_execution%]",
"thinking_budget": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::thinking_budget%]",
"thinking_effort": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::thinking_effort%]",
"user_location": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::user_location%]",
@@ -127,6 +129,7 @@
},
"model": {
"data": {
"code_execution": "Code execution",
"thinking_budget": "Thinking budget",
"thinking_effort": "Thinking effort",
"user_location": "Include home location",
@@ -134,6 +137,7 @@
"web_search_max_uses": "Maximum web searches"
},
"data_description": {
"code_execution": "Allow the model to execute code in a secure sandbox environment, enabling it to analyze data and perform complex calculations.",
"thinking_budget": "The number of tokens the model can use to think about the response out of the total maximum number of tokens. Set to 1024 or greater to enable extended thinking.",
"thinking_effort": "Control how many tokens Claude uses when responding, trading off between response thoroughness and token efficiency",
"user_location": "Localize search results based on home location",

View File

@@ -117,6 +117,7 @@ class SharpAquosTVDevice(MediaPlayerEntity):
| MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.PLAY
)
_attr_volume_step = 2 / 60
def __init__(
self, name: str, remote: sharp_aquos_rc.TV, power_on_enabled: bool = False
@@ -161,22 +162,6 @@ class SharpAquosTVDevice(MediaPlayerEntity):
"""Turn off tvplayer."""
self._remote.power(0)
@_retry
def volume_up(self) -> None:
"""Volume up the media player."""
if self.volume_level is None:
_LOGGER.debug("Unknown volume in volume_up")
return
self._remote.volume(int(self.volume_level * 60) + 2)
@_retry
def volume_down(self) -> None:
"""Volume down media player."""
if self.volume_level is None:
_LOGGER.debug("Unknown volume in volume_down")
return
self._remote.volume(int(self.volume_level * 60) - 2)
@_retry
def set_volume_level(self, volume: float) -> None:
"""Set Volume media player."""

View File

@@ -30,5 +30,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.4"]
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"]
}

View File

@@ -19,7 +19,7 @@ from homeassistant.components.backup import (
from homeassistant.core import HomeAssistant, callback
from . import S3ConfigEntry
from .const import CONF_BUCKET, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
from .const import CONF_BUCKET, CONF_PREFIX, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
from .helpers import async_list_backups_from_s3
_LOGGER = logging.getLogger(__name__)
@@ -100,6 +100,13 @@ class S3BackupAgent(BackupAgent):
self.unique_id = entry.entry_id
self._backup_cache: dict[str, AgentBackup] = {}
self._cache_expiration = time()
self._prefix: str = entry.data.get(CONF_PREFIX, "")
def _with_prefix(self, key: str) -> str:
"""Add prefix to a key if configured."""
if not self._prefix:
return key
return f"{self._prefix}/{key}"
@handle_boto_errors
async def async_download_backup(
@@ -115,7 +122,9 @@ class S3BackupAgent(BackupAgent):
backup = await self._find_backup_by_id(backup_id)
tar_filename, _ = suggested_filenames(backup)
response = await self._client.get_object(Bucket=self._bucket, Key=tar_filename)
response = await self._client.get_object(
Bucket=self._bucket, Key=self._with_prefix(tar_filename)
)
return response["Body"].iter_chunks()
async def async_upload_backup(
@@ -142,7 +151,7 @@ class S3BackupAgent(BackupAgent):
metadata_content = json.dumps(backup.as_dict())
await self._client.put_object(
Bucket=self._bucket,
Key=metadata_filename,
Key=self._with_prefix(metadata_filename),
Body=metadata_content,
)
except BotoCoreError as err:
@@ -169,7 +178,7 @@ class S3BackupAgent(BackupAgent):
await self._client.put_object(
Bucket=self._bucket,
Key=tar_filename,
Key=self._with_prefix(tar_filename),
Body=bytes(file_data),
)
@@ -186,7 +195,7 @@ class S3BackupAgent(BackupAgent):
_LOGGER.debug("Starting multipart upload for %s", tar_filename)
multipart_upload = await self._client.create_multipart_upload(
Bucket=self._bucket,
Key=tar_filename,
Key=self._with_prefix(tar_filename),
)
upload_id = multipart_upload["UploadId"]
try:
@@ -216,7 +225,7 @@ class S3BackupAgent(BackupAgent):
)
part = await cast(Any, self._client).upload_part(
Bucket=self._bucket,
Key=tar_filename,
Key=self._with_prefix(tar_filename),
PartNumber=part_number,
UploadId=upload_id,
Body=part_data.tobytes(),
@@ -244,7 +253,7 @@ class S3BackupAgent(BackupAgent):
)
part = await cast(Any, self._client).upload_part(
Bucket=self._bucket,
Key=tar_filename,
Key=self._with_prefix(tar_filename),
PartNumber=part_number,
UploadId=upload_id,
Body=remaining_data.tobytes(),
@@ -253,7 +262,7 @@ class S3BackupAgent(BackupAgent):
await cast(Any, self._client).complete_multipart_upload(
Bucket=self._bucket,
Key=tar_filename,
Key=self._with_prefix(tar_filename),
UploadId=upload_id,
MultipartUpload={"Parts": parts},
)
@@ -262,7 +271,7 @@ class S3BackupAgent(BackupAgent):
try:
await self._client.abort_multipart_upload(
Bucket=self._bucket,
Key=tar_filename,
Key=self._with_prefix(tar_filename),
UploadId=upload_id,
)
except BotoCoreError:
@@ -283,8 +292,12 @@ class S3BackupAgent(BackupAgent):
tar_filename, metadata_filename = suggested_filenames(backup)
# Delete both the backup file and its metadata file
await self._client.delete_object(Bucket=self._bucket, Key=tar_filename)
await self._client.delete_object(Bucket=self._bucket, Key=metadata_filename)
await self._client.delete_object(
Bucket=self._bucket, Key=self._with_prefix(tar_filename)
)
await self._client.delete_object(
Bucket=self._bucket, Key=self._with_prefix(metadata_filename)
)
# Reset cache after successful deletion
self._cache_expiration = time()
@@ -317,7 +330,9 @@ class S3BackupAgent(BackupAgent):
if time() <= self._cache_expiration:
return self._backup_cache
backups_list = await async_list_backups_from_s3(self._client, self._bucket)
backups_list = await async_list_backups_from_s3(
self._client, self._bucket, self._prefix
)
self._backup_cache = {b.backup_id: b for b in backups_list}
self._cache_expiration = time() + CACHE_TTL

View File

@@ -22,6 +22,7 @@ from .const import (
CONF_ACCESS_KEY_ID,
CONF_BUCKET,
CONF_ENDPOINT_URL,
CONF_PREFIX,
CONF_SECRET_ACCESS_KEY,
DEFAULT_ENDPOINT_URL,
DESCRIPTION_AWS_S3_DOCS_URL,
@@ -39,6 +40,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_ENDPOINT_URL, default=DEFAULT_ENDPOINT_URL): TextSelector(
config=TextSelectorConfig(type=TextSelectorType.URL)
),
vol.Optional(CONF_PREFIX, default=""): cv.string,
}
)
@@ -53,16 +55,20 @@ class S3ConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
if user_input is not None:
self._async_abort_entries_match(
{
CONF_BUCKET: user_input[CONF_BUCKET],
CONF_ENDPOINT_URL: user_input[CONF_ENDPOINT_URL],
}
)
normalized_prefix = user_input.get(CONF_PREFIX, "").strip("/")
# Check for existing entries, treating missing prefix as empty
for entry in self._async_current_entries(include_ignore=False):
entry_prefix = (entry.data.get(CONF_PREFIX) or "").strip("/")
if (
entry.data.get(CONF_BUCKET) == user_input[CONF_BUCKET]
and entry.data.get(CONF_ENDPOINT_URL)
== user_input[CONF_ENDPOINT_URL]
and entry_prefix == normalized_prefix
):
return self.async_abort(reason="already_configured")
if not urlparse(user_input[CONF_ENDPOINT_URL]).hostname.endswith(
AWS_DOMAIN
):
hostname = urlparse(user_input[CONF_ENDPOINT_URL]).hostname
if not hostname or not hostname.endswith(AWS_DOMAIN):
errors[CONF_ENDPOINT_URL] = "invalid_endpoint_url"
else:
try:
@@ -84,9 +90,18 @@ class S3ConfigFlow(ConfigFlow, domain=DOMAIN):
except ConnectionError:
errors[CONF_ENDPOINT_URL] = "cannot_connect"
else:
return self.async_create_entry(
title=user_input[CONF_BUCKET], data=user_input
)
data = dict(user_input)
if not normalized_prefix:
# Do not persist empty optional values
data.pop(CONF_PREFIX, None)
else:
data[CONF_PREFIX] = normalized_prefix
title = user_input[CONF_BUCKET]
if normalized_prefix:
title = f"{title} - {normalized_prefix}"
return self.async_create_entry(title=title, data=data)
return self.async_show_form(
step_id="user",

View File

@@ -11,6 +11,7 @@ CONF_ACCESS_KEY_ID = "access_key_id"
CONF_SECRET_ACCESS_KEY = "secret_access_key"
CONF_ENDPOINT_URL = "endpoint_url"
CONF_BUCKET = "bucket"
CONF_PREFIX = "prefix"
AWS_DOMAIN = "amazonaws.com"
DEFAULT_ENDPOINT_URL = f"https://s3.eu-central-1.{AWS_DOMAIN}/"

View File

@@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_BUCKET, DOMAIN
from .const import CONF_BUCKET, CONF_PREFIX, DOMAIN
from .helpers import async_list_backups_from_s3
SCAN_INTERVAL = timedelta(hours=6)
@@ -53,11 +53,14 @@ class S3DataUpdateCoordinator(DataUpdateCoordinator[SensorData]):
)
self.client = client
self._bucket: str = entry.data[CONF_BUCKET]
self._prefix: str = entry.data.get(CONF_PREFIX, "")
async def _async_update_data(self) -> SensorData:
"""Fetch data from AWS S3."""
try:
backups = await async_list_backups_from_s3(self.client, self._bucket)
backups = await async_list_backups_from_s3(
self.client, self._bucket, self._prefix
)
except BotoCoreError as error:
raise UpdateFailed(
translation_domain=DOMAIN,

View File

@@ -0,0 +1,55 @@
"""Diagnostics support for AWS S3."""
from __future__ import annotations
import dataclasses
from typing import Any
from homeassistant.components.backup import (
DATA_MANAGER as BACKUP_DATA_MANAGER,
BackupManager,
)
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.core import HomeAssistant
from .const import (
CONF_ACCESS_KEY_ID,
CONF_BUCKET,
CONF_PREFIX,
CONF_SECRET_ACCESS_KEY,
DOMAIN,
)
from .coordinator import S3ConfigEntry
from .helpers import async_list_backups_from_s3
TO_REDACT = (CONF_ACCESS_KEY_ID, CONF_SECRET_ACCESS_KEY)
async def async_get_config_entry_diagnostics(
hass: HomeAssistant,
entry: S3ConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data
backup_manager: BackupManager = hass.data[BACKUP_DATA_MANAGER]
backups = await async_list_backups_from_s3(
coordinator.client,
bucket=entry.data[CONF_BUCKET],
prefix=entry.data.get(CONF_PREFIX, ""),
)
data = {
"coordinator_data": dataclasses.asdict(coordinator.data),
"config": {
**entry.data,
**entry.options,
},
"backup_agents": [
{"name": agent.name}
for agent in backup_manager.backup_agents.values()
if agent.domain == DOMAIN
],
"backup": [backup.as_dict() for backup in backups],
}
return async_redact_data(data, TO_REDACT)

View File

@@ -17,11 +17,17 @@ _LOGGER = logging.getLogger(__name__)
async def async_list_backups_from_s3(
client: S3Client,
bucket: str,
prefix: str,
) -> list[AgentBackup]:
"""List backups from an S3 bucket by reading metadata files."""
paginator = client.get_paginator("list_objects_v2")
metadata_files: list[dict[str, Any]] = []
async for page in paginator.paginate(Bucket=bucket):
list_kwargs: dict[str, Any] = {"Bucket": bucket}
if prefix:
list_kwargs["Prefix"] = prefix + "/"
async for page in paginator.paginate(**list_kwargs):
metadata_files.extend(
obj
for obj in page.get("Contents", [])

View File

@@ -23,7 +23,9 @@ rules:
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
unique-config-entry:
status: exempt
comment: Hassfest does not recognize the duplicate prevention logic. Duplicate entries are prevented by checking bucket, endpoint URL, and prefix in the config flow.
# Silver
action-exceptions:
@@ -36,14 +38,14 @@ rules:
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: todo
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
test-coverage: done
# Gold
devices: done
diagnostics: todo
diagnostics: done
discovery-update-info:
status: exempt
comment: S3 is a cloud service that is not discovered on the network.

View File

@@ -15,12 +15,14 @@
"access_key_id": "Access key ID",
"bucket": "Bucket name",
"endpoint_url": "Endpoint URL",
"prefix": "Prefix",
"secret_access_key": "Secret access key"
},
"data_description": {
"access_key_id": "Access key ID to connect to AWS S3 API",
"bucket": "Bucket must already exist and be writable by the provided credentials.",
"endpoint_url": "Endpoint URL provided to [Boto3 Session]({boto3_docs_url}). Region-specific [AWS S3 endpoints]({aws_s3_docs_url}) are available in their docs.",
"prefix": "Folder or prefix to store backups in, for example `backups`",
"secret_access_key": "Secret access key to connect to AWS S3 API"
},
"title": "Add AWS S3 bucket"

View File

@@ -43,11 +43,11 @@
"title": "The backup location {agent_id} is unavailable"
},
"automatic_backup_failed_addons": {
"description": "Add-ons {failed_addons} could not be included in automatic backup. Please check the Supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured.",
"title": "Not all add-ons could be included in automatic backup"
"description": "Apps {failed_addons} could not be included in automatic backup. Please check the Supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured.",
"title": "Not all apps could be included in automatic backup"
},
"automatic_backup_failed_agents_addons_folders": {
"description": "The automatic backup was created with errors:\n* Locations which the backup could not be uploaded to: {failed_agents}\n* Add-ons which could not be backed up: {failed_addons}\n* Folders which could not be backed up: {failed_folders}\n\nPlease check the Core and Supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured.",
"description": "The automatic backup was created with errors:\n* Locations which the backup could not be uploaded to: {failed_agents}\n* Apps which could not be backed up: {failed_addons}\n* Folders which could not be backed up: {failed_folders}\n\nPlease check the Core and Supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured.",
"title": "Automatic backup was created with errors"
},
"automatic_backup_failed_create": {

View File

@@ -16,6 +16,7 @@ from typing import IO, Any, cast
import aiohttp
from securetar import (
InvalidPasswordError,
SecureTarArchive,
SecureTarError,
SecureTarFile,
@@ -165,7 +166,7 @@ def validate_password(path: Path, password: str | None) -> bool:
):
# If we can read the tar file, the password is correct
return True
except tarfile.ReadError, SecureTarReadError:
except tarfile.ReadError, InvalidPasswordError, SecureTarReadError:
LOGGER.debug("Invalid password")
return False
except Exception: # noqa: BLE001
@@ -192,13 +193,14 @@ def validate_password_stream(
for obj in input_archive.tar:
if not obj.name.endswith((".tar", ".tgz", ".tar.gz")):
continue
with input_archive.extract_tar(obj) as decrypted:
if decrypted.plaintext_size is None:
raise UnsupportedSecureTarVersion
try:
try:
with input_archive.extract_tar(obj) as decrypted:
if decrypted.plaintext_size is None:
raise UnsupportedSecureTarVersion
decrypted.read(1) # Read a single byte to trigger the decryption
except SecureTarReadError as err:
raise IncorrectPassword from err
except (InvalidPasswordError, SecureTarReadError) as err:
raise IncorrectPassword from err
else:
return
raise BackupEmpty

View File

@@ -190,7 +190,7 @@ class BitcoinSensor(SensorEntity):
elif sensor_type == "miners_revenue_usd":
self._attr_native_value = f"{stats.miners_revenue_usd:.0f}"
elif sensor_type == "btc_mined":
self._attr_native_value = str(stats.btc_mined * 0.00000001)
self._attr_native_value = str(stats.btc_mined * 1e-8)
elif sensor_type == "trade_volume_usd":
self._attr_native_value = f"{stats.trade_volume_usd:.1f}"
elif sensor_type == "difficulty":
@@ -208,13 +208,13 @@ class BitcoinSensor(SensorEntity):
elif sensor_type == "blocks_size":
self._attr_native_value = f"{stats.blocks_size:.1f}"
elif sensor_type == "total_fees_btc":
self._attr_native_value = f"{stats.total_fees_btc * 0.00000001:.2f}"
self._attr_native_value = f"{stats.total_fees_btc * 1e-8:.2f}"
elif sensor_type == "total_btc_sent":
self._attr_native_value = f"{stats.total_btc_sent * 0.00000001:.2f}"
self._attr_native_value = f"{stats.total_btc_sent * 1e-8:.2f}"
elif sensor_type == "estimated_btc_sent":
self._attr_native_value = f"{stats.estimated_btc_sent * 0.00000001:.2f}"
self._attr_native_value = f"{stats.estimated_btc_sent * 1e-8:.2f}"
elif sensor_type == "total_btc":
self._attr_native_value = f"{stats.total_btc * 0.00000001:.2f}"
self._attr_native_value = f"{stats.total_btc * 1e-8:.2f}"
elif sensor_type == "total_blocks":
self._attr_native_value = f"{stats.total_blocks:.0f}"
elif sensor_type == "next_retarget":
@@ -222,7 +222,7 @@ class BitcoinSensor(SensorEntity):
elif sensor_type == "estimated_transaction_volume_usd":
self._attr_native_value = f"{stats.estimated_transaction_volume_usd:.2f}"
elif sensor_type == "miners_revenue_btc":
self._attr_native_value = f"{stats.miners_revenue_btc * 0.00000001:.1f}"
self._attr_native_value = f"{stats.miners_revenue_btc * 1e-8:.1f}"
elif sensor_type == "market_price_usd":
self._attr_native_value = f"{stats.market_price_usd:.2f}"

View File

@@ -85,6 +85,7 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
_attr_media_content_type = MediaType.MUSIC
_attr_has_entity_name = True
_attr_name = None
_attr_volume_step = 0.01
def __init__(
self,
@@ -688,24 +689,6 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
await self._player.play_url(url)
async def async_volume_up(self) -> None:
"""Volume up the media player."""
if self.volume_level is None:
return
new_volume = self.volume_level + 0.01
new_volume = min(1, new_volume)
await self.async_set_volume_level(new_volume)
async def async_volume_down(self) -> None:
"""Volume down the media player."""
if self.volume_level is None:
return
new_volume = self.volume_level - 0.01
new_volume = max(0, new_volume)
await self.async_set_volume_level(new_volume)
async def async_set_volume_level(self, volume: float) -> None:
"""Send volume_up command to media player."""
volume = int(round(volume * 100))

View File

@@ -0,0 +1,291 @@
"""The Brands integration."""
from __future__ import annotations
from collections import deque
from http import HTTPStatus
import logging
from pathlib import Path
from random import SystemRandom
import time
from typing import Any, Final
from aiohttp import ClientError, hdrs, web
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
from homeassistant.core import HomeAssistant, callback, valid_domain
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_custom_components
from .const import (
ALLOWED_IMAGES,
BRANDS_CDN_URL,
CACHE_TTL,
CATEGORY_RE,
CDN_TIMEOUT,
DOMAIN,
HARDWARE_IMAGE_RE,
IMAGE_FALLBACKS,
PLACEHOLDER,
TOKEN_CHANGE_INTERVAL,
)
_LOGGER = logging.getLogger(__name__)
_RND: Final = SystemRandom()
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Brands integration."""
access_tokens: deque[str] = deque([], 2)
access_tokens.append(hex(_RND.getrandbits(256))[2:])
hass.data[DOMAIN] = access_tokens
@callback
def _rotate_token(_now: Any) -> None:
"""Rotate the access token."""
access_tokens.append(hex(_RND.getrandbits(256))[2:])
async_track_time_interval(hass, _rotate_token, TOKEN_CHANGE_INTERVAL)
hass.http.register_view(BrandsIntegrationView(hass))
hass.http.register_view(BrandsHardwareView(hass))
websocket_api.async_register_command(hass, ws_access_token)
return True
@callback
@websocket_api.websocket_command({vol.Required("type"): "brands/access_token"})
def ws_access_token(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Return the current brands access token."""
access_tokens: deque[str] = hass.data[DOMAIN]
connection.send_result(msg["id"], {"token": access_tokens[-1]})
def _read_cached_file_with_marker(
cache_path: Path,
) -> tuple[bytes | None, float] | None:
"""Read a cached file, distinguishing between content and 404 markers.
Returns (content, mtime) where content is None for 404 markers (empty files).
Returns None if the file does not exist at all.
"""
if not cache_path.is_file():
return None
mtime = cache_path.stat().st_mtime
data = cache_path.read_bytes()
if not data:
# Empty file is a 404 marker
return (None, mtime)
return (data, mtime)
def _write_cache_file(cache_path: Path, data: bytes) -> None:
"""Write data to cache file, creating directories as needed."""
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.write_bytes(data)
def _read_brand_file(brand_dir: Path, image: str) -> bytes | None:
"""Read a brand image, trying fallbacks in a single I/O pass."""
for candidate in (image, *IMAGE_FALLBACKS.get(image, ())):
file_path = brand_dir / candidate
if file_path.is_file():
return file_path.read_bytes()
return None
class _BrandsBaseView(HomeAssistantView):
"""Base view for serving brand images."""
requires_auth = False
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the view."""
self._hass = hass
self._cache_dir = Path(hass.config.cache_path(DOMAIN))
def _authenticate(self, request: web.Request) -> None:
"""Authenticate the request using Bearer token or query token."""
access_tokens: deque[str] = self._hass.data[DOMAIN]
authenticated = (
request[KEY_AUTHENTICATED] or request.query.get("token") in access_tokens
)
if not authenticated:
if hdrs.AUTHORIZATION in request.headers:
raise web.HTTPUnauthorized
raise web.HTTPForbidden
async def _serve_from_custom_integration(
self,
domain: str,
image: str,
) -> web.Response | None:
"""Try to serve a brand image from a custom integration."""
custom_components = await async_get_custom_components(self._hass)
if (integration := custom_components.get(domain)) is None:
return None
if not integration.has_branding:
return None
brand_dir = Path(integration.file_path) / "brand"
data = await self._hass.async_add_executor_job(
_read_brand_file, brand_dir, image
)
if data is not None:
return self._build_response(data)
return None
async def _serve_from_cache_or_cdn(
self,
cdn_path: str,
cache_subpath: str,
*,
fallback_placeholder: bool = True,
) -> web.Response:
"""Serve from disk cache, fetching from CDN if needed."""
cache_path = self._cache_dir / cache_subpath
now = time.time()
# Try disk cache
result = await self._hass.async_add_executor_job(
_read_cached_file_with_marker, cache_path
)
if result is not None:
data, mtime = result
# Schedule background refresh if stale
if now - mtime > CACHE_TTL:
self._hass.async_create_background_task(
self._fetch_and_cache(cdn_path, cache_path),
f"brands_refresh_{cache_subpath}",
)
else:
# Cache miss - fetch from CDN
data = await self._fetch_and_cache(cdn_path, cache_path)
if data is None:
if fallback_placeholder:
return await self._serve_placeholder(
image=cache_subpath.rsplit("/", 1)[-1]
)
return web.Response(status=HTTPStatus.NOT_FOUND)
return self._build_response(data)
async def _fetch_and_cache(
self,
cdn_path: str,
cache_path: Path,
) -> bytes | None:
"""Fetch from CDN and write to cache. Returns data or None on 404."""
url = f"{BRANDS_CDN_URL}/{cdn_path}"
session = async_get_clientsession(self._hass)
try:
resp = await session.get(url, timeout=CDN_TIMEOUT)
except ClientError, TimeoutError:
_LOGGER.debug("Failed to fetch brand from CDN: %s", cdn_path)
return None
if resp.status == HTTPStatus.NOT_FOUND:
# Cache the 404 as empty file
await self._hass.async_add_executor_job(_write_cache_file, cache_path, b"")
return None
if resp.status != HTTPStatus.OK:
_LOGGER.debug("Unexpected CDN response %s for %s", resp.status, cdn_path)
return None
data = await resp.read()
await self._hass.async_add_executor_job(_write_cache_file, cache_path, data)
return data
async def _serve_placeholder(self, image: str) -> web.Response:
"""Serve a placeholder image."""
return await self._serve_from_cache_or_cdn(
cdn_path=f"_/{PLACEHOLDER}/{image}",
cache_subpath=f"integrations/{PLACEHOLDER}/{image}",
fallback_placeholder=False,
)
@staticmethod
def _build_response(data: bytes) -> web.Response:
"""Build a response with proper headers."""
return web.Response(
body=data,
content_type="image/png",
)
class BrandsIntegrationView(_BrandsBaseView):
"""Serve integration brand images."""
name = "api:brands:integration"
url = "/api/brands/integration/{domain}/{image}"
async def get(
self,
request: web.Request,
domain: str,
image: str,
) -> web.Response:
"""Handle GET request for an integration brand image."""
self._authenticate(request)
if not valid_domain(domain) or image not in ALLOWED_IMAGES:
return web.Response(status=HTTPStatus.NOT_FOUND)
use_placeholder = request.query.get("placeholder") != "no"
# 1. Try custom integration local files
if (
response := await self._serve_from_custom_integration(domain, image)
) is not None:
return response
# 2. Try cache / CDN (always use direct path for proper 404 caching)
return await self._serve_from_cache_or_cdn(
cdn_path=f"brands/{domain}/{image}",
cache_subpath=f"integrations/{domain}/{image}",
fallback_placeholder=use_placeholder,
)
class BrandsHardwareView(_BrandsBaseView):
"""Serve hardware brand images."""
name = "api:brands:hardware"
url = "/api/brands/hardware/{category}/{image:.+}"
async def get(
self,
request: web.Request,
category: str,
image: str,
) -> web.Response:
"""Handle GET request for a hardware brand image."""
self._authenticate(request)
if not CATEGORY_RE.match(category):
return web.Response(status=HTTPStatus.NOT_FOUND)
# Hardware images have dynamic names like "manufacturer_model.png"
# Validate it ends with .png and contains only safe characters
if not HARDWARE_IMAGE_RE.match(image):
return web.Response(status=HTTPStatus.NOT_FOUND)
cache_subpath = f"hardware/{category}/{image}"
return await self._serve_from_cache_or_cdn(
cdn_path=cache_subpath,
cache_subpath=cache_subpath,
)

View File

@@ -0,0 +1,57 @@
"""Constants for the Brands integration."""
from __future__ import annotations
from datetime import timedelta
import re
from typing import Final
from aiohttp import ClientTimeout
DOMAIN: Final = "brands"
# CDN
BRANDS_CDN_URL: Final = "https://brands.home-assistant.io"
CDN_TIMEOUT: Final = ClientTimeout(total=10)
PLACEHOLDER: Final = "_placeholder"
# Caching
CACHE_TTL: Final = 30 * 24 * 60 * 60 # 30 days in seconds
# Access token
TOKEN_CHANGE_INTERVAL: Final = timedelta(minutes=30)
# Validation
CATEGORY_RE: Final = re.compile(r"^[a-z0-9_]+$")
HARDWARE_IMAGE_RE: Final = re.compile(r"^[a-z0-9_-]+\.png$")
# Images and fallback chains
ALLOWED_IMAGES: Final = frozenset(
{
"icon.png",
"logo.png",
"icon@2x.png",
"logo@2x.png",
"dark_icon.png",
"dark_logo.png",
"dark_icon@2x.png",
"dark_logo@2x.png",
}
)
# Fallback chains for image resolution, mirroring the brands CDN build logic.
# When a requested image is not found, we try each fallback in order.
IMAGE_FALLBACKS: Final[dict[str, list[str]]] = {
"logo.png": ["icon.png"],
"icon@2x.png": ["icon.png"],
"logo@2x.png": ["logo.png", "icon.png"],
"dark_icon.png": ["icon.png"],
"dark_logo.png": ["dark_icon.png", "logo.png", "icon.png"],
"dark_icon@2x.png": ["icon@2x.png", "icon.png"],
"dark_logo@2x.png": [
"dark_icon@2x.png",
"logo@2x.png",
"logo.png",
"icon.png",
],
}

View File

@@ -0,0 +1,10 @@
{
"domain": "brands",
"name": "Brands",
"codeowners": ["@home-assistant/core"],
"config_flow": false,
"dependencies": ["http", "websocket_api"],
"documentation": "https://www.home-assistant.io/integrations/brands",
"integration_type": "system",
"quality_scale": "internal"
}

View File

@@ -1,4 +1,4 @@
"""The BSB-Lan integration."""
"""The BSB-LAN integration."""
import asyncio
import dataclasses
@@ -36,7 +36,7 @@ from .const import CONF_PASSKEY, DOMAIN
from .coordinator import BSBLanFastCoordinator, BSBLanSlowCoordinator
from .services import async_setup_services
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER]
PLATFORMS = [Platform.BUTTON, Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -56,13 +56,13 @@ class BSBLanData:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the BSB-Lan integration."""
"""Set up the BSB-LAN integration."""
async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bool:
"""Set up BSB-Lan from a config entry."""
"""Set up BSB-LAN from a config entry."""
# create config using BSBLANConfig
config = BSBLANConfig(

View File

@@ -0,0 +1,59 @@
"""Button platform for BSB-Lan integration."""
from __future__ import annotations
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BSBLanConfigEntry, BSBLanData
from .coordinator import BSBLanFastCoordinator
from .entity import BSBLanEntity
from .helpers import async_sync_device_time
PARALLEL_UPDATES = 1
BUTTON_DESCRIPTIONS: tuple[ButtonEntityDescription, ...] = (
ButtonEntityDescription(
key="sync_time",
translation_key="sync_time",
entity_category=EntityCategory.CONFIG,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: BSBLanConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up BSB-Lan button entities from a config entry."""
data = entry.runtime_data
async_add_entities(
BSBLanButtonEntity(data.fast_coordinator, data, description)
for description in BUTTON_DESCRIPTIONS
)
class BSBLanButtonEntity(BSBLanEntity, ButtonEntity):
"""Defines a BSB-Lan button entity."""
entity_description: ButtonEntityDescription
def __init__(
self,
coordinator: BSBLanFastCoordinator,
data: BSBLanData,
description: ButtonEntityDescription,
) -> None:
"""Initialize BSB-Lan button entity."""
super().__init__(coordinator, data)
self.entity_description = description
self._attr_unique_id = f"{data.device.MAC}-{description.key}"
self._data = data
async def async_press(self) -> None:
"""Handle the button press."""
await async_sync_device_time(self._data.client, self._data.device.name)

View File

@@ -39,15 +39,15 @@ PRESET_MODES = [
PRESET_NONE,
]
# Mapping from Home Assistant HVACMode to BSB-Lan integer values
# BSB-Lan uses: 0=off, 1=auto, 2=eco/reduced, 3=heat/comfort
# Mapping from Home Assistant HVACMode to BSB-LAN integer values
# BSB-LAN uses: 0=off, 1=auto, 2=eco/reduced, 3=heat/comfort
HA_TO_BSBLAN_HVAC_MODE: Final[dict[HVACMode, int]] = {
HVACMode.OFF: 0,
HVACMode.AUTO: 1,
HVACMode.HEAT: 3,
}
# Mapping from BSB-Lan integer values to Home Assistant HVACMode
# Mapping from BSB-LAN integer values to Home Assistant HVACMode
BSBLAN_TO_HA_HVAC_MODE: Final[dict[int, HVACMode]] = {
0: HVACMode.OFF,
1: HVACMode.AUTO,
@@ -69,7 +69,6 @@ async def async_setup_entry(
class BSBLANClimate(BSBLanEntity, ClimateEntity):
"""Defines a BSBLAN climate device."""
_attr_has_entity_name = True
_attr_name = None
# Determine preset modes
_attr_supported_features = (
@@ -138,7 +137,7 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode."""
# BSB-Lan mode 2 is eco/reduced mode
# BSB-LAN mode 2 is eco/reduced mode
if self._hvac_mode_value == 2:
return PRESET_ECO
return PRESET_NONE
@@ -163,7 +162,7 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
if ATTR_HVAC_MODE in kwargs:
data[ATTR_HVAC_MODE] = HA_TO_BSBLAN_HVAC_MODE[kwargs[ATTR_HVAC_MODE]]
if ATTR_PRESET_MODE in kwargs:
# eco preset uses BSB-Lan mode 2, none preset uses mode 1 (auto)
# eco preset uses BSB-LAN mode 2, none preset uses mode 1 (auto)
if kwargs[ATTR_PRESET_MODE] == PRESET_ECO:
data[ATTR_HVAC_MODE] = 2
elif kwargs[ATTR_PRESET_MODE] == PRESET_NONE:

View File

@@ -1,4 +1,4 @@
"""Config flow for BSB-Lan integration."""
"""Config flow for BSB-LAN integration."""
from __future__ import annotations

View File

@@ -1,4 +1,4 @@
"""Constants for the BSB-Lan integration."""
"""Constants for the BSB-LAN integration."""
from __future__ import annotations

View File

@@ -1,4 +1,4 @@
"""DataUpdateCoordinator for the BSB-Lan integration."""
"""DataUpdateCoordinator for the BSB-LAN integration."""
from __future__ import annotations
@@ -29,8 +29,13 @@ if TYPE_CHECKING:
# Filter lists for optimized API calls - only fetch parameters we actually use
# This significantly reduces response time (~0.2s per parameter saved)
STATE_INCLUDE = ["current_temperature", "target_temperature", "hvac_mode"]
SENSOR_INCLUDE = ["current_temperature", "outside_temperature"]
STATE_INCLUDE = [
"current_temperature",
"target_temperature",
"hvac_mode",
"hvac_action",
]
SENSOR_INCLUDE = ["current_temperature", "outside_temperature", "total_energy"]
DHW_STATE_INCLUDE = [
"operating_mode",
"nominal_setpoint",
@@ -57,7 +62,7 @@ class BSBLanSlowData:
class BSBLanCoordinator[T](DataUpdateCoordinator[T]):
"""Base BSB-Lan coordinator."""
"""Base BSB-LAN coordinator."""
config_entry: BSBLanConfigEntry
@@ -69,7 +74,7 @@ class BSBLanCoordinator[T](DataUpdateCoordinator[T]):
name: str,
update_interval: timedelta,
) -> None:
"""Initialize the BSB-Lan coordinator."""
"""Initialize the BSB-LAN coordinator."""
super().__init__(
hass,
logger=LOGGER,
@@ -81,7 +86,7 @@ class BSBLanCoordinator[T](DataUpdateCoordinator[T]):
class BSBLanFastCoordinator(BSBLanCoordinator[BSBLanFastData]):
"""The BSB-Lan fast update coordinator for frequently changing data."""
"""The BSB-LAN fast update coordinator for frequently changing data."""
def __init__(
self,
@@ -89,7 +94,7 @@ class BSBLanFastCoordinator(BSBLanCoordinator[BSBLanFastData]):
config_entry: BSBLanConfigEntry,
client: BSBLAN,
) -> None:
"""Initialize the BSB-Lan fast coordinator."""
"""Initialize the BSB-LAN fast coordinator."""
super().__init__(
hass,
config_entry,
@@ -99,7 +104,7 @@ class BSBLanFastCoordinator(BSBLanCoordinator[BSBLanFastData]):
)
async def _async_update_data(self) -> BSBLanFastData:
"""Fetch fast-changing data from the BSB-Lan device."""
"""Fetch fast-changing data from the BSB-LAN device."""
try:
# Client is already initialized in async_setup_entry
# Use include filtering to only fetch parameters we actually use
@@ -110,12 +115,15 @@ class BSBLanFastCoordinator(BSBLanCoordinator[BSBLanFastData]):
except BSBLANAuthError as err:
raise ConfigEntryAuthFailed(
"Authentication failed for BSB-Lan device"
translation_domain=DOMAIN,
translation_key="coordinator_auth_error",
) from err
except BSBLANConnectionError as err:
host = self.config_entry.data[CONF_HOST]
raise UpdateFailed(
f"Error while establishing connection with BSB-Lan device at {host}"
translation_domain=DOMAIN,
translation_key="coordinator_connection_error",
translation_placeholders={"host": host},
) from err
return BSBLanFastData(
@@ -126,7 +134,7 @@ class BSBLanFastCoordinator(BSBLanCoordinator[BSBLanFastData]):
class BSBLanSlowCoordinator(BSBLanCoordinator[BSBLanSlowData]):
"""The BSB-Lan slow update coordinator for infrequently changing data."""
"""The BSB-LAN slow update coordinator for infrequently changing data."""
def __init__(
self,
@@ -134,7 +142,7 @@ class BSBLanSlowCoordinator(BSBLanCoordinator[BSBLanSlowData]):
config_entry: BSBLanConfigEntry,
client: BSBLAN,
) -> None:
"""Initialize the BSB-Lan slow coordinator."""
"""Initialize the BSB-LAN slow coordinator."""
super().__init__(
hass,
config_entry,
@@ -144,7 +152,7 @@ class BSBLanSlowCoordinator(BSBLanCoordinator[BSBLanSlowData]):
)
async def _async_update_data(self) -> BSBLanSlowData:
"""Fetch slow-changing data from the BSB-Lan device."""
"""Fetch slow-changing data from the BSB-LAN device."""
try:
# Client is already initialized in async_setup_entry
# Use include filtering to only fetch parameters we actually use

View File

@@ -32,6 +32,15 @@ class BSBLanEntityBase[_T: BSBLanCoordinator](CoordinatorEntity[_T]):
model=(
data.info.device_identification.value
if data.info.device_identification
and data.info.device_identification.value
else None
),
model_id=(
f"{data.info.controller_family.value}_{data.info.controller_variant.value}"
if data.info.controller_family
and data.info.controller_variant
and data.info.controller_family.value
and data.info.controller_variant.value
else None
),
sw_version=data.device.version,

View File

@@ -0,0 +1,42 @@
"""Helper functions for BSB-Lan integration."""
from __future__ import annotations
from bsblan import BSBLAN, BSBLANError
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import dt as dt_util
from .const import DOMAIN
async def async_sync_device_time(client: BSBLAN, device_name: str) -> None:
"""Synchronize BSB-LAN device time with Home Assistant.
Only updates if device time differs from Home Assistant time.
Args:
client: The BSB-LAN client instance.
device_name: The name of the device (used in error messages).
Raises:
HomeAssistantError: If the time sync operation fails.
"""
try:
device_time = await client.time()
current_time = dt_util.now()
current_time_str = current_time.strftime("%d.%m.%Y %H:%M:%S")
# Only sync if device time differs from HA time
if device_time.time.value != current_time_str:
await client.set_time(current_time_str)
except BSBLANError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="sync_time_failed",
translation_placeholders={
"device_name": device_name,
"error": str(err),
},
) from err

View File

@@ -1,4 +1,11 @@
{
"entity": {
"button": {
"sync_time": {
"default": "mdi:timer-sync-outline"
}
}
},
"services": {
"set_hot_water_schedule": {
"service": "mdi:calendar-clock"

View File

@@ -1,13 +1,14 @@
{
"domain": "bsblan",
"name": "BSB-Lan",
"name": "BSB-LAN",
"codeowners": ["@liudger"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bsblan",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["bsblan"],
"requirements": ["python-bsblan==5.0.1"],
"quality_scale": "silver",
"requirements": ["python-bsblan==5.1.0"],
"zeroconf": [
{
"name": "bsb-lan*",

View File

@@ -0,0 +1,74 @@
rules:
# Bronze
action-setup: done
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions: done
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
Entities of this integration does not explicitly subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: done
test-coverage: done
# Gold
devices: done
diagnostics: done
discovery-update-info: done
discovery: done
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices:
status: exempt
comment: |
This integration has a fixed single device.
entity-category: done
entity-device-class: done
entity-disabled-by-default:
status: exempt
comment: |
This integration provides a limited number of entities, all of which are useful to users.
entity-translations: done
exception-translations: done
icon-translations: todo
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: |
This integration doesn't have any cases where raising an issue is needed.
stale-devices:
status: exempt
comment: |
This integration has a fixed single device.
# Platinum
async-dependency: done
inject-websession: done
strict-typing: done

View File

@@ -1,4 +1,4 @@
"""Support for BSB-Lan sensors."""
"""Support for BSB-LAN sensors."""
from __future__ import annotations
@@ -11,7 +11,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import UnitOfTemperature
from homeassistant.const import UnitOfEnergy, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
@@ -25,7 +25,7 @@ PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class BSBLanSensorEntityDescription(SensorEntityDescription):
"""Describes BSB-Lan sensor entity."""
"""Describes BSB-LAN sensor entity."""
value_fn: Callable[[BSBLanFastData], StateType]
exists_fn: Callable[[BSBLanFastData], bool] = lambda data: True
@@ -58,6 +58,21 @@ SENSOR_TYPES: tuple[BSBLanSensorEntityDescription, ...] = (
),
exists_fn=lambda data: data.sensor.outside_temperature is not None,
),
BSBLanSensorEntityDescription(
key="total_energy",
translation_key="total_energy",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=0,
entity_registry_enabled_default=False,
value_fn=lambda data: (
data.sensor.total_energy.value
if data.sensor.total_energy is not None
else None
),
exists_fn=lambda data: data.sensor.total_energy is not None,
),
)
@@ -66,7 +81,7 @@ async def async_setup_entry(
entry: BSBLanConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up BSB-Lan sensor based on a config entry."""
"""Set up BSB-LAN sensor based on a config entry."""
data = entry.runtime_data
# Only create sensors for available data points
@@ -81,7 +96,7 @@ async def async_setup_entry(
class BSBLanSensor(BSBLanEntity, SensorEntity):
"""Defines a BSB-Lan sensor."""
"""Defines a BSB-LAN sensor."""
entity_description: BSBLanSensorEntityDescription
@@ -90,7 +105,7 @@ class BSBLanSensor(BSBLanEntity, SensorEntity):
data: BSBLanData,
description: BSBLanSensorEntityDescription,
) -> None:
"""Initialize BSB-Lan sensor."""
"""Initialize BSB-LAN sensor."""
super().__init__(data.fast_coordinator, data)
self.entity_description = description
self._attr_unique_id = f"{data.device.MAC}-{description.key}"

View File

@@ -1,4 +1,4 @@
"""Support for BSB-Lan services."""
"""Support for BSB-LAN services."""
from __future__ import annotations
@@ -13,9 +13,9 @@ from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.util import dt as dt_util
from .const import DOMAIN
from .helpers import async_sync_device_time
if TYPE_CHECKING:
from . import BSBLanConfigEntry
@@ -31,10 +31,6 @@ ATTR_FRIDAY_SLOTS = "friday_slots"
ATTR_SATURDAY_SLOTS = "saturday_slots"
ATTR_SUNDAY_SLOTS = "sunday_slots"
# Service names
SERVICE_SET_HOT_WATER_SCHEDULE = "set_hot_water_schedule"
SERVICE_SYNC_TIME = "sync_time"
# Schema for a single time slot
_SLOT_SCHEMA = vol.Schema(
@@ -192,7 +188,7 @@ async def set_hot_water_schedule(service_call: ServiceCall) -> None:
)
try:
# Call the BSB-Lan API to set the schedule
# Call the BSB-LAN API to set the schedule
await client.set_hot_water_schedule(dhw_schedule)
except BSBLANError as err:
raise HomeAssistantError(
@@ -245,25 +241,7 @@ async def async_sync_time(service_call: ServiceCall) -> None:
)
client = entry.runtime_data.client
try:
# Get current device time
device_time = await client.time()
current_time = dt_util.now()
current_time_str = current_time.strftime("%d.%m.%Y %H:%M:%S")
# Only sync if device time differs from HA time
if device_time.time.value != current_time_str:
await client.set_time(current_time_str)
except BSBLANError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="sync_time_failed",
translation_placeholders={
"device_name": device_entry.name or device_id,
"error": str(err),
},
) from err
await async_sync_device_time(client, device_entry.name or device_id)
SYNC_TIME_SCHEMA = vol.Schema(
@@ -275,17 +253,17 @@ SYNC_TIME_SCHEMA = vol.Schema(
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register the BSB-Lan services."""
"""Register the BSB-LAN services."""
hass.services.async_register(
DOMAIN,
SERVICE_SET_HOT_WATER_SCHEDULE,
"set_hot_water_schedule",
set_hot_water_schedule,
schema=SERVICE_SET_HOT_WATER_SCHEDULE_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_SYNC_TIME,
"sync_time",
async_sync_time,
schema=SYNC_TIME_SCHEMA,
)

View File

@@ -22,8 +22,8 @@
"password": "[%key:component::bsblan::config::step::user::data_description::password%]",
"username": "[%key:component::bsblan::config::step::user::data_description::username%]"
},
"description": "A BSB-Lan device was discovered at {host}. Please provide credentials if required.",
"title": "BSB-Lan device discovered"
"description": "A BSB-LAN device was discovered at {host}. Please provide credentials if required.",
"title": "BSB-LAN device discovered"
},
"reauth_confirm": {
"data": {
@@ -36,7 +36,7 @@
"password": "[%key:component::bsblan::config::step::user::data_description::password%]",
"username": "[%key:component::bsblan::config::step::user::data_description::username%]"
},
"description": "The BSB-Lan integration needs to re-authenticate with {name}",
"description": "The BSB-LAN integration needs to re-authenticate with {name}",
"title": "[%key:common::config_flow::title::reauth%]"
},
"user": {
@@ -48,24 +48,32 @@
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"host": "The hostname or IP address of your BSB-Lan device.",
"passkey": "The passkey for your BSB-Lan device.",
"password": "The password for your BSB-Lan device.",
"port": "The port number of your BSB-Lan device.",
"username": "The username for your BSB-Lan device."
"host": "The hostname or IP address of your BSB-LAN device.",
"passkey": "The passkey for your BSB-LAN device.",
"password": "The password for your BSB-LAN device.",
"port": "The port number of your BSB-LAN device.",
"username": "The username for your BSB-LAN device."
},
"description": "Set up your BSB-Lan device to integrate with Home Assistant.",
"title": "Connect to the BSB-Lan device"
"description": "Set up your BSB-LAN device to integrate with Home Assistant.",
"title": "Connect to the BSB-LAN device"
}
}
},
"entity": {
"button": {
"sync_time": {
"name": "Sync time"
}
},
"sensor": {
"current_temperature": {
"name": "Current temperature"
},
"outside_temperature": {
"name": "Outside temperature"
},
"total_energy": {
"name": "Total energy"
}
}
},
@@ -73,6 +81,12 @@
"config_entry_not_loaded": {
"message": "The device `{device_name}` is not currently loaded or available"
},
"coordinator_auth_error": {
"message": "Authentication failed for BSB-LAN device"
},
"coordinator_connection_error": {
"message": "Error while establishing connection with BSB-LAN device at {host}"
},
"end_time_before_start_time": {
"message": "End time ({end_time}) must be after start time ({start_time})"
},
@@ -83,14 +97,11 @@
"message": "No configuration entry found for device: {device_id}"
},
"set_data_error": {
"message": "An error occurred while sending the data to the BSB-Lan device"
"message": "An error occurred while sending the data to the BSB-LAN device"
},
"set_operation_mode_error": {
"message": "An error occurred while setting the operation mode"
},
"set_preset_mode_error": {
"message": "Can't set preset mode to {preset_mode} when HVAC mode is not set to auto"
},
"set_schedule_failed": {
"message": "Failed to set hot water schedule: {error}"
},
@@ -101,7 +112,7 @@
"message": "Authentication failed while retrieving static device data"
},
"setup_connection_error": {
"message": "Failed to retrieve static device data from BSB-Lan device at {host}"
"message": "Failed to retrieve static device data from BSB-LAN device at {host}"
},
"setup_general_error": {
"message": "An unknown error occurred while retrieving static device data"
@@ -150,7 +161,7 @@
"name": "Set hot water schedule"
},
"sync_time": {
"description": "Synchronize Home Assistant time to the BSB-Lan device. Only updates if device time differs from Home Assistant time.",
"description": "Synchronize Home Assistant time to the BSB-LAN device. Only updates if device time differs from Home Assistant time.",
"fields": {
"device_id": {
"description": "The BSB-LAN device to sync time for.",

View File

@@ -63,6 +63,7 @@ class BSBLANWaterHeater(BSBLanDualCoordinatorEntity, WaterHeaterEntity):
"""Defines a BSBLAN water heater entity."""
_attr_name = None
_attr_operation_list = list(HA_TO_BSBLAN_OPERATION_MODE.keys())
_attr_supported_features = (
WaterHeaterEntityFeature.TARGET_TEMPERATURE
| WaterHeaterEntityFeature.OPERATION_MODE
@@ -73,7 +74,6 @@ class BSBLANWaterHeater(BSBLanDualCoordinatorEntity, WaterHeaterEntity):
"""Initialize BSBLAN water heater."""
super().__init__(data.fast_coordinator, data.slow_coordinator, data)
self._attr_unique_id = format_mac(data.device.MAC)
self._attr_operation_list = list(HA_TO_BSBLAN_OPERATION_MODE.keys())
# Set temperature unit
self._attr_temperature_unit = data.fast_coordinator.client.get_temperature_unit

View File

@@ -38,7 +38,7 @@ async def _root_payload(
media_class=MediaClass.DIRECTORY,
media_content_id="",
media_content_type="presets",
thumbnail="https://brands.home-assistant.io/_/cambridge_audio/logo.png",
thumbnail="/api/brands/integration/cambridge_audio/logo.png",
can_play=False,
can_expand=True,
)

View File

@@ -807,6 +807,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
# The lovelace app loops media to prevent timing out, don't show that
if self.app_id == CAST_APP_ID_HOMEASSISTANT_LOVELACE:
return MediaPlayerState.PLAYING
if (media_status := self._media_status()[0]) is not None:
if media_status.player_state == MEDIA_PLAYER_STATE_PLAYING:
return MediaPlayerState.PLAYING
@@ -817,19 +818,19 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
if media_status.player_is_idle:
return MediaPlayerState.IDLE
if self._chromecast is not None and self._chromecast.is_idle:
# If library consider us idle, that is our off state
# it takes HDMI status into account for cast devices.
return MediaPlayerState.OFF
if self.app_id in APP_IDS_UNRELIABLE_MEDIA_INFO:
# Some apps don't report media status, show the player as playing
return MediaPlayerState.PLAYING
if self.app_id is not None:
if self.app_id is not None and self.app_id != pychromecast.config.APP_BACKDROP:
# We have an active app
return MediaPlayerState.IDLE
if self._chromecast is not None and self._chromecast.is_idle:
# If library consider us idle, that is our off state
# it takes HDMI status into account for cast devices.
return MediaPlayerState.OFF
return None
@property

View File

@@ -10,9 +10,12 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .coordinator import CompitConfigEntry, CompitDataUpdateCoordinator
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.CLIMATE,
Platform.FAN,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.WATER_HEATER,
]

View File

@@ -0,0 +1,189 @@
"""Binary sensor platform for Compit integration."""
from dataclasses import dataclass
from compit_inext_api.consts import CompitParameter
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER_NAME
from .coordinator import CompitConfigEntry, CompitDataUpdateCoordinator
PARALLEL_UPDATES = 0
NO_SENSOR = "no_sensor"
ON_STATES = ["on", "yes", "charging", "alert", "exceeded"]
DESCRIPTIONS: dict[CompitParameter, BinarySensorEntityDescription] = {
CompitParameter.AIRING: BinarySensorEntityDescription(
key=CompitParameter.AIRING.value,
translation_key="airing",
device_class=BinarySensorDeviceClass.WINDOW,
entity_category=EntityCategory.DIAGNOSTIC,
),
CompitParameter.BATTERY_CHARGE_STATUS: BinarySensorEntityDescription(
key=CompitParameter.BATTERY_CHARGE_STATUS.value,
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
entity_category=EntityCategory.DIAGNOSTIC,
),
CompitParameter.CO2_ALERT: BinarySensorEntityDescription(
key=CompitParameter.CO2_ALERT.value,
translation_key="co2_alert",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
),
CompitParameter.CO2_LEVEL: BinarySensorEntityDescription(
key=CompitParameter.CO2_LEVEL.value,
translation_key="co2_level",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
),
CompitParameter.DUST_ALERT: BinarySensorEntityDescription(
key=CompitParameter.DUST_ALERT.value,
translation_key="dust_alert",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
),
CompitParameter.PUMP_STATUS: BinarySensorEntityDescription(
key=CompitParameter.PUMP_STATUS.value,
translation_key="pump_status",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
),
CompitParameter.TEMPERATURE_ALERT: BinarySensorEntityDescription(
key=CompitParameter.TEMPERATURE_ALERT.value,
translation_key="temperature_alert",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
),
}
@dataclass(frozen=True, kw_only=True)
class CompitDeviceDescription:
"""Class to describe a Compit device."""
name: str
parameters: dict[CompitParameter, BinarySensorEntityDescription]
DEVICE_DEFINITIONS: dict[int, CompitDeviceDescription] = {
12: CompitDeviceDescription(
name="Nano Color",
parameters={
CompitParameter.CO2_LEVEL: DESCRIPTIONS[CompitParameter.CO2_LEVEL],
},
),
78: CompitDeviceDescription(
name="SPM - Nano Color 2",
parameters={
CompitParameter.DUST_ALERT: DESCRIPTIONS[CompitParameter.DUST_ALERT],
CompitParameter.TEMPERATURE_ALERT: DESCRIPTIONS[
CompitParameter.TEMPERATURE_ALERT
],
CompitParameter.CO2_ALERT: DESCRIPTIONS[CompitParameter.CO2_ALERT],
},
),
223: CompitDeviceDescription(
name="Nano Color 2",
parameters={
CompitParameter.AIRING: DESCRIPTIONS[CompitParameter.AIRING],
CompitParameter.CO2_LEVEL: DESCRIPTIONS[CompitParameter.CO2_LEVEL],
},
),
225: CompitDeviceDescription(
name="SPM - Nano Color",
parameters={
CompitParameter.CO2_LEVEL: DESCRIPTIONS[CompitParameter.CO2_LEVEL],
},
),
226: CompitDeviceDescription(
name="AF-1",
parameters={
CompitParameter.BATTERY_CHARGE_STATUS: DESCRIPTIONS[
CompitParameter.BATTERY_CHARGE_STATUS
],
CompitParameter.PUMP_STATUS: DESCRIPTIONS[CompitParameter.PUMP_STATUS],
},
),
}
async def async_setup_entry(
hass: HomeAssistant,
entry: CompitConfigEntry,
async_add_devices: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Compit binary sensor entities from a config entry."""
coordinator = entry.runtime_data
async_add_devices(
CompitBinarySensor(
coordinator,
device_id,
device_definition.name,
code,
entity_description,
)
for device_id, device in coordinator.connector.all_devices.items()
if (device_definition := DEVICE_DEFINITIONS.get(device.definition.code))
for code, entity_description in device_definition.parameters.items()
if coordinator.connector.get_current_value(device_id, code) != NO_SENSOR
)
class CompitBinarySensor(
CoordinatorEntity[CompitDataUpdateCoordinator], BinarySensorEntity
):
"""Representation of a Compit binary sensor entity."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: CompitDataUpdateCoordinator,
device_id: int,
device_name: str,
parameter_code: CompitParameter,
entity_description: BinarySensorEntityDescription,
) -> None:
"""Initialize the binary sensor entity."""
super().__init__(coordinator)
self.device_id = device_id
self.entity_description = entity_description
self._attr_unique_id = f"{device_id}_{entity_description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, str(device_id))},
name=device_name,
manufacturer=MANUFACTURER_NAME,
model=device_name,
)
self.parameter_code = parameter_code
@property
def available(self) -> bool:
"""Return if entity is available."""
return (
super().available
and self.coordinator.connector.get_device(self.device_id) is not None
)
@property
def is_on(self) -> bool | None:
"""Return the state of the binary sensor."""
value = self.coordinator.connector.get_current_value(
self.device_id, self.parameter_code
)
if value is None:
return None
return value in ON_STATES

View File

@@ -0,0 +1,172 @@
"""Fan platform for Compit integration."""
from typing import Any
from compit_inext_api import PARAM_VALUES
from compit_inext_api.consts import CompitParameter
from homeassistant.components.fan import (
FanEntity,
FanEntityDescription,
FanEntityFeature,
)
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.percentage import (
ordered_list_item_to_percentage,
percentage_to_ordered_list_item,
)
from .const import DOMAIN, MANUFACTURER_NAME
from .coordinator import CompitConfigEntry, CompitDataUpdateCoordinator
PARALLEL_UPDATES = 0
COMPIT_GEAR_TO_HA = PARAM_VALUES[CompitParameter.VENTILATION_GEAR_TARGET]
HA_STATE_TO_COMPIT = {value: key for key, value in COMPIT_GEAR_TO_HA.items()}
DEVICE_DEFINITIONS: dict[int, FanEntityDescription] = {
223: FanEntityDescription(
key="Nano Color 2",
translation_key="ventilation",
),
12: FanEntityDescription(
key="Nano Color",
translation_key="ventilation",
),
}
async def async_setup_entry(
hass: HomeAssistant,
entry: CompitConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Compit fan entities from a config entry."""
coordinator = entry.runtime_data
async_add_entities(
CompitFan(
coordinator,
device_id,
device_definition,
)
for device_id, device in coordinator.connector.all_devices.items()
if (device_definition := DEVICE_DEFINITIONS.get(device.definition.code))
)
class CompitFan(CoordinatorEntity[CompitDataUpdateCoordinator], FanEntity):
"""Representation of a Compit fan entity."""
_attr_speed_count = len(COMPIT_GEAR_TO_HA)
_attr_has_entity_name = True
_attr_name = None
_attr_supported_features = (
FanEntityFeature.TURN_ON
| FanEntityFeature.TURN_OFF
| FanEntityFeature.SET_SPEED
)
def __init__(
self,
coordinator: CompitDataUpdateCoordinator,
device_id: int,
entity_description: FanEntityDescription,
) -> None:
"""Initialize the fan entity."""
super().__init__(coordinator)
self.device_id = device_id
self.entity_description = entity_description
self._attr_unique_id = f"{device_id}_{entity_description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, str(device_id))},
name=entity_description.key,
manufacturer=MANUFACTURER_NAME,
model=entity_description.key,
)
@property
def available(self) -> bool:
"""Return if entity is available."""
return (
super().available
and self.coordinator.connector.get_device(self.device_id) is not None
)
@property
def is_on(self) -> bool | None:
"""Return true if the fan is on."""
value = self.coordinator.connector.get_current_option(
self.device_id, CompitParameter.VENTILATION_ON_OFF
)
return True if value == STATE_ON else False if value == STATE_OFF else None
async def async_turn_on(
self,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs: Any,
) -> None:
"""Turn on the fan."""
await self.coordinator.connector.select_device_option(
self.device_id, CompitParameter.VENTILATION_ON_OFF, STATE_ON
)
if percentage is None:
self.async_write_ha_state()
return
await self.async_set_percentage(percentage)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the fan."""
await self.coordinator.connector.select_device_option(
self.device_id, CompitParameter.VENTILATION_ON_OFF, STATE_OFF
)
self.async_write_ha_state()
@property
def percentage(self) -> int | None:
"""Return the current fan speed as a percentage."""
if self.is_on is False:
return 0
mode = self.coordinator.connector.get_current_option(
self.device_id, CompitParameter.VENTILATION_GEAR_TARGET
)
if mode is None:
return None
gear = COMPIT_GEAR_TO_HA.get(mode)
return (
None
if gear is None
else ordered_list_item_to_percentage(
list(COMPIT_GEAR_TO_HA.values()),
gear,
)
)
async def async_set_percentage(self, percentage: int) -> None:
"""Set the fan speed."""
if percentage == 0:
await self.async_turn_off()
return
gear = int(
percentage_to_ordered_list_item(
list(COMPIT_GEAR_TO_HA.values()),
percentage,
)
)
mode = HA_STATE_TO_COMPIT.get(gear)
if mode is None:
return
await self.coordinator.connector.select_device_option(
self.device_id, CompitParameter.VENTILATION_GEAR_TARGET, mode
)
self.async_write_ha_state()

View File

@@ -1,5 +1,33 @@
{
"entity": {
"binary_sensor": {
"airing": {
"default": "mdi:window-open-variant"
},
"co2_alert": {
"default": "mdi:alert"
},
"co2_level": {
"default": "mdi:molecule-co2"
},
"dust_alert": {
"default": "mdi:alert"
},
"pump_status": {
"default": "mdi:pump"
},
"temperature_alert": {
"default": "mdi:alert"
}
},
"fan": {
"ventilation": {
"default": "mdi:fan",
"state": {
"off": "mdi:fan-off"
}
}
},
"number": {
"boiler_target_temperature": {
"default": "mdi:water-boiler"
@@ -138,6 +166,119 @@
"winter": "mdi:snowflake"
}
}
},
"sensor": {
"alarm_code": {
"default": "mdi:alert-circle",
"state": {
"no_alarm": "mdi:check-circle"
}
},
"battery_level": {
"default": "mdi:battery"
},
"boiler_temperature": {
"default": "mdi:thermometer"
},
"calculated_heating_temperature": {
"default": "mdi:thermometer"
},
"calculated_target_temperature": {
"default": "mdi:thermometer"
},
"charging_power": {
"default": "mdi:flash"
},
"circuit_target_temperature": {
"default": "mdi:thermometer"
},
"co2_percent": {
"default": "mdi:molecule-co2"
},
"collector_power": {
"default": "mdi:solar-power"
},
"collector_temperature": {
"default": "mdi:thermometer"
},
"dhw_measured_temperature": {
"default": "mdi:thermometer"
},
"energy_consumption": {
"default": "mdi:lightning-bolt"
},
"energy_smart_grid_yesterday": {
"default": "mdi:lightning-bolt"
},
"energy_today": {
"default": "mdi:lightning-bolt"
},
"energy_total": {
"default": "mdi:lightning-bolt"
},
"energy_yesterday": {
"default": "mdi:lightning-bolt"
},
"fuel_level": {
"default": "mdi:gauge"
},
"humidity": {
"default": "mdi:water-percent"
},
"mixer_temperature": {
"default": "mdi:thermometer"
},
"outdoor_temperature": {
"default": "mdi:thermometer"
},
"pk1_function": {
"default": "mdi:cog",
"state": {
"cooling": "mdi:snowflake-thermometer",
"off": "mdi:cog-off",
"summer": "mdi:weather-sunny",
"winter": "mdi:snowflake"
}
},
"pm10_level": {
"default": "mdi:air-filter",
"state": {
"exceeded": "mdi:alert",
"no_sensor": "mdi:cancel",
"normal": "mdi:air-filter",
"warning": "mdi:alert-circle-outline"
}
},
"pm25_level": {
"default": "mdi:air-filter",
"state": {
"exceeded": "mdi:alert",
"no_sensor": "mdi:cancel",
"normal": "mdi:air-filter",
"warning": "mdi:alert-circle-outline"
}
},
"return_circuit_temperature": {
"default": "mdi:thermometer"
},
"tank_temperature_t2": {
"default": "mdi:thermometer"
},
"tank_temperature_t3": {
"default": "mdi:thermometer"
},
"tank_temperature_t4": {
"default": "mdi:thermometer"
},
"target_heating_temperature": {
"default": "mdi:thermometer"
},
"ventilation_alarm": {
"default": "mdi:alert",
"state": {
"no_alarm": "mdi:check-circle"
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -33,6 +33,31 @@
}
},
"entity": {
"binary_sensor": {
"airing": {
"name": "Airing"
},
"co2_alert": {
"name": "CO2 alert"
},
"co2_level": {
"name": "CO2 level"
},
"dust_alert": {
"name": "Dust alert"
},
"pump_status": {
"name": "Pump status"
},
"temperature_alert": {
"name": "Temperature alert"
}
},
"fan": {
"ventilation": {
"name": "[%key:component::fan::title%]"
}
},
"number": {
"boiler_target_temperature": {
"name": "Boiler target temperature"
@@ -183,6 +208,219 @@
"winter": "Winter"
}
}
},
"sensor": {
"actual_buffer_temp": {
"name": "Actual buffer temperature"
},
"actual_dhw_temp": {
"name": "Actual DHW temperature"
},
"actual_hc_temperature_zone": {
"name": "Actual heating circuit {zone} temperature"
},
"actual_upper_source_temp": {
"name": "Actual upper source temperature"
},
"alarm_code": {
"name": "Alarm code",
"state": {
"battery_fault": "Battery fault",
"damaged_outdoor_temp": "Damaged outdoor temperature sensor",
"damaged_return_temp": "Damaged return temperature sensor",
"discharged_battery": "Discharged battery",
"internal_af": "Internal fault",
"low_battery_level": "Low battery level",
"no_alarm": "No alarm",
"no_battery": "No battery",
"no_power": "No power",
"no_pump": "No pump",
"pump_fault": "Pump fault"
}
},
"battery_level": {
"name": "Battery level"
},
"boiler_temperature": {
"name": "Boiler temperature"
},
"buffer_return_temperature": {
"name": "Buffer return temperature"
},
"buffer_set_temperature": {
"name": "Buffer set temperature"
},
"calculated_buffer_temp": {
"name": "Calculated buffer temperature"
},
"calculated_dhw_temp": {
"name": "Calculated DHW temperature"
},
"calculated_heating_temperature": {
"name": "Calculated heating temperature"
},
"calculated_target_temperature": {
"name": "Calculated target temperature"
},
"calculated_upper_source_temp": {
"name": "Calculated upper source temperature"
},
"charging_power": {
"name": "Charging power"
},
"circuit_target_temperature": {
"name": "Circuit target temperature"
},
"co2_percent": {
"name": "CO2 percent"
},
"collector_power": {
"name": "Collector power"
},
"collector_temperature": {
"name": "Collector temperature"
},
"dhw_measured_temperature": {
"name": "DHW measured temperature"
},
"dhw_temperature": {
"name": "DHW temperature"
},
"energy_consumption": {
"name": "Energy consumption"
},
"energy_smart_grid_yesterday": {
"name": "Energy smart grid yesterday"
},
"energy_today": {
"name": "Energy today"
},
"energy_total": {
"name": "Energy total"
},
"energy_yesterday": {
"name": "Energy yesterday"
},
"fuel_level": {
"name": "Fuel level"
},
"heating_target_temperature_zone": {
"name": "Heating circuit {zone} target temperature"
},
"lower_source_temperature": {
"name": "Lower source temperature"
},
"mixer_temperature": {
"name": "Mixer temperature"
},
"mixer_temperature_zone": {
"name": "Mixer {zone} temperature"
},
"outdoor_temperature": {
"name": "Outdoor temperature"
},
"pk1_function": {
"name": "PK1 function",
"state": {
"cooling": "Cooling",
"holiday": "Holiday",
"nano_nr_1": "Nano 1",
"nano_nr_2": "Nano 2",
"nano_nr_3": "Nano 3",
"nano_nr_4": "Nano 4",
"nano_nr_5": "Nano 5",
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]",
"summer": "Summer",
"winter": "Winter"
}
},
"pm10_level": {
"name": "PM10 level",
"state": {
"exceeded": "Exceeded",
"no_sensor": "No sensor",
"normal": "Normal",
"warning": "Warning"
}
},
"pm1_level": {
"name": "PM1 level"
},
"pm25_level": {
"name": "PM2.5 level",
"state": {
"exceeded": "Exceeded",
"no_sensor": "No sensor",
"normal": "Normal",
"warning": "Warning"
}
},
"pm4_level": {
"name": "PM4 level"
},
"preset_mode": {
"name": "Preset mode"
},
"protection_temperature": {
"name": "Protection temperature"
},
"pump_status": {
"name": "Pump status",
"state": {
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]"
}
},
"return_circuit_temperature": {
"name": "Return circuit temperature"
},
"set_target_temperature": {
"name": "Set target temperature"
},
"tank_temperature_t2": {
"name": "Tank T2 bottom temperature"
},
"tank_temperature_t3": {
"name": "Tank T3 top temperature"
},
"tank_temperature_t4": {
"name": "Tank T4 temperature"
},
"target_heating_temperature": {
"name": "Target heating temperature"
},
"target_temperature": {
"name": "Target temperature"
},
"temperature_alert": {
"name": "Temperature alert",
"state": {
"alert": "Alert",
"no_alert": "No alert"
}
},
"upper_source_temperature": {
"name": "Upper source temperature"
},
"ventilation_alarm": {
"name": "Ventilation alarm",
"state": {
"ahu_alarm": "AHU alarm",
"bot_alarm": "BOT alarm",
"damaged_exhaust_sensor": "Damaged exhaust sensor",
"damaged_preheater_sensor": "Damaged preheater sensor",
"damaged_supply_and_exhaust_sensors": "Damaged supply and exhaust sensors",
"damaged_supply_sensor": "Damaged supply sensor",
"no_alarm": "No alarm"
}
},
"ventilation_gear": {
"name": "Ventilation gear"
},
"weather_curve": {
"name": "Weather curve"
}
}
}
}

View File

@@ -48,6 +48,8 @@ def async_setup(hass: HomeAssistant) -> None:
vol.Optional("conversation_id"): vol.Any(str, None),
vol.Optional("language"): str,
vol.Optional("agent_id"): agent_id_validator,
vol.Optional("device_id"): vol.Any(str, None),
vol.Optional("satellite_id"): vol.Any(str, None),
}
)
@websocket_api.async_response
@@ -64,6 +66,8 @@ async def websocket_process(
context=connection.context(msg),
language=msg.get("language"),
agent_id=msg.get("agent_id"),
device_id=msg.get("device_id"),
satellite_id=msg.get("satellite_id"),
)
connection.send_result(msg["id"], result.as_dict())
@@ -248,6 +252,8 @@ class ConversationProcessView(http.HomeAssistantView):
vol.Optional("conversation_id"): str,
vol.Optional("language"): str,
vol.Optional("agent_id"): agent_id_validator,
vol.Optional("device_id"): vol.Any(str, None),
vol.Optional("satellite_id"): vol.Any(str, None),
}
)
)
@@ -262,6 +268,8 @@ class ConversationProcessView(http.HomeAssistantView):
context=self.context(request),
language=data.get("language"),
agent_id=data.get("agent_id"),
device_id=data.get("device_id"),
satellite_id=data.get("satellite_id"),
)
return self.json(result.as_dict())

View File

@@ -112,11 +112,12 @@ def _zone_is_configured(zone: DaikinZone) -> bool:
def _zone_temperature_lists(device: Appliance) -> tuple[list[str], list[str]]:
"""Return the decoded zone temperature lists."""
try:
heating = device.represent(DAIKIN_ZONE_TEMP_HEAT)[1]
cooling = device.represent(DAIKIN_ZONE_TEMP_COOL)[1]
except AttributeError:
values = device.values
if DAIKIN_ZONE_TEMP_HEAT not in values or DAIKIN_ZONE_TEMP_COOL not in values:
return ([], [])
heating = device.represent(DAIKIN_ZONE_TEMP_HEAT)[1]
cooling = device.represent(DAIKIN_ZONE_TEMP_COOL)[1]
return (list(heating or []), list(cooling or []))

View File

@@ -139,18 +139,6 @@ class AbstractDemoPlayer(MediaPlayerEntity):
self._attr_is_volume_muted = mute
self.schedule_update_ha_state()
def volume_up(self) -> None:
"""Increase volume."""
assert self.volume_level is not None
self._attr_volume_level = min(1.0, self.volume_level + 0.1)
self.schedule_update_ha_state()
def volume_down(self) -> None:
"""Decrease volume."""
assert self.volume_level is not None
self._attr_volume_level = max(0.0, self.volume_level - 0.1)
self.schedule_update_ha_state()
def set_volume_level(self, volume: float) -> None:
"""Set the volume level, range 0..1."""
self._attr_volume_level = volume

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["dsmr_parser"],
"requirements": ["dsmr-parser==1.4.3"]
"requirements": ["dsmr-parser==1.5.0"]
}

View File

@@ -1,22 +0,0 @@
"""The Duke Energy integration."""
from __future__ import annotations
from homeassistant.core import HomeAssistant
from .coordinator import DukeEnergyConfigEntry, DukeEnergyCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: DukeEnergyConfigEntry) -> bool:
"""Set up Duke Energy from a config entry."""
coordinator = DukeEnergyCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
return True
async def async_unload_entry(hass: HomeAssistant, entry: DukeEnergyConfigEntry) -> bool:
"""Unload a config entry."""
return True

View File

@@ -1,67 +0,0 @@
"""Config flow for Duke Energy integration."""
from __future__ import annotations
import logging
from typing import Any
from aiodukeenergy import DukeEnergy
from aiohttp import ClientError, ClientResponseError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)
class DukeEnergyConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Duke Energy."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
session = async_get_clientsession(self.hass)
api = DukeEnergy(
user_input[CONF_USERNAME], user_input[CONF_PASSWORD], session
)
try:
auth = await api.authenticate()
except ClientResponseError as e:
errors["base"] = "invalid_auth" if e.status == 404 else "cannot_connect"
except ClientError, TimeoutError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
username = auth["internalUserID"].lower()
await self.async_set_unique_id(username)
self._abort_if_unique_id_configured()
email = auth["loginEmailAddress"].lower()
data = {
CONF_EMAIL: email,
CONF_USERNAME: username,
CONF_PASSWORD: user_input[CONF_PASSWORD],
}
self._async_abort_entries_match(data)
return self.async_create_entry(title=email, data=data)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

View File

@@ -1,3 +0,0 @@
"""Constants for the Duke Energy integration."""
DOMAIN = "duke_energy"

View File

@@ -1,222 +0,0 @@
"""Coordinator to handle Duke Energy connections."""
from datetime import datetime, timedelta
import logging
from typing import Any, cast
from aiodukeenergy import DukeEnergy
from aiohttp import ClientError
from homeassistant.components.recorder import get_instance
from homeassistant.components.recorder.models import (
StatisticData,
StatisticMeanType,
StatisticMetaData,
)
from homeassistant.components.recorder.statistics import (
async_add_external_statistics,
get_last_statistics,
statistics_during_period,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, UnitOfEnergy, UnitOfVolume
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import EnergyConverter
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
_SUPPORTED_METER_TYPES = ("ELECTRIC",)
type DukeEnergyConfigEntry = ConfigEntry[DukeEnergyCoordinator]
class DukeEnergyCoordinator(DataUpdateCoordinator[None]):
"""Handle inserting statistics."""
config_entry: DukeEnergyConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: DukeEnergyConfigEntry
) -> None:
"""Initialize the data handler."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name="Duke Energy",
# Data is updated daily on Duke Energy.
# Refresh every 12h to be at most 12h behind.
update_interval=timedelta(hours=12),
)
self.api = DukeEnergy(
config_entry.data[CONF_USERNAME],
config_entry.data[CONF_PASSWORD],
async_get_clientsession(hass),
)
self._statistic_ids: set = set()
@callback
def _dummy_listener() -> None:
pass
# Force the coordinator to periodically update by registering at least one listener.
# Duke Energy does not provide forecast data, so all information is historical.
# This makes _async_update_data get periodically called so we can insert statistics.
self.async_add_listener(_dummy_listener)
self.config_entry.async_on_unload(self._clear_statistics)
def _clear_statistics(self) -> None:
"""Clear statistics."""
get_instance(self.hass).async_clear_statistics(list(self._statistic_ids))
async def _async_update_data(self) -> None:
"""Insert Duke Energy statistics."""
meters: dict[str, dict[str, Any]] = await self.api.get_meters()
for serial_number, meter in meters.items():
if (
not isinstance(meter["serviceType"], str)
or meter["serviceType"] not in _SUPPORTED_METER_TYPES
):
_LOGGER.debug(
"Skipping unsupported meter type %s", meter["serviceType"]
)
continue
id_prefix = f"{meter['serviceType'].lower()}_{serial_number}"
consumption_statistic_id = f"{DOMAIN}:{id_prefix}_energy_consumption"
self._statistic_ids.add(consumption_statistic_id)
_LOGGER.debug(
"Updating Statistics for %s",
consumption_statistic_id,
)
last_stat = await get_instance(self.hass).async_add_executor_job(
get_last_statistics, self.hass, 1, consumption_statistic_id, True, set()
)
if not last_stat:
_LOGGER.debug("Updating statistic for the first time")
usage = await self._async_get_energy_usage(meter)
consumption_sum = 0.0
last_stats_time = None
else:
usage = await self._async_get_energy_usage(
meter,
last_stat[consumption_statistic_id][0]["start"],
)
if not usage:
_LOGGER.debug("No recent usage data. Skipping update")
continue
stats = await get_instance(self.hass).async_add_executor_job(
statistics_during_period,
self.hass,
min(usage.keys()),
None,
{consumption_statistic_id},
"hour",
None,
{"sum"},
)
consumption_sum = cast(float, stats[consumption_statistic_id][0]["sum"])
last_stats_time = stats[consumption_statistic_id][0]["start"]
consumption_statistics = []
for start, data in usage.items():
if last_stats_time is not None and start.timestamp() <= last_stats_time:
continue
consumption_sum += data["energy"]
consumption_statistics.append(
StatisticData(
start=start, state=data["energy"], sum=consumption_sum
)
)
name_prefix = (
f"Duke Energy {meter['serviceType'].capitalize()} {serial_number}"
)
consumption_metadata = StatisticMetaData(
mean_type=StatisticMeanType.NONE,
has_sum=True,
name=f"{name_prefix} Consumption",
source=DOMAIN,
statistic_id=consumption_statistic_id,
unit_class=EnergyConverter.UNIT_CLASS,
unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR
if meter["serviceType"] == "ELECTRIC"
else UnitOfVolume.CENTUM_CUBIC_FEET,
)
_LOGGER.debug(
"Adding %s statistics for %s",
len(consumption_statistics),
consumption_statistic_id,
)
async_add_external_statistics(
self.hass, consumption_metadata, consumption_statistics
)
async def _async_get_energy_usage(
self, meter: dict[str, Any], start_time: float | None = None
) -> dict[datetime, dict[str, float | int]]:
"""Get energy usage.
If start_time is None, get usage since account activation (or as far back as possible),
otherwise since start_time - 30 days to allow corrections in data.
Duke Energy provides hourly data all the way back to ~3 years.
"""
# All of Duke Energy Service Areas are currently in America/New_York timezone
# May need to re-think this if that ever changes and determine timezone based
# on the service address somehow.
tz = await dt_util.async_get_time_zone("America/New_York")
lookback = timedelta(days=30)
one = timedelta(days=1)
if start_time is None:
# Max 3 years of data
start = dt_util.now(tz) - timedelta(days=3 * 365)
else:
start = datetime.fromtimestamp(start_time, tz=tz) - lookback
agreement_date = dt_util.parse_datetime(meter["agreementActiveDate"])
if agreement_date is not None:
start = max(agreement_date.replace(tzinfo=tz), start)
start = start.replace(hour=0, minute=0, second=0, microsecond=0)
end = dt_util.now(tz).replace(hour=0, minute=0, second=0, microsecond=0) - one
_LOGGER.debug("Data lookup range: %s - %s", start, end)
start_step = max(end - lookback, start)
end_step = end
usage: dict[datetime, dict[str, float | int]] = {}
while True:
_LOGGER.debug("Getting hourly usage: %s - %s", start_step, end_step)
try:
# Get data
results = await self.api.get_energy_usage(
meter["serialNum"], "HOURLY", "DAY", start_step, end_step
)
usage = {**results["data"], **usage}
for missing in results["missing"]:
_LOGGER.debug("Missing data: %s", missing)
# Set next range
end_step = start_step - one
start_step = max(start_step - lookback, start)
# Make sure we don't go back too far
if end_step < start:
break
except TimeoutError, ClientError:
# ClientError is raised when there is no more data for the range
break
_LOGGER.debug("Got %s meter usage reads", len(usage))
return usage

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