Compare commits

...

480 Commits

Author SHA1 Message Date
Robert Resch
943d873782 Make host required for hddtemp 2025-07-09 16:06:40 +02:00
epenet
59fe6da47c Adjust tuya test docstrings (#148493) 2025-07-09 15:59:43 +02:00
epenet
e1cdc1af1c Add diagnostics tests to tuya (#148489) 2025-07-09 15:47:48 +02:00
epenet
f6e2b962fd Use SnapshotAssertion in lifx diagnostics tests (#148491) 2025-07-09 15:30:17 +02:00
epenet
fe0ce9bc6d Use real product_id in tuya fixture (#148415) 2025-07-09 14:44:18 +02:00
Robert Resch
b083919031 Revert "Deprecate hddtemp" (#148482) 2025-07-09 13:53:15 +02:00
epenet
ef2e699d2c Add tuya snapshot tests for curtain switch (#148465)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-07-09 13:05:53 +02:00
Raphael Hehl
71df8ffe6e Bump uiprotect to version 7.14.2 (#148453) 2025-07-09 12:37:45 +02:00
Joakim Sørensen
98604f09fc Bump hass-nabucasa from 0.105.0 to 0.106.0 (#148473) 2025-07-09 12:30:43 +02:00
Artur Pragacz
b97b04661e Improve logging in bootstrap (#148469) 2025-07-09 11:29:56 +01:00
Andrew Jackson
828037de1f Set quality scale on Mealie to silver (#148467) 2025-07-09 11:25:56 +01:00
Norbert Rittel
659504c91f Fix friendly name of increased_non_neutral_output in zha (#148468) 2025-07-09 12:24:44 +02:00
Franck Nijhof
434ac421d1 Tiny tweaks to task form (#148475) 2025-07-09 12:04:00 +02:00
Denis Shulyaka
de849b920a Enable web search for OpenAI reasoning models (#148393) 2025-07-09 10:54:49 +02:00
Artur Pragacz
e387d4834f Fix unloading update listener in Unifi (#148471) 2025-07-09 10:44:21 +02:00
Artur Pragacz
39ed877a17 Fix unloading update listener in Axis (#148470) 2025-07-09 10:43:55 +02:00
epenet
13d05a338b Sort tuya definitions by category (#148472) 2025-07-09 10:42:55 +02:00
Avi Miller
cb2095bcbe Bump aiolifx to 1.2.1 (#148464)
Signed-off-by: Avi Miller <me@dje.li>
2025-07-09 09:43:29 +02:00
Norbert Rittel
6de630ef3e Fix sentence-casing of trigger subtypes in xiaomi_ble (#148463) 2025-07-09 10:43:22 +03:00
Rico Hageman
a02359b25d Add dew point to Awair integration (#148403) 2025-07-09 09:28:55 +02:00
Oliver Heesakkers
afcd991262 Handle processing errors when writing to Zabbix (#148449) 2025-07-09 08:01:54 +02:00
J. Nick Koston
6b5b35fece Bump aioesphomeapi to 34.2.0 (#148456) 2025-07-08 22:34:35 -06:00
Norbert Rittel
ed8effa162 Fix spelling of "non-existent", "non-blocking" and "currently used" (#148440) 2025-07-08 22:58:39 +01:00
Simone Chemelli
70c01efe57 Update Alexa Devices quality scale to silver (#148435) 2025-07-08 17:58:35 +01:00
Norbert Rittel
ebffaed0bd Fix spelling of "non-resettable" in iskra (#148417) 2025-07-08 19:45:39 +03:00
Norbert Rittel
ab1e323d49 Fix spelling of "non-volatile memory" in z-wave_js (#148422) 2025-07-08 19:44:11 +03:00
Simone Chemelli
6e63c17b39 Improve exceptions in Alexa Devices (#148260) 2025-07-08 17:58:48 +02:00
Petro31
a35299d94c Add preview tests for number and sensor (#148426) 2025-07-08 16:04:06 +01:00
Tucker Kern
c97ad9657f Add metadata support to Snapcast media players (#132283)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-07-08 16:58:32 +02:00
Erik Montnemery
aab8908af8 Improve entity registry tests related to config entries in devices (#148399)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-07-08 16:24:06 +02:00
Maciej Bieniek
ae7bc14059 Make the update interval a property of the NextDNS coordinator class (#148410) 2025-07-08 16:14:02 +02:00
Maciej Bieniek
546f6afac2 Bump gios to version 6.1.1 (#148414) 2025-07-08 16:11:15 +02:00
epenet
8ccd097e98 Add tuya snapshot tests for bladeless tower fan (#148401) 2025-07-08 14:50:49 +02:00
epenet
77ae6048ef Add tuya snapshot tests for gas leak sensor (#148400) 2025-07-08 14:49:52 +02:00
Abílio Costa
420d1e169d Fix hassfest command in copilot-instructions (#148405) 2025-07-08 14:49:09 +02:00
hanwg
91b8262128 Update strings for Telegram bot (#148409) 2025-07-08 14:48:44 +02:00
Ludovic BOUÉ
e393929014 Matter EVSE StateOfCharge (#148213) 2025-07-08 14:28:13 +02:00
Samuel Xiao
11938762eb Fix Switchbot cloud plug mini current unit Issue (#148314) 2025-07-08 13:57:30 +02:00
Simone Chemelli
94862e6a50 Update Alexa Devices quality scale (#147259) 2025-07-08 13:49:00 +02:00
epenet
1a8d4c5041 Add tuya snapshot tests for Avatto WT598 thermostat (#148398) 2025-07-08 13:40:16 +02:00
Erik Montnemery
b775ba2955 Do not add switch_as_x config entry to source device (#148346) 2025-07-08 13:23:28 +02:00
Josef Zweck
d2bf27195a Bump pylamarzocco to 2.0.11 (#148386) 2025-07-08 13:06:43 +02:00
Josef Zweck
824006729b Create own clientsession for lamarzocco (#148385) 2025-07-08 13:06:05 +02:00
Joakim Plate
a7cba2b9bb Handle binary coils with non default mappings in nibe heatpump (#148354) 2025-07-08 13:05:16 +02:00
Joost Lekkerkerker
bd1917c9b6 Bump pySmartThings to 3.2.7 (#148394) 2025-07-08 12:34:51 +02:00
Josef Zweck
7541e266da Make api_version runtime_data in pi_hole (#148238) 2025-07-08 11:46:13 +02:00
Manu
f58c76c883 Fix error when personalDetail is missing in PlayStation Network integration (#148389) 2025-07-08 10:16:10 +02:00
Simone Chemelli
a77a071954 Bump aioamazondevices to 3.2.8 (#148365)
Co-authored-by: Joakim Plate <elupus@ecce.se>
2025-07-08 10:14:41 +02:00
Jiacheng Ma
0dc145aee3 Fix tuya vacuum return_to_base function (#144362)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-07-08 10:03:35 +02:00
Joakim Plate
ac5d4f4a81 Fix CI issues due to nibe heatpump (#148388) 2025-07-08 09:17:27 +02:00
Noah Husby
d44b822295 Add play media support to Russound RIO (#148240) 2025-07-08 08:51:18 +02:00
Paulus Schoutsen
6d0891e970 OpenAI: Extract file attachment logic (#148288) 2025-07-08 08:01:49 +02:00
Avi Miller
73730e3eb3 Bump aiolifx to 1.2.0 (#148382) 2025-07-08 07:57:41 +02:00
Alexandre CUER
87b00fdc7b Emoncms add reconfigure flow (#145108)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-07-08 07:28:16 +02:00
hahn-th
f780b9763d Add support for ELV-SH-CTV Sensor to homematicip_cloud (#143737) 2025-07-08 07:24:55 +02:00
Joakim Sørensen
7a7e16bbb6 Change how subscription information is fetched (#148337)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-07-08 06:52:41 +02:00
J. Nick Koston
dcf8d7f74d Track ESPHome entities by (device_id, key) to support sub-devices with overlaping names (#148297) 2025-07-08 06:41:20 +02:00
Ville Skyttä
ccc80c78a0 Add huawei_lte device registry upnp udn connection (#148370) 2025-07-08 06:32:29 +02:00
epenet
b0f7c985e4 Add snapshots tests for new platforms in tuya (#148334) 2025-07-08 06:25:53 +02:00
Joakim Sørensen
7875290256 Adds claude-code feature to the devcontainer (#148338) 2025-07-08 06:24:31 +02:00
Ruben van Dijk
f478812568 Allow multiple set-cookie headers with hassio ingress (#148148) 2025-07-08 06:13:08 +02:00
Joakim Plate
9ce03c79f0 Switch to box default for numbers in nibe_heatpump integration (#148364) 2025-07-08 06:09:22 +02:00
Joakim Plate
19951d9403 Handle when heat pump rejects same value writes in nibe_heatpump (#148366) 2025-07-08 06:07:41 +02:00
G Johansson
4b8dcc39b4 Bump holidays to 0.76 (#148363) 2025-07-08 06:05:18 +02:00
Joakim Plate
b151a9bf75 Add missing connection for gardena ble device (#148376) 2025-07-08 06:02:56 +02:00
Manu
e3cc4acdc6 Remove deprecated max_health, habits and rewards sensors from Habitica integration (#148377) 2025-07-08 05:57:46 +02:00
Ville Skyttä
fc53ddb3b4 Remove huawei_lte notify related timeout suppression (#148373) 2025-07-08 00:08:43 +02:00
hanwg
0409c05265 Add basic authentication option for Telegram bot (#148247) 2025-07-07 22:08:49 +02:00
jlanchares
9d2ffa6372 Goodwe TCP support (port 502) (#147900) 2025-07-07 19:37:20 +02:00
Joakim Plate
5c4f166f6f Add translation for write failures in nibe_heatpump (#148352) 2025-07-07 18:48:34 +02:00
Erik Montnemery
6396f54e0d Move zone conditions to the zone integration (#148157) 2025-07-07 18:27:44 +02:00
Denis Shulyaka
090b8f0659 Bump openai to 1.93.0 (#148350) 2025-07-07 18:07:28 +02:00
G Johansson
a46cc82916 Don't log deprecation warning in vacuum until after entity added to hass (#147959)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-07-07 16:52:29 +02:00
J. Nick Koston
8007bf1c31 Fix REST sensor charset handling to respect Content-Type header (#148223) 2025-07-07 14:32:58 +01:00
Manu
c296e1f818 Remove deprecated register_static_path method (#148303)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-07-07 14:27:19 +01:00
Retha Runolfsson
799dc97d4a Bump pyswitchbot to 0.68.1 (#148335) 2025-07-07 14:26:23 +01:00
Mark Adkins
e4c9df6d98 Bump sharkiq to 1.1.1 (#148244) 2025-07-07 15:18:15 +02:00
J. Nick Koston
03e295ace0 Restore httpx compatibility for non-primitive REST query parameters (#148286) 2025-07-07 08:01:48 -05:00
Abílio Costa
b71bcb002b Move target selector extractor method to common module (#148087) 2025-07-07 13:48:48 +01:00
Norbert Rittel
c60e06d32f Fix missing sentence-casing and spelling of "REST" in iskra (#148330) 2025-07-07 14:06:27 +02:00
Norbert Rittel
448d6041e5 Fix missing sentence-casing in wallbox (#148332) 2025-07-07 14:06:13 +02:00
tronikos
15c9ddea78 Bump gassist-text to 0.0.14 (#148312) 2025-07-07 04:10:50 -07:00
Erik Montnemery
0c783e87d1 Fix homee test (#148322) 2025-07-07 11:59:35 +02:00
Franck Nijhof
42b50c71ec Revert "Add tests for Sonos Alarms" (#148319) 2025-07-07 11:54:36 +02:00
Maciej Bieniek
991864a8af Bump gios to version 6.1.0 (#148274) 2025-07-07 11:43:39 +02:00
Arie Catsman
b79e770bcf Bump pyenphase to 2.2.1 (#148292) 2025-07-07 11:40:48 +02:00
Shay Levy
f02c1b0d4e Bump aiowebostv to 0.7.4 (#148273) 2025-07-07 11:37:39 +02:00
Norbert Rittel
a5d6bfd1b3 Reword option for 'Main' control in wled (#148309) 2025-07-07 10:30:39 +02:00
Norbert Rittel
21f6bf3914 Improve translation_key of EnergyEvseSupplyStateSensor in matter (#148280) 2025-07-07 10:26:20 +02:00
Hessel
0bce01da0b Address some Wallbox quality scale issues (#148200) 2025-07-07 10:09:07 +02:00
Ludovic BOUÉ
6351c3302e Matter OperationalState CountdownTime (#147705)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-07-06 23:40:05 +02:00
J. Nick Koston
2ea20ee2ab Fix UTF-8 encoding for REST basic authentication (#148225) 2025-07-06 12:40:19 -05:00
Paulus Schoutsen
008e2a3d10 Add attachment support to AI task (#148120) 2025-07-06 19:33:41 +02:00
Joakim Sørensen
699c60f293 Add the current version to the starting log to aid troubleshooting (#148271) 2025-07-06 19:06:27 +02:00
karwosts
404d17efca Translate number selector unit for utility_meter (#148276) 2025-07-06 18:36:38 +02:00
Allen Porter
4b5c04b2f0 Add AI Task support in Ollama (#148226)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2025-07-06 16:56:37 +02:00
Paulus Schoutsen
8cb9cadce9 Extract files_to_prompt from Gemini action (#148203)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Allen Porter <allen.porter@gmail.com>
2025-07-06 15:15:38 +02:00
Robin Thoni
075efb469a Bump sfrbox-api to 0.0.12 (#148259)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-07-06 13:08:27 +02:00
starkillerOG
0e7a4c91bf bump motionblinds to 0.6.29 (#148265) 2025-07-06 12:38:57 +02:00
Norbert Rittel
4ee930507d Fix typo in wrong_hub abort message of homee (#148261) 2025-07-06 12:11:44 +02:00
Markus Adrario
1b11ac9123 Add Homee general tests (#137128) 2025-07-06 12:05:43 +02:00
Norbert Rittel
8d7e387b46 Deduplicate strings in nordpool actions (#148258) 2025-07-06 11:23:57 +02:00
Joakim Sørensen
70e9c4e2d0 Add reauth flow to the Traccar Server integration (#148236)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-07-06 08:09:59 +02:00
Josef Zweck
26de1ea37b Update strings in pihole (#148234)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-05 23:14:59 +01:00
Josef Zweck
3ffcfa42ba Bump pylamarzocco to 2.0.10 (#148233) 2025-07-05 23:34:23 +02:00
G Johansson
e304022560 Add service in Nord Pool for fetching normalized price indices (#147979) 2025-07-05 21:39:48 +02:00
G Johansson
160e4e4d05 Block options flow for default hostname in dnsip (#148221) 2025-07-05 21:36:15 +02:00
Noah Husby
eb0f11a859 Bump aiorussound to 4.8.0 (#148235) 2025-07-05 21:13:48 +02:00
TheJulianJES
295b15ace9 Change ZHA string "autoshutdown" to "auto-shutdown" (#148230) 2025-07-05 20:23:03 +02:00
Pete Sage
d997efc500 Add tests for Sonos Alarms (#146308) 2025-07-05 17:39:52 +02:00
Jack Powell
736865c130 Add binary sensor platform to PlayStation Network Integration (#147639) 2025-07-05 17:27:23 +02:00
Paulus Schoutsen
4f4ec6f41a Add Google Gen AI structured data support (#148143) 2025-07-05 08:22:17 -07:00
Luka Matijević
33d05d99eb Fix Miele hob plate power step typo (#148214) 2025-07-05 16:44:41 +02:00
Guido Schmitz
8d82e34ba5 Make connected stations coordinator a dict in devolo Home Network (#147042) 2025-07-05 11:42:15 +02:00
Sören Beye
2ea09ff37a Squeezebox: Fix track selection in media browser (#147185) 2025-07-05 11:36:45 +02:00
Sören Beye
676567f471 Squeezebox: Fix tracks not having thumbnails (#147187) 2025-07-05 11:31:30 +02:00
Denis Shulyaka
3151713a34 Replace dot with underscores for NamespacedTool and ActionTool (#147764) 2025-07-05 11:27:27 +02:00
David Rapan
23773759ea Starlink's last boot time occasional, back and forth changes by 1 s fix (#147969) 2025-07-05 11:18:54 +02:00
Norbert Rittel
ef255788d2 Make lat/long attribute names localizable in dwd_weather_warnings (#147988) 2025-07-05 11:01:27 +02:00
Norbert Rittel
b72536acfa Make "autorelock" consistent across integrations in matter (#148023) 2025-07-05 10:59:57 +02:00
tronikos
fea7dc7eba Remember Opower utility and username on config flow errors (#148097) 2025-07-05 10:26:15 +02:00
Markus Adrario
f1698cdb75 Add reauth flow to homee (#147258) 2025-07-05 10:26:04 +02:00
HarvsG
1b21c986e8 Enable Pihole API v6 (#145890)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-07-05 10:21:32 +02:00
Paulus Schoutsen
1e164c94b1 Include path when media source file can be accessed on disk (#148180) 2025-07-05 10:14:52 +02:00
epenet
7898e3f0fb Add initial tuya snapshot tests (#148034)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-07-05 09:54:54 +02:00
Norbert Rittel
0d54e75940 Fix spelling of "auto" prefixes in zha (#148022) 2025-07-05 09:34:24 +02:00
karwosts
3cfff4de3a Add a preview to history_stats options flow (#145721) 2025-07-05 09:09:02 +02:00
Andrey Kupreychik
275d390a6c Add reconfiguration support for keenetic_ndms2 integration (#142191)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-07-05 08:52:43 +02:00
Matt Zimmerman
e63e6a6072 Bump python-smarttub to 0.0.43 (#147317) 2025-07-05 08:08:52 +02:00
Josef Zweck
e592e565c0 Make ready time sensors unavailable instead in lamarzocco (#147985) 2025-07-05 07:20:42 +02:00
Arie Catsman
12b90f3c8e Add debug logs to trace enphase auth process at load. (#148117) 2025-07-04 23:14:51 +01:00
epenet
76be2fdba1 Improve (and align) deprecation messages (#147948) 2025-07-05 00:02:36 +02:00
Thomas55555
528daad854 Constant polling for Husqvarna Automower (#147957) 2025-07-04 23:42:17 +02:00
Ville Skyttä
dcad5bbe04 Simplify unnecessary re.findall calls (#147907)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-04 23:26:36 +02:00
Michael Podhorodecki
ca85ffc068 Add Deadlock (SecureMode) support to the Yale Access Bluetooth integration (#144107)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-04 23:07:13 +02:00
Kevin Stillhammer
9a5cbe483b Remove obsolete string unit_system in here_travel_time (#146656) 2025-07-04 23:06:47 +02:00
Pete Sage
be7735964b Sonos remove unneeded mocking from test (#147064) 2025-07-04 23:02:38 +02:00
Guido Schmitz
79683c8267 Log availability of devices in devolo Home Control (#147091) 2025-07-04 22:59:38 +02:00
Andre Lengwenus
8f24ebe967 Remove deprecated support for lock sensors and corresponding actions in lcn (#147143) 2025-07-04 22:55:20 +02:00
Andre Lengwenus
520d92b902 Use brightness stored in hardware device when switching LCN lights (#147375) 2025-07-04 22:53:11 +02:00
karwosts
22e46d9977 Make derivative sensor unavailable when source sensor is unavailable (#147468) 2025-07-04 22:48:48 +02:00
TimL
57c04f3a56 Bump pysmlight to v0.2.7 (#148101)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-07-04 22:35:44 +02:00
Franck Nijhof
c0368f2448 Add weekdays to time trigger (#147505)
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-04 22:31:11 +02:00
Paulus Schoutsen
6a7f4953cd Fix media selector validation (#147855)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-04 22:30:35 +02:00
Joakim Plate
470baa782e Add zeroconf discovery to philips_js (#147913)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-07-04 22:24:40 +02:00
Sid
6e607ffa01 Add reconfigure flow to eheimdigital (#147930) 2025-07-04 22:18:13 +02:00
Wesley Vos
f5b51c6cf0 Add serial_numbers to device_info of inverters, encharge and enpower (#147964) 2025-07-04 22:04:48 +02:00
Hessel
bfccee17ef Wallbox, Improve test setup (#148036) 2025-07-04 21:56:44 +02:00
Erik Montnemery
b6b6de24ac Replace MediaPlayerState.STANDBY with MediaPlayerState.OFF in cambridge_audio (#148133) 2025-07-04 21:54:11 +02:00
Thomas55555
70624f72b6 Additional icon translation for Husqvarna Automower (#148167) 2025-07-04 21:51:47 +02:00
Thomas55555
c61cd422d1 Delete stale icon translation in Husqvarna Automower (#148168) 2025-07-04 20:47:32 +02:00
karwosts
0b2db2510f Support translating number selector UoM (#148162) 2025-07-04 21:06:33 +03:00
Erik Montnemery
bb1e263149 Remove cv.SUN_CONDITION_SCHEMA (#148158) 2025-07-04 18:34:55 +02:00
Michael Freeman
8e6b9c04f6 Bump venstarcolortouch to 0.21 (#148152) 2025-07-04 17:46:59 +02:00
Greg Dowling
cf931a75a7 Remove incorrect use of via_device in roon component (#146572) 2025-07-04 17:04:16 +02:00
Thomas55555
3250a2fb46 Bump aioautomower to 1.2.0 (#148078) 2025-07-04 16:43:36 +02:00
Franck Nijhof
6235adc69a Fix flaky emulated_roku/test_binding.py::test_events_fired_properly test (#148069) 2025-07-04 16:42:24 +02:00
Simone Chemelli
5d258c2f82 Bump aioamazondevices to 3.2.3 (#148082) 2025-07-04 16:33:16 +02:00
hanwg
cc2aca2c2c Fix Telegram bots using plain text parser failing to load on restart (#148050) 2025-07-04 16:32:46 +02:00
Erik Montnemery
04bd1967a7 Replace MediaPlayerState.STANDBY with MediaPlayerState.OFF in apple_tv (#148132) 2025-07-04 16:31:44 +02:00
Erik Montnemery
a046530eaf Replace MediaPlayerState.STANDBY with MediaPlayerState.IDLE in mediaroom (#148135) 2025-07-04 16:30:03 +02:00
Erik Montnemery
631523dfaf Replace MediaPlayerState.STANDBY with MediaPlayerState.OFF in lookin (#148134) 2025-07-04 16:27:54 +02:00
Erik Montnemery
dc20375506 Replace MediaPlayerState.STANDBY with MediaPlayerState.OFF in snapcast (#148138) 2025-07-04 16:27:33 +02:00
Erik Montnemery
811f085556 Replace MediaPlayerState.STANDBY with MediaPlayerState.IDLE in androidtv (#148130) 2025-07-04 16:27:01 +02:00
Erik Montnemery
fd86a43b28 Replace MediaPlayerState.STANDBY with MediaPlayerState.OFF in ps4 (#148136) 2025-07-04 16:25:59 +02:00
Bram Kragten
b7f830523e Update frontend to 20250702.1 (#148131) 2025-07-04 16:25:28 +02:00
Erik Montnemery
3f752e13ff Replace MediaPlayerState.STANDBY with MediaPlayerState.OFF in roku (#148137) 2025-07-04 16:23:18 +02:00
Marc Mueller
783102f2f6 [ci] Fix typing issue with aiohttp and aiosignal (#148141) 2025-07-04 16:22:38 +02:00
Erik Montnemery
8ce30d9559 Add tests of legacy entity without platform writing state (#148109) 2025-07-04 16:21:48 +02:00
Paulus Schoutsen
cde17fc0ca add extra tests for media source URI parsing (#148114) 2025-07-04 16:21:11 +02:00
David Knowles
83ae5f52da Bump pydrawise to 2025.7.0 (#148088) 2025-07-04 16:20:24 +02:00
tronikos
1cb9767bb8 Enable strict typing for Opower (#148096) 2025-07-04 16:19:04 +02:00
tronikos
e98fe7dc9c Add data_description to Opower forms (#148099) 2025-07-04 16:17:41 +02:00
tronikos
40ec51c0a3 Add redirect URL in Google Assistant SDK setup (#148076) 2025-07-04 16:17:10 +02:00
Harry Heymann
40fcc3b75b Rename Matter device conversion methods (#148090) 2025-07-04 16:13:40 +02:00
Erik Montnemery
510fd09163 Allow core integrations to describe their conditions (#147529)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-07-04 16:03:42 +02:00
epenet
e47bdc06a0 Set docstyle convention to google in ruff (#148142) 2025-07-04 16:00:37 +02:00
Allen Porter
b3d9908cd9 Add AI task structured output (#148083)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2025-07-04 15:03:34 +02:00
Erik Montnemery
99d63c49bb Add comment about error assigning in frame.report_usage (#148105) 2025-07-04 14:47:01 +02:00
Robin Thoni
4be2e84ce6 Add backward compatibility with older versions of Traccar server (#146639)
Co-authored-by: Joakim Sørensen <joasoe@proton.me>
2025-07-04 14:36:25 +02:00
Allen Porter
1fc624c7a7 Update LLM selector serializer to support ObjectSelector fields and arrays (#148094) 2025-07-04 13:05:16 +02:00
tronikos
8641a2141c Fix has-entity-name and entity-translations in Opower (#148098) 2025-07-04 10:10:21 +02:00
Paulus Schoutsen
04cc451c76 Add AI Task platform to Google Gen AI (#146766) 2025-07-03 23:36:34 -07:00
Erik Montnemery
a3b03caead Deduce integration from module in loader.async_get_issue_tracker (#148017) 2025-07-04 07:55:20 +02:00
Franck Nijhof
49d1d781b8 Fix ezviz test timeout (#148066) 2025-07-03 23:11:54 +02:00
HeroOfCanton16
11c75d7ef2 Add sensor attributes restore to modem_callerid integration (#147753) 2025-07-03 22:10:26 +01:00
Arie Catsman
8ef6b62d9a Cancel enphase mac verification on unload. (#148072) 2025-07-03 22:06:38 +02:00
tronikos
b410b414ec Add reconfigure flow in Android TV Remote (#148044) 2025-07-03 22:00:07 +02:00
Arie Catsman
e5f7421703 Bump pyenphase to 2.2.0 (#148070) 2025-07-03 21:04:13 +02:00
Marc Mueller
8330ae2d3a Update license-expression to 30.4.3 (#147941) 2025-07-03 20:22:10 +02:00
tronikos
4b162f09bd Bump androidtvremote2 to 0.2.3 (#148042) 2025-07-03 20:15:47 +02:00
tronikos
9c558fabcd Use AndroidTVRemoteConfigEntry (#148046) 2025-07-03 20:15:36 +02:00
tronikos
5f9cc0a5f6 Add data_description to forms in Android TV Remote (#148045)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Artem Draft <Drafteed@users.noreply.github.com>
2025-07-03 20:13:44 +02:00
Erik Montnemery
bc4a322e81 Improve helpers.frame.report_usage when called from outside the event loop (#148021) 2025-07-03 20:12:52 +02:00
Jeef
b999c5906e Bump weatherflow4py to 1.4.1 (#148054) 2025-07-03 20:11:33 +02:00
Erik Montnemery
d2825e1c80 Don't gather TRIGGER_PLATFORM_SUBSCRIPTIONS (#147954)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-07-03 19:33:28 +02:00
epenet
419e4f3b1d Remove unused module in tuya tests (#148058) 2025-07-03 19:14:27 +02:00
Thomas55555
4a937d2452 Set timeout for remote calendar (#147024) 2025-07-03 10:08:58 -07:00
Noah Husby
01b4a5ceed Bump aiorussound to 4.7.0 (#148057) 2025-07-03 19:04:18 +02:00
Abílio Costa
4e71745c62 Set assist_satellite preannounce default to True (#148060) 2025-07-03 18:41:08 +02:00
Franck Nijhof
6a88ee7a8f Add Task issue form (#148038) 2025-07-03 18:27:51 +02:00
J. Nick Koston
3c4ecffa1b Bump aioesphomeapi to 34.1.0 (#148048) 2025-07-03 17:33:44 +02:00
Joakim Sørensen
244e0f5ea8 Bump hass-nabucasa from 0.104.0 to 0.105.0 (#148040) 2025-07-03 14:24:51 +02:00
epenet
a656b6e26a Use HassKey in media_source (#148011) 2025-07-03 09:56:46 +02:00
epenet
691681a78a Move medcom_ble coordinator to separate module (#148009) 2025-07-03 09:32:57 +02:00
epenet
3bc00824e2 Use runtime_data in mystrom (#148020) 2025-07-03 09:27:38 +02:00
epenet
7d36a2e3a7 Move meteoclimatic coordinator to separate module (#148018) 2025-07-03 09:26:24 +02:00
Norbert Rittel
b1e3561ead Clarify description of autorelock setting in zwave_js (#148019) 2025-07-03 09:23:45 +02:00
epenet
bfc814c839 Use entry.async_on_unload in meteo_france (#148015) 2025-07-03 09:22:27 +02:00
epenet
5008151688 Use entry.async_on_unload in monoprice (#148016) 2025-07-03 09:20:50 +02:00
Franck Nijhof
d738c0d6b1 Merge branch 'master' into dev 2025-07-03 07:04:46 +00:00
epenet
e42235285d Use runtime_data in melcloud (#148012)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-03 08:57:22 +02:00
epenet
04e69479f4 Fix hass.data reference in lookin (#148008) 2025-07-03 08:54:20 +02:00
epenet
b973916032 Move met_eireann coordinator to separate module (#148014) 2025-07-03 08:53:22 +02:00
epenet
6f4757ef42 Use runtime_data in melnor (#148013) 2025-07-03 08:52:40 +02:00
epenet
a6962e9e1e Fix missing port in samsungtv (#147962)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-07-03 08:51:38 +02:00
Marcel van der Veldt
142c10cccc Fix state being incorrectly reported in some situations on Music Assistant players (#147997) 2025-07-03 08:50:41 +02:00
Matthias Alphart
c137c96cfd KNX: use async_load_json_object_fixture in tests (#147991) 2025-07-03 08:00:34 +02:00
Robert Svensson
f0e0c954e7 Bump aiounifi to v84 (#147987) 2025-07-02 23:10:21 +02:00
Norbert Rittel
681961d3a5 Use common config_flow strings in vegehub (#147984) 2025-07-02 22:14:55 +02:00
Matthias Alphart
53d2f6b0c6 KNX: Use a ConfigExtractor helper class for value retrieval (#147983) 2025-07-02 21:49:24 +02:00
G Johansson
78c39f8a06 Remove deprecated battery properties from demo vacuum (#147980) 2025-07-02 21:49:12 +02:00
Ludovic BOUÉ
a748525e03 Allow LevelControl Cluster for Matter Pump devices (#145004)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-07-02 21:48:15 +02:00
Manuel Rüger
8ca1fe83b7 Bump switchbot-api to v2.7.0 (#147978) 2025-07-02 21:36:06 +02:00
Matthias Alphart
8968cf704b Use send_json_auto_id in KNX tests (#147982) 2025-07-02 21:34:30 +02:00
puddly
ebe04466f4 Bump ZHA to 0.0.62 (#147966) 2025-07-02 21:19:32 +02:00
G Johansson
e31470ba5b Change breaking version for battery props in vacuum (#147956) 2025-07-02 19:06:56 +02:00
Franck Nijhof
4bc2951f44 2025.7.0 (#147533) 2025-07-02 18:01:06 +02:00
Franck Nijhof
8334a0398c Bump version to 2025.7.0 2025-07-02 15:12:16 +00:00
Ville Skyttä
80a1e0e4cd Improve huawei_lte config flow class naming (#147910) 2025-07-02 17:02:39 +02:00
Thomas55555
3778f537d5 Remove noisy debug logs in Husgvarna Automower (#147958) 2025-07-02 15:28:42 +01:00
Petro31
adec157d43 Allow trigger based numeric sensors to be set to unknown (#137047)
* Allow trigger based numeric sensors to be set to unknown

* resolve comments

* Do case insensitive check

* use _parse_result

---------

Co-authored-by: abmantis <amfcalt@gmail.com>
2025-07-02 15:35:47 +02:00
Franck Nijhof
8fc3fa51a8 Bump version to 2025.7.0b9 2025-07-02 13:30:51 +00:00
c0ffeeca7
4eb688b560 Z-Wave JS: rename controller to adapter according to term decision (#147955)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-02 13:30:31 +00:00
Simone Chemelli
9472ff5d36 Bump aioamazondevices to 3.2.2 (#147953) 2025-07-02 13:30:29 +00:00
Bram Kragten
12e8b81ec7 Update frontend to 20250702.0 (#147952) 2025-07-02 13:30:28 +00:00
Paulus Schoutsen
ec5e543c09 Ollama: Migrate pick model to subentry (#147944) 2025-07-02 13:30:27 +00:00
Paulus Schoutsen
116c745872 Split Ollama entity (#147769) 2025-07-02 13:30:26 +00:00
Robert Resch
1fdf152292 Bump deebot-client to 13.5.0 (#147938) 2025-07-02 13:27:47 +00:00
G Johansson
b816f1a408 Handle additional errors in Nord Pool (#147937) 2025-07-02 13:27:46 +00:00
John Hess
eb351e6505 Bump thermopro-ble to 0.13.1 (#147924) 2025-07-02 13:27:45 +00:00
Maciej Bieniek
2f27d55495 Open repair issue when outbound WebSocket is enabled for Shelly non-sleeping RPC device (#147901) 2025-07-02 13:26:03 +00:00
Space
fa1bed1849 Skip processing request body for HTTP HEAD requests (#147899)
* Skip processing request body for HTTP HEAD requests

* Use aiohttp's must_be_empty_body() to check whether ingress requests should be streamed

* Only call must_be_empty_body() once per request

* Fix incorrect use of walrus operator
2025-07-02 13:26:01 +00:00
Raphael Hehl
b8c19f23f3 UnifiProtect Change log level from debug to error for connection exceptions in ProtectFlowHandler (#147730) 2025-07-02 13:26:00 +00:00
Erwin Douna
b677ce6c90 SMA add DHCP strictness (#145753)
* Add DHCP strictness (needs beta check)

* Update to check on CONF_MAC

* Update to check on CONF_HOST

* Update hostname

* Polish it a bit

* Update to CONF_HOST, again

* Add split

* Add CONF_MAC add upon detection

* epenet feedback

* epenet round II
2025-07-02 13:25:59 +00:00
c0ffeeca7
d6da686ffe Z-Wave JS: rename controller to adapter according to term decision (#147955)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-02 15:23:08 +02:00
Paulus Schoutsen
f50ef79c72 Ollama: Migrate pick model to subentry (#147944) 2025-07-02 15:20:42 +02:00
Erik Montnemery
943fb9948b Adjust logic related to entity platform state (#147882)
* Adjust logic related to entity platform state

* Break up hard to read if-statement

* Add and improve tests
2025-07-02 14:57:53 +02:00
Raphael Hehl
7447cf329b UnifiProtect Change log level from debug to error for connection exceptions in ProtectFlowHandler (#147730) 2025-07-02 14:57:46 +02:00
Erwin Douna
3d27c0ce52 SMA add DHCP strictness (#145753)
* Add DHCP strictness (needs beta check)

* Update to check on CONF_MAC

* Update to check on CONF_HOST

* Update hostname

* Polish it a bit

* Update to CONF_HOST, again

* Add split

* Add CONF_MAC add upon detection

* epenet feedback

* epenet round II
2025-07-02 14:48:21 +02:00
Simone Chemelli
b7496be61f Bump aioamazondevices to 3.2.2 (#147953) 2025-07-02 14:27:51 +02:00
Bram Kragten
57a98240bd Update frontend to 20250702.0 (#147952) 2025-07-02 14:26:19 +02:00
Ville Skyttä
ff76017ba6 Simplify unnecessary re match.groups()[0] calls (#147909) 2025-07-02 14:12:26 +02:00
Maikel Punie
f10fcde6d8 Remove the deprecated interface paramater for velbus (#147868) 2025-07-02 14:07:47 +02:00
Marc Mueller
a7002e3a24 Update pytest to 8.4.1 (#147951) 2025-07-02 13:02:18 +01:00
tronikos
bbe03dcab7 Add missing Opower tests (#147934) 2025-07-02 13:46:40 +02:00
Andre Lengwenus
f77e6cc8fc Add missing exception translations to LCN (#147723) 2025-07-02 13:41:06 +02:00
Petro31
cb8e076703 Fix missing device_class and state_class on compensation entities (#146115)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-07-02 13:39:19 +02:00
G Johansson
73251fbb1c Handle additional errors in Nord Pool (#147937) 2025-07-02 13:26:47 +02:00
Maciej Bieniek
7ff90ca49d Open repair issue when outbound WebSocket is enabled for Shelly non-sleeping RPC device (#147901) 2025-07-02 13:06:27 +02:00
Manu
bab9ec9976 Add sensor for online status to PlayStation Network (#147842) 2025-07-02 11:47:41 +01:00
Marc Mueller
1051f85ac0 Update coverage to 7.9.1 (#147940) 2025-07-02 12:20:50 +02:00
Marc Mueller
6c7da57af2 Update pytest-cov to 6.2.1 (#147942) 2025-07-02 12:14:27 +02:00
Marc Mueller
73e505d48d Update pytest-xdist to 3.8.0 (#147943) 2025-07-02 12:11:09 +02:00
Marc Mueller
ec65066f5e Update mypy-dev to 1.17.0a4 (#147939) 2025-07-02 12:09:39 +02:00
Robert Resch
9c4951261c Bump deebot-client to 13.5.0 (#147938) 2025-07-02 12:00:48 +02:00
Space
00dfc04b86 Skip processing request body for HTTP HEAD requests (#147899)
* Skip processing request body for HTTP HEAD requests

* Use aiohttp's must_be_empty_body() to check whether ingress requests should be streamed

* Only call must_be_empty_body() once per request

* Fix incorrect use of walrus operator
2025-07-02 11:45:45 +02:00
Manu
bee07ad284 Fix Online ID string in PlayStation Network integration (#147915) 2025-07-02 10:45:07 +02:00
Paulus Schoutsen
b2108fdd40 Update Dockerfile.dev to only use uv for Python (#147926) 2025-07-02 10:40:16 +02:00
John Hess
3730a1a379 Bump thermopro-ble to 0.13.1 (#147924) 2025-07-02 10:11:49 +02:00
Sid
088c02d38a Complete tests for eheimdigital (#143337)
* Complete tests for eheimdigital

* Review

* Review

* Review

* Review

* Fix tests
2025-07-02 10:09:30 +02:00
Harry Heymann
afb247c907 Bump Python Matter server to 8.0.0 (#147783) 2025-07-02 08:12:47 +02:00
Simone Chemelli
77dcba0984 Manager wrong country selection in Alexa Devices (#147914)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-07-02 08:02:53 +02:00
Simone Chemelli
48f9a12cca Bump aioamazondevices to 3.2.1 (#147912) 2025-07-02 07:36:41 +02:00
J. Nick Koston
bdd2ac9ae4 Bump bluetooth-data-tools to 1.28.2 (#147920) 2025-07-02 07:34:40 +02:00
Ivan Lopez Hernandez
2e7113d881 Swap the Models label for the model name not it's display name, (#147918)
Swap display name for name.
2025-07-01 21:12:58 -07:00
Sid
6842bfae4c Bump eheimdigital to 1.3.0 (#147908) 2025-07-01 23:00:25 +01:00
nadimz
392cde20d9 Add support for opening state in template lock (#147813)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-07-01 22:03:20 +01:00
cristianburrini
a6146fb5a9 Increase the number of irrigation zones up to 8 for Tuya enabled controllers. (#147793) 2025-07-01 22:40:36 +02:00
Jesse Hills
6104731d53 Remove codeowner from ESPHome (#147850) 2025-07-01 22:09:23 +02:00
Erik Montnemery
66308a848a Set Entity._platform_state in google_assistant tests (#147892) 2025-07-01 21:46:36 +02:00
Erik Montnemery
c71dbd9d4d Set Entity._platform_state in universal tests (#147894) 2025-07-01 21:46:01 +02:00
Erik Montnemery
1195c2ec10 Set Entity._platform_state in core customize test (#147895) 2025-07-01 21:45:08 +02:00
Norbert Rittel
78a9cd9201 Use (new) common state "Empty" for water level in switchbot (#147836) 2025-07-01 21:43:21 +02:00
Erik Montnemery
639a749a0f Mock recorder in ista_ecotrend tests (#147893) 2025-07-01 20:09:48 +01:00
Simone Chemelli
058f3b8b6e Add reauth to Alexa Devices config flow (#147773) 2025-07-01 20:57:24 +02:00
Manu
926e9261ab Add switch to enable/disable boost in IronOS integration (#147831) 2025-07-01 20:53:13 +02:00
Erik Montnemery
d6fb860889 Use entity_registry_enabled_by_default fixture in dsmr_reader tests (#147891) 2025-07-01 20:50:38 +02:00
Marcel van der Veldt
5e03900e0a Bump Music Assistant Client to 1.2.3 (#147885) 2025-07-01 20:26:26 +02:00
Erik Montnemery
1e6e5ca1b6 Fix broadlink tests (#147890) 2025-07-01 18:32:58 +01:00
Erik Montnemery
60e3b38de1 Set Entity._platform_state in arcam_fmj tests (#147889) 2025-07-01 17:58:15 +02:00
epenet
852522219c Use correctly formatted MAC in bond tests (#147887) 2025-07-01 17:56:10 +02:00
epenet
23f1e8d1a3 Use correctly formatted MAC in elkm1 tests (#147888) 2025-07-01 17:55:46 +02:00
avee87
655f009f07 Fix station name sensor for metoffice (#145500) 2025-07-01 16:18:13 +02:00
Jamin
59bf39f4ed Bump VoIP utils to 0.3.3 (#147880) 2025-07-01 16:09:51 +02:00
Fredrik Mårtensson
510e3977df Add water_level sensor to Tuya pet fountain cwysj (#146602)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-07-01 14:57:17 +01:00
micha91
922720576a fix: Create new aiohttp session with DummyCookieJar (#147827) 2025-07-01 15:50:04 +02:00
Paul Bottein
e10b581d4b Fix Meteo france Ciel clair condition mapping (#146965)
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2025-07-01 15:43:34 +02:00
hanwg
e38eac9415 Include chat ID in Telegram bot subentry title (#147643) 2025-07-01 15:42:32 +02:00
Maciej Bieniek
11c9aa9280 Bump Nettigo Air Monitor backend library to version 5.0.0 (#147812) 2025-07-01 15:39:29 +02:00
Paul Bottein
52c86f8a6a Update frontend to 20250701.0 (#147879) 2025-07-01 15:38:04 +02:00
Marc Mueller
6364a9ad98 Update pillow to 11.3.0 (#147869) 2025-07-01 14:31:06 +01:00
Manu
651162b8e7 Fix error in last online sensor of PlayStation integration (#147844)
* Fix Last online sensor

* set unavailable

* available_fn
2025-07-01 15:17:10 +02:00
Denis Shulyaka
7deca35172 Add multiple LLM API support for MCP Server (#147785)
* Add multiple LLM API support for MCP Server

* Update homeassistant/components/mcp_server/config_flow.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* ruff

* Update tests/components/mcp_server/conftest.py

Co-authored-by: Allen Porter <allen.porter@gmail.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Allen Porter <allen.porter@gmail.com>
2025-07-01 06:14:03 -07:00
epenet
073a467fb2 Use correctly formatted MAC in bond tests (#147870) 2025-07-01 14:41:31 +02:00
epenet
3f9590b03b Use correctly formatted MAC in gogogate2 tests (#147872) 2025-07-01 14:41:20 +02:00
epenet
b47f989c77 Use correctly formatted MAC in wmspro tests (#147876) 2025-07-01 14:40:41 +02:00
epenet
4ebffa8d23 Use correctly formatted MAC in palazzetti tests (#147875) 2025-07-01 14:40:27 +02:00
epenet
c5873c6dd0 Use correctly formatted MAC in dlink tests (#147871) 2025-07-01 14:40:12 +02:00
Erik Montnemery
2cb80e083e Initialize EsphomeEntity._has_state (#147877) 2025-07-01 07:33:33 -05:00
epenet
871296dff6 Use correctly formatted MAC in lamarzocco tests (#147874) 2025-07-01 14:13:21 +02:00
Claudio Ruggeri - CR-Tech
c92873bbff Change default slave id from 0 to 1 in modbus actions (#142865)
* set default slave id in service calls

* add test

* revert out of scope change
2025-07-01 13:15:32 +02:00
Norbert Rittel
5fea4915ef Use (new) common state "Empty" in litterrobot (#147835) 2025-07-01 13:13:12 +02:00
dependabot[bot]
8fa016059d Bump github/codeql-action from 3.29.1 to 3.29.2 (#147867)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-01 12:30:01 +02:00
Bob Laz
61a29db72c fix state_class for water used today sensor (#147787) 2025-07-01 12:28:13 +02:00
epenet
5a3aa7874d Use correctly formatted MAC in airthings tests (#147817) 2025-07-01 12:26:10 +02:00
Parker Brown
12e2493c42 Capitalize "version" in Tesla fleet strings (#146501) 2025-07-01 12:18:55 +02:00
Paulus Schoutsen
659cd42739 Move async_reload on updates in async_setup_entry in Anthropic (#147862)
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-01 12:16:00 +02:00
Paulus Schoutsen
7fcea17e83 Move async_reload on updates in async_setup_entry in OpenAI Conversation (#147863)
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-01 12:15:28 +02:00
Paulus Schoutsen
30a85c40da Move async_reload on updates in async_setup_entry in Ollama (#147861)
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-01 12:14:46 +02:00
epenet
57a8f1e0cc Use correctly formatted MAC in rehlko tests (#147864) 2025-07-01 12:09:00 +02:00
epenet
78aeae577d Use correctly formatted MAC in roomba tests (#147865) 2025-07-01 11:24:08 +02:00
epenet
3f95cb37e6 Use correctly formatted MAC in sma tests (#147866) 2025-07-01 11:23:31 +02:00
epenet
12aef4aae5 Use correctly formatted MAC in knocki tests (#147821) 2025-07-01 11:22:48 +02:00
Thomas55555
2e12db001d Fix wrong state in Husqvarna Automower (#146075) 2025-07-01 10:53:55 +02:00
epenet
573325be97 Use correctly formatted MAC in home_connect tests (#147818) 2025-07-01 10:51:49 +02:00
Erik Montnemery
7021fe7495 Correct openai conversation config entry migration (#147859) 2025-07-01 10:49:07 +02:00
Erik Montnemery
b7999755bd Correct anthropic config entry migration (#147857) 2025-07-01 10:47:06 +02:00
Erik Montnemery
99f7a031d6 Correct Google generative AI config entry migration (#147856) 2025-07-01 10:46:13 +02:00
Erik Montnemery
8fc31283b7 Correct ollama config entry migration (#147858) 2025-07-01 10:45:17 +02:00
Jan-Philipp Benecke
5ff698c78d Catch access denied errors in webdav and display proper message (#147093) 2025-07-01 10:15:45 +02:00
Jesse Hills
9469c6ad1c Implement suggested_display_precision for ESPHome (#147849) 2025-07-01 09:16:23 +02:00
Norbert Rittel
35f0505c7b Use (new) common state "Empty" in whirlpool (#147847)
Use (new) common state "Empty"
2025-07-01 08:59:55 +02:00
Norbert Rittel
a180cabea9 Use (new) common state "Full" in overkiz (#147848)
Use (new) common state "Full"
2025-07-01 08:58:31 +02:00
Jan Bouwhuis
4f7348b8bc Fix invalid configuration of MQTT device QoS option in subentry flow (#147837) 2025-07-01 08:46:58 +02:00
On Freund
ddf56f053b Support device removal in CoolMasterNet integration (#147851) 2025-07-01 08:26:04 +02:00
G Johansson
9719d2ef2b Start deprecation of battery properties in vacuum (#146401)
* Start deprecation of battery properties in vacuum

* Small fixes

* Fixes

* Deprecate battery supported feature
2025-07-01 08:23:47 +02:00
Manu
2afe475234 Add more mac address prefixes for discovery to PlayStation Network (#147739) 2025-07-01 07:12:00 +02:00
Norbert Rittel
23c304fc75 Use (new) common state "Full" in enphase_envoy (#147834)
Use (new) common state "Full"
2025-06-30 20:13:05 -04:00
Norbert Rittel
84645d0ca6 Use (new) common states for "Full" and "Empty" in lg_thinq (#147833)
Use (new) common states for "Full" and "Empty"
2025-07-01 01:59:33 +02:00
Norbert Rittel
2bdfc8cf5e Add common states "Empty" and "Full" (#146646) 2025-06-30 22:08:55 +02:00
epenet
603e277a5b Add docstring to DhcpServiceInfo MAC address (#147823)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-06-30 21:54:05 +02:00
Paulus Schoutsen
38a7b21052 Split Anthropic entity (#147770) 2025-06-30 21:47:44 +02:00
Paulus Schoutsen
bf74ba990a Split Ollama entity (#147769) 2025-06-30 21:31:54 +02:00
Paulus Schoutsen
70856bd92a Split OpenAI entity (#147771) 2025-06-30 21:11:51 +02:00
Paulus Schoutsen
be6b624081 Improve validation for media selector (#147768) 2025-06-30 20:26:52 +02:00
mvn23
217fbb2849 Populate hvac_modes list in opentherm_gw (#142074) 2025-06-30 20:24:13 +02:00
epenet
22a14da19c Rename service registration method (#146615) 2025-06-30 20:21:38 +02:00
puddly
20f5d85800 Await firmware installation task when flashing ZBT-1/Yellow firmware (#147824) 2025-06-30 20:18:22 +02:00
hanwg
88feb5139b Fix Telegram bot proxy URL not initialized when creating a new bot (#147707) 2025-06-30 20:16:45 +02:00
Hessel
90cbe272a0 Wallbox Integration, Reduce API impact by limiting the amount of API calls made (#147618) 2025-06-30 20:15:48 +02:00
Paulus Schoutsen
511b739bf6 Use media selector for Assist Satellite actions (#147767)
Co-authored-by: Michael Hansen <mike@rhasspy.org>
2025-06-30 20:12:03 +02:00
Manu
9961a499ee Fix sensor displaying unknown when getting readings from heat meters in ista EcoTrend (#147741) 2025-06-30 20:11:46 +02:00
rubenbe
d8c7ed473b Bump xiaomi-ble to 1.1.0 (#147828)
Bump xiaomi-ble to 1.1.0
2025-06-30 20:11:03 +02:00
Manu
2c30a5a14c Improve exception handling of PlayStation Network (#147792) 2025-06-30 19:53:46 +02:00
Manu
5e3fc858d8 Add sensor last online to PlayStation Network integration (#147796) 2025-06-30 19:52:11 +02:00
epenet
f03af213d4 Use correctly formatted MAC in lg_thinq tests (#147822) 2025-06-30 19:50:50 +02:00
epenet
1e3ebd5650 Use correctly formatted MAC in incomfort tests (#147819) 2025-06-30 18:02:42 +02:00
epenet
53936ab062 Use async_load_fixture in weatherflow_cloud (#147816) 2025-06-30 18:01:14 +02:00
Bouwe Westerdijk
b52a248def Bump plugwise to v1.7.7 and adapt (#147809) 2025-06-30 14:40:10 +01:00
Jeef
ea70229426 Add Weatherflow Cloud wind support via websocket (#125611)
* rebase off of dev

* update tests

* update tests

* addressing PR finally

* API to back

* adding a return type

* need to test

* removed teh extra check on available

* some changes

* ready for re-review

* change assertions

* remove icon function

* update ambr

* ruff

* update snapshot and push

* Update homeassistant/components/weatherflow_cloud/coordinator.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/components/weatherflow_cloud/coordinator.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* enhnaced tests

* better coverage

* Update homeassistant/components/weatherflow_cloud/coordinator.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/components/weatherflow_cloud/coordinator.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/components/weatherflow_cloud/coordinator.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/components/weatherflow_cloud/coordinator.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/components/weatherflow_cloud/coordinator.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* remove comments

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-06-30 15:26:17 +02:00
Erik Montnemery
741a3d5009 Remove backup helper (#143558)
* Remove backup helper

* Update aws_s3 tests
2025-06-30 14:11:10 +02:00
Pete Sage
ee8830cc77 Person ble_trackers for non-home zones not processed correctly (#138475)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-06-30 13:35:19 +02:00
Bouwe Westerdijk
7fbf25e862 Plugwise: remove outdated fixtures (#147806) 2025-06-30 12:15:52 +02:00
epenet
e642cd45ae Enforce async_load_fixture in async test functions (#145709) 2025-06-30 11:56:26 +02:00
dependabot[bot]
179e1c2b00 Bump github/codeql-action from 3.29.0 to 3.29.1 (#147799)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-30 11:53:30 +02:00
Phill (pssc)
52a99aea0c Squeezebox: Fix Allow server device details to merge with players with the same MAC (#133517)
* Disambiguate bewtween servers and player to stop them being merged

* ruff format

* make SqueezeLite players not a service

* ruff

* Tidy redunant code

* config url

* revert config url

* change to domain server

* use default to see how they are mereged with server device

* refactor to use defaults so where a player is part of a bigger ie server service device in the same intergration it doesnt replace its information

* ruff

* make test match the new data

* Fix merge

* Fix tests

* Fix meregd test data

* Fix all tests add new test for merged device in reg

* Remove info from device_info so its only a lookup

* manual merge of server player shared devices

* Fix format of merged entires

* fixes for testing

* Fix test with input from @peteS-UK device knowlonger exits for this test

* Fix test now device doesnt exits for tests

* Update homeassistant/components/squeezebox/media_player.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix Copilots formatting

* Apply suggestions from code review

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-06-30 11:41:22 +02:00
Paulus Schoutsen
c7b2f236be Type Z-Wave JS config entry (#147456)
* Type Z-Wave JS config entry

* Migrate to data class
2025-06-30 11:15:12 +02:00
Evan Severson
a6e3da43ca Fixed pushbullet handling of fields longer than 255 characters (#146993) 2025-06-30 11:08:50 +02:00
Steffen Rusitschka
4d58024d5d Add publish_string_states config to zabbix (#134773)
* Add include_strings config to zabbix

* Remove commented code

* Fix ruff formatting

* Update homeassistant/components/zabbix/__init__.py

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>

* Update homeassistant/components/zabbix/__init__.py

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>

* Don't use dict.get, CONF_INCLUDE_STRINGS has a default value and will always be set.

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Convert to string only when include_strings is true

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* change to guard

* Fix review comments

* ruff, mypy, pylint fixes

* more ruff, mypy fixes

* and another ruff format fix

---------

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-06-30 10:52:33 +02:00
Manu
c7603b39ec Fix inputs to correctly handle Fahrenheit in IronOS (#135421)
* Fix inputs to correctly handle Fahrenheit in IronOS

* some refactoring

* add boost switch entity

* Revert switch entity

* refactor

* remove commented code

* some changes
2025-06-30 10:44:39 +02:00
epenet
c17ee0d123 Allow binary sensor template to return state unknown (#128861)
* Allow binary sensor template to return state unknown

* Add tests

* Adjust TriggerBinarySensorEntity

* Add restore tests for BinarySensorTemplate

* Add tests for TriggerBinarySensorEntity

* Tweak

* Tweak

* Adjust tests

* Adjust
2025-06-30 10:06:05 +02:00
Alexandre CUER
97c1e21a69 Add possibility to synchronize automatically all available feeds in emoncms (#128122)
* Add checkbox in options to sync all feeds once

* Add sync mode selector in async_step_user
Remove checkbox in options

* Correct use of SYNC_MODE & SYNC_MODE_AUTO in tests

* Use dropdown for mode selection

* rmv_unused_const

* Add separate tests + use SelectSelector
2025-06-30 10:05:07 +02:00
starkillerOG
c9a6b1fd45 Bump reolink_aio to 0.14.2 (#147797) 2025-06-30 09:39:02 +02:00
mkmer
05ceee568e Honeywell: Don't use shared session (#147772) 2025-06-29 21:22:59 +02:00
Shay Levy
08a6b38699 Bump aioshelly to 13.7.1 (#146221)
* Bump aioshelly to 13.8.0

* Change version to 13.7.1
2025-06-29 21:41:50 +03:00
Norbert Rittel
4add346272 Deduplicate strings and fix sentence-casing in proximity (#147777)
* Deduplicate strings and fix sentence-casing in `proximity`

* Update test_init.py
2025-06-29 21:00:16 +03:00
Andre Lengwenus
369c8d1e0d Bump pypck to 0.8.10 (#147774) 2025-06-29 20:58:41 +03:00
tronikos
25ab47a587 Move the async_reload on updates in async_setup_entry in Google Generative AI (#147748)
Move the async_reload on updates in async_setup_entry
2025-06-29 07:56:37 +02:00
Marc Hörsken
617ea1925c Update pywmspro to 0.3.0 to wait for short-lived actions (#147679)
Replace action delays with detailed action responses.
2025-06-29 07:33:44 +02:00
cdnninja
8bacab4f9c Fix Vesync set_percentage error (#147751) 2025-06-29 07:22:04 +02:00
J. Nick Koston
6d28b99344 Preserve httpx boolean behavior in REST integration after aiohttp conversion (#147738) 2025-06-28 17:24:09 -05:00
cnico
bbd1cbf5c9 Correct Chlorine unit definition in flipr integration (#147537)
* Correction of bug 145683

* constant for chlorine unit correction

* constant name correction

* Review correction
2025-06-28 22:29:24 +01:00
starkillerOG
43450d4489 Reduce idle timeout of HLS stream to conserve camera battery life (#147728)
* Reduce IDLE timeout of HLS stream to conserve camera battery life

* adjust tests
2025-06-28 22:20:47 +02:00
J. Nick Koston
f8c052e0ce Improve rest error logging (#147736)
* Improve rest error logging

* Improve rest error logging

* Improve rest error logging

* Improve rest error logging

* Improve rest error logging

* top level
2025-06-28 22:18:46 +02:00
Florian von Garrel
1f3bdfc7b7 bump pypaperless to 4.1.1 (#147735) 2025-06-28 22:13:51 +02:00
Antoni Czaplicki
0652bffd68 Bump vulcan-api to 2.4.2 (#146857) 2025-06-28 22:11:59 +02:00
Manu
8322611099 Use test parametrization in ista EcoTrend integration (#147729) 2025-06-28 21:57:51 +02:00
Marc Hörsken
134967b817 Fix error if cover position is not available or unknown (#147732) 2025-06-28 21:57:26 +02:00
Shay Levy
39abae36f0 Fix Shelly Block entity removal (#147694) 2025-06-28 22:40:58 +03:00
Marc Mueller
227760f203 Fix RuntimeWarnings in homeassistant_yellow tests (#147724) 2025-06-28 20:31:01 +02:00
Jan Bouwhuis
969809456e Move MQTT device sw and hw version to collapsed section in subentry flow (#147685)
Move MQTT device sw and hw version to collapsed section
2025-06-28 11:25:59 +02:00
Daniel Hjelseth Høyer
d2e8a48b2c Bump pytibber to 0.31.6 (#147703) 2025-06-28 10:11:17 +02:00
epenet
ea6332ee42 Move backup services to separate module (#146427) 2025-06-27 20:54:56 +02:00
Erik Montnemery
91c3b43d7f Improve comment for helpers.entity.entity_sources (#146529) 2025-06-27 20:54:19 +02:00
Thomas55555
1d82d44794 Add device prefix to summary in Husqvarna Automower (#147405) 2025-06-27 20:34:50 +02:00
Thomas55555
571376badc Bump aioautomower to 1.0.1 (#147683) 2025-06-27 20:28:45 +02:00
Manu
32236b2f4d Add reconfiguration flow to PlayStation Network (#147552) 2025-06-27 20:17:06 +02:00
Samuel Xiao
18c1953bc5 Add lock models to switchbot cloud (#147569) 2025-06-27 20:16:21 +02:00
Bernardus Jansen
d874c28dc9 Add previously missing state classes to dsmr sensors (#147633) 2025-06-27 19:45:36 +02:00
Brett Adams
19d89c8952 Fix energy history in Teslemetry (#147646) 2025-06-27 19:43:03 +02:00
Ludovic BOUÉ
e3ba1f34ca Matter TemperatureControl (#145706)
* TemperatureControl

* Add tests

* Commands.SetTemperature

* Update homeassistant/components/matter/number.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update number.py

* Update number.py

* Update number.py

* Update homeassistant/components/matter/number.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Refactor MatterRangeNumber to streamline command handling in async_set_native_value

* testing requested changes

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-06-27 19:41:39 +02:00
Thomas55555
b630fb0520 Respect availability of parent class in Husqvarna Automower (#147649) 2025-06-27 19:38:42 +02:00
Ville Skyttä
5129f89086 Finish config flow in huawei_lte SSDP test (#147542) 2025-06-27 19:00:01 +02:00
Ville Skyttä
0be0e22e76 Simplify rflink dimmable set_level parsing (#147636) 2025-06-27 18:59:10 +02:00
epenet
b8500b338a Improve tests for binary sensor template (#147657) 2025-06-27 18:58:16 +02:00
Simone Chemelli
4cab3a0465 Bump aioamazondevices to 3.1.22 (#147681) 2025-06-27 18:44:01 +02:00
hanwg
ff711324d5 Add codeowner for Telegram bot (#147680) 2025-06-27 18:18:01 +02:00
Michael
113e7dc003 Add data descriptions to PEGELONLINE integration (#147594) 2025-06-27 18:16:38 +02:00
Shay Levy
2120ff6a0a Fix Shelly entity removal (#147665) 2025-06-27 18:50:35 +03:00
Marc Mueller
8ee5c30754 Update ruff to 0.12.1 (#147677) 2025-06-27 17:40:08 +02:00
Paul Bottein
a1518b96c4 Update frontend to 20250627.0 (#147668) 2025-06-27 17:28:14 +02:00
Petar Petrov
bba7f5c3f0 Z-WaveJS config flow: Change keys question (#147518)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-06-27 17:27:43 +02:00
Manu
8a5671af76 Remove dweet.io integration (#147645) 2025-06-27 17:23:42 +02:00
Raphael Hehl
8a18dea8c7 UniFi Protect removing early access checks and issue creation (#147432)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-27 17:15:34 +02:00
Thomas55555
4b02f22724 Bump aioautomower to 1.0.0 (#147676) 2025-06-27 17:02:52 +02:00
mkmer
7229c2ca2c Bump aiosomecomfort to 0.0.33 (#147673) 2025-06-27 16:32:25 +02:00
Norbert Rittel
d83eddf13b Fix sentence-casing and spacing of button in thermopro (#147671) 2025-06-27 15:53:18 +02:00
Josef Zweck
4a192a7b09 Bump jellyfin-apiclient-python to 1.11.0 (#147658) 2025-06-27 11:07:14 +02:00
Josef Zweck
58c434887e Fix: Unhandled NoneType sessions in jellyfin (#147659) 2025-06-27 11:00:23 +02:00
Abílio Costa
78c2405e61 Bump whirlpool to 0.21.1 (#147611) 2025-06-27 10:33:49 +02:00
Josef Zweck
8cc4105984 Make jellyfin not single config entry (#147656) 2025-06-27 10:31:13 +02:00
Josef Zweck
917f1e4c6f Make entities unavailable when machine is physically off in lamarzocco (#147426) 2025-06-27 10:03:14 +02:00
hanwg
3879f6d2ef Fix Telegram bot yaml import for webhooks containing None value for URL (#147586) 2025-06-27 10:03:03 +02:00
Norbert Rittel
78060e4833 Clarify descriptions of subaru.unlock_specific_door action (#147655) 2025-06-27 10:01:44 +02:00
Guido Schmitz
fda66c4be4 Handle deleted devices dynamically in devolo Home Control (#147585) 2025-06-27 09:52:00 +02:00
Michael
21131d00b3 Fix config schema to make credentials optional in NUT flows (#147593) 2025-06-27 09:51:28 +02:00
puddly
a84313de33 Allow setup of Zigbee/Thread for ZBT-1 and Yellow without internet access (#147549)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-06-27 09:50:45 +02:00
Manu
c73346e6b3 Bump pynecil to v4.1.1 (#147648) 2025-06-27 09:31:35 +02:00
Franck Nijhof
55a37a2936 Extend GitHub Copilot instructions with new learnings from reviews (#147652) 2025-06-27 09:01:09 +02:00
Abílio Costa
e481f14335 Simplify reolink light tests (#147637) 2025-06-27 08:58:09 +02:00
Petar Petrov
1ca03c8ae9 Do not factory reset old Z-Wave controller during migration (#147576)
* Do not factory reset old Z-Wave controller during migration

* PR comments

* remove obsolete test
2025-06-27 08:02:12 +02:00
Ville Skyttä
61b43ca1fc Remove unnecessary wilight trigger regex use (#147638) 2025-06-26 23:16:21 +01:00
Joost Lekkerkerker
1b2be083c2 Make sure Google Generative AI integration migration is clean (#147625) 2025-06-26 23:03:36 +02:00
Joost Lekkerkerker
4bdf3d6f30 Make sure OpenAI integration migration is clean (#147627) 2025-06-26 23:03:11 +02:00
Joost Lekkerkerker
43535ede8b Make sure Anthropic integration migration is clean (#147629) 2025-06-26 23:02:59 +02:00
Joost Lekkerkerker
9bd0762799 Make sure Ollama integration migration is clean (#147630) 2025-06-26 23:02:35 +02:00
Ville Skyttä
1bb653b4f7 Remove unused config regexps (#147631) 2025-06-26 23:02:14 +02:00
Franck Nijhof
2655edcfc8 Extend GitHub Copilot instructions and make it suitable for Claude Code (#147632) 2025-06-26 23:00:02 +02:00
Franck Nijhof
7a08edc3dd Add Claude to gitignore (#147622) 2025-06-26 21:06:34 +02:00
Abílio Costa
b3131355b0 Use non-autospec mock for Reolink's light tests (#147621) 2025-06-26 21:05:23 +02:00
Abílio Costa
06d04c001d Use non-autospec mock for Reolink's host tests (#147619) 2025-06-26 20:55:46 +02:00
Jack Powell
babecdf32c Add Diagnostics to PlayStation Network (#147607)
* Add Diagnostics support to PlayStation_Network

* Remove unused constant

* minor cleanup

* Redact additional data

* Redact additional data
2025-06-26 20:52:07 +02:00
Renat Sibgatulin
17cd39748b Create a new client session for air-Q to fix cookie polution (#147027) 2025-06-26 19:59:49 +02:00
Simone Chemelli
c2f1e86a4e Add action exceptions to Alexa Devices (#147546) 2025-06-26 19:59:02 +02:00
Joost Lekkerkerker
61a32466b6 Hide Telegram bot proxy URL behind section (#147613)
Co-authored-by: Manu <4445816+tr4nt0r@users.noreply.github.com>
2025-06-26 19:55:38 +02:00
Manu
aef08091f8 Fix asset url in Habitica integration (#147612) 2025-06-26 19:52:58 +02:00
Joost Lekkerkerker
1416f0f1e0 Fix meaters not being added after a reload (#147614) 2025-06-26 19:52:29 +02:00
HarvsG
af7b1a76bc Add description placeholders to SchemaFlowFormStep (#147544)
* test description placeholders

* Update test_schema_config_entry_flow.py

* fix copy and paste indentation

* Apply suggestions from code review

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-06-26 19:51:31 +02:00
Maximilian Arzberger
bf88fcd5bf Add Manual Charge Switch for Installers for Kostal Plenticore (#146932)
* Add Manual Charge Switch for Installers

* Update stale docstring

* Installer config fixture

* fix ruff
2025-06-26 19:50:27 +02:00
Joost Lekkerkerker
35478e3162 Set Google AI model as device model (#147582)
* Set Google AI model as device model

* fix
2025-06-26 19:44:15 +02:00
Joost Lekkerkerker
69af74a593 Improve explanation on how to get API token in Telegram (#147605) 2025-06-26 18:21:56 +02:00
tronikos
b4dd912bee Refactor in Google AI TTS in preparation for STT (#147562) 2025-06-26 11:53:16 -04:00
Bram Kragten
b5821ef499 Update frontend to 20250626.0 (#147601) 2025-06-26 17:46:45 +02:00
Fabio Natanael Kepler
1a92d4530e Fix playing TTS and local media source over DLNA (#134903)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-06-26 17:12:15 +02:00
Joost Lekkerkerker
7b80c1c693 Add default conversation name for OpenAI integration (#147597) 2025-06-26 17:11:48 +02:00
Joost Lekkerkerker
e7cc03c1d9 Add default title to migrated Claude entry (#147598) 2025-06-26 17:11:13 +02:00
Luca Angemi
69f0b6244a Remove default icon for wind direction sensor for Buienradar (#147603)
* Fix wind direction state class sensor

* Remove default icon for wind direction sensor
2025-06-26 17:05:59 +02:00
Joost Lekkerkerker
01205f8a14 Add default title to migrated Ollama entry (#147599) 2025-06-26 17:05:26 +02:00
hanwg
68924d23ab Fix Telegram bot default target when sending messages (#147470)
* handle targets

* updated error message

* validate chat id for single target

* add validation for chat id

* handle empty target

* handle empty target
2025-06-26 16:43:09 +02:00
Artur Pragacz
40f553a007 Migrate device connections to a normalized form (#140383)
* Normalize device connections migration

* Update version

* Slightly improve tests

* Update homeassistant/helpers/device_registry.py

* Add validators

* Fix validator

* Move format mac function too

* Add validator test

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-06-26 15:33:34 +02:00
Robin Lintermann
bc46894b74 Fixed issue when tests (should) fail in Smarla (#146102)
* Fixed issue when tests (should) fail

* Use usefixture decorator

* Throw ConfigEntryError instead of AuthFailed
2025-06-26 15:30:03 +02:00
Anders Peter Fugmann
6f4615f012 Bump dependency on pyW215 for DLink integration to 0.8.0 (#147534) 2025-06-26 12:56:46 +02:00
Joost Lekkerkerker
4244d2f66f Set right model in OpenAI conversation (#147575) 2025-06-26 12:49:33 +02:00
Petar Petrov
a73dafe097 Hide unnamed paths when selecting a USB Z-Wave adapter (#147571)
* Hide unnamed paths when selecting a USB Z-Wave adapter

* remove pointless sorting
2025-06-26 12:15:02 +02:00
Stefan Agner
be49296547 Deduplicate shared logic in Matter vacuum commands (#147578)
Get the run mode by tag in a single place to avoid code duplication.
Also raise an error if the run mode (unexpectedly) is not found.
2025-06-26 11:54:52 +02:00
Marcel van der Veldt
d55ecd885e Do not make the favorite button unavailable when no content playing on a Music Assistant player (#147579) 2025-06-26 11:49:06 +02:00
Luca Angemi
076248c455 Fix wind direction state class sensor for AEMET (#147535) 2025-06-26 11:07:07 +02:00
Petar Petrov
13ce27c94c Remove obsolete routing info when migrating a Z-Wave network (#147568) 2025-06-26 11:06:36 +02:00
Joost Lekkerkerker
4b9b08ece5 Show current Lametric version if there is no newer version (#147538) 2025-06-26 10:55:31 +02:00
Simone Chemelli
79df38eff2 Improve config flow strings for Alexa Devices (#147523) 2025-06-26 10:52:14 +02:00
tronikos
fb133664e4 Include subentries in Google Generative AI diagnostics (#147558) 2025-06-26 10:50:47 +02:00
Marcel van der Veldt
38669ce96c Fix sending commands to Matter vacuum (#147567) 2025-06-26 10:47:24 +02:00
Petar Petrov
651b33d49b Bump zwave-js-server-python to 0.65.0 (#147561)
* Bump zwave-js-server-python to 0.65.0

* update tests
2025-06-26 10:11:25 +03:00
Erik Montnemery
3b64db5f76 Set end date for when allowing unique id collisions in config entries (#147516)
* Set end date for when allowing unique id collisions in config entries

* Update test
2025-06-26 08:20:26 +02:00
tronikos
0f95fe566c Use default title for migrated Google Generative AI entries (#147551) 2025-06-25 22:30:41 -04:00
Simone Chemelli
6290facffb Fix unload for Alexa Devices (#147548) 2025-06-26 01:55:58 +02:00
tronikos
f0a78aadbe Fixes in Google AI TTS (#147501)
* Fix Google AI not using correct config options after subentries migration

* Fixes in Google AI TTS

* Fix tests by @IvanLH

* Change type name.

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2025-06-25 18:12:23 -04:00
Pete Sage
345ec97dd5 Add enum sensor for Sonos Power Source (#147449)
* feat: add power source sensor

* fix: translations

* fix:cleanup

* fix: simpify

* fix: improve coverage

* fix: improve coverage

* fix: add missing test

* fix: call it charging_base

* fix: disable entity by default

* update snapshots

* Update homeassistant/components/sonos/strings.json

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* fix: update test

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-06-25 23:49:06 +02:00
Franck Nijhof
1286b5d9d8 Bump version to 2025.8.0dev0 (#147531) 2025-06-25 21:38:35 +02:00
883 changed files with 31850 additions and 12571 deletions

View File

@@ -8,6 +8,7 @@
"PYTHONASYNCIODEBUG": "1"
},
"features": {
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
"ghcr.io/devcontainers/features/github-cli:1": {}
},
// Port 5683 udp is used by Shelly integration

53
.github/ISSUE_TEMPLATE/task.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: Task
description: For staff only - Create a task
type: Task
body:
- type: markdown
attributes:
value: |
## ⚠️ RESTRICTED ACCESS
**This form is restricted to Open Home Foundation staff, authorized contributors, and integration code owners only.**
If you are a community member wanting to contribute, please:
- For bug reports: Use the [bug report form](https://github.com/home-assistant/core/issues/new?template=bug_report.yml)
- For feature requests: Submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)
---
### For authorized contributors
Use this form to create tasks for development work, improvements, or other actionable items that need to be tracked.
- type: textarea
id: description
attributes:
label: Description
description: |
Provide a clear and detailed description of the task that needs to be accomplished.
Be specific about what needs to be done, why it's important, and any constraints or requirements.
placeholder: |
Describe the task, including:
- What needs to be done
- Why this task is needed
- Expected outcome
- Any constraints or requirements
validations:
required: true
- type: textarea
id: additional_context
attributes:
label: Additional context
description: |
Any additional information, links, research, or context that would be helpful.
Include links to related issues, research, prototypes, roadmap opportunities etc.
placeholder: |
- Roadmap opportunity: [link]
- Epic: [link]
- Feature request: [link]
- Technical design documents: [link]
- Prototype/mockup: [link]
- Dependencies: [links]
validations:
required: false

File diff suppressed because it is too large Load Diff

View File

@@ -37,10 +37,10 @@ on:
type: boolean
env:
CACHE_VERSION: 3
CACHE_VERSION: 4
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.7"
HA_SHORT_VERSION: "2025.8"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13']"
# 10.3 is the oldest supported version

View File

@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.29.0
uses: github/codeql-action/init@v3.29.2
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.29.0
uses: github/codeql-action/analyze@v3.29.2
with:
category: "/language:python"

View File

@@ -0,0 +1,84 @@
name: Restrict task creation
# yamllint disable-line rule:truthy
on:
issues:
types: [opened]
jobs:
check-authorization:
runs-on: ubuntu-latest
# Only run if this is a Task issue type (from the issue form)
if: github.event.issue.issue_type == 'Task'
steps:
- name: Check if user is authorized
uses: actions/github-script@v7
with:
script: |
const issueAuthor = context.payload.issue.user.login;
// First check if user is an organization member
try {
await github.rest.orgs.checkMembershipForUser({
org: 'home-assistant',
username: issueAuthor
});
console.log(`✅ ${issueAuthor} is an organization member`);
return; // Authorized, no need to check further
} catch (error) {
console.log(` ${issueAuthor} is not an organization member, checking codeowners...`);
}
// If not an org member, check if they're a codeowner
try {
// Fetch CODEOWNERS file from the repository
const { data: codeownersFile } = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: 'CODEOWNERS',
ref: 'dev'
});
// Decode the content (it's base64 encoded)
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf-8');
// Check if the issue author is mentioned in CODEOWNERS
// GitHub usernames in CODEOWNERS are prefixed with @
if (codeownersContent.includes(`@${issueAuthor}`)) {
console.log(`✅ ${issueAuthor} is a integration code owner`);
return; // Authorized
}
} catch (error) {
console.error('Error checking CODEOWNERS:', error);
}
// If we reach here, user is not authorized
console.log(`❌ ${issueAuthor} is not authorized to create Task issues`);
// Close the issue with a comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `Hi @${issueAuthor}, thank you for your contribution!\n\n` +
`Task issues are restricted to Open Home Foundation staff, authorized contributors, and integration code owners.\n\n` +
`If you would like to:\n` +
`- Report a bug: Please use the [bug report form](https://github.com/home-assistant/core/issues/new?template=bug_report.yml)\n` +
`- Request a feature: Please submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)\n\n` +
`If you believe you should have access to create Task issues, please contact the maintainers.`
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: 'closed'
});
// Add a label to indicate this was auto-closed
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['auto-closed']
});

6
.gitignore vendored
View File

@@ -137,4 +137,8 @@ tmp_cache
.ropeproject
# Will be created from script/split_tests.py
pytest_buckets.txt
pytest_buckets.txt
# AI tooling
.claude

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.0
rev: v0.12.1
hooks:
- id: ruff-check
args:

View File

@@ -381,6 +381,7 @@ homeassistant.components.openai_conversation.*
homeassistant.components.openexchangerates.*
homeassistant.components.opensky.*
homeassistant.components.openuv.*
homeassistant.components.opower.*
homeassistant.components.oralb.*
homeassistant.components.otbr.*
homeassistant.components.overkiz.*

1
CLAUDE.md Symbolic link
View File

@@ -0,0 +1 @@
.github/copilot-instructions.md

4
CODEOWNERS generated
View File

@@ -452,8 +452,8 @@ build.json @home-assistant/supervisor
/tests/components/eq3btsmart/ @eulemitkeule @dbuezas
/homeassistant/components/escea/ @lazdavila
/tests/components/escea/ @lazdavila
/homeassistant/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco
/tests/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco
/homeassistant/components/esphome/ @jesserockz @kbx81 @bdraco
/tests/components/esphome/ @jesserockz @kbx81 @bdraco
/homeassistant/components/eufylife_ble/ @bdr99
/tests/components/eufylife_ble/ @bdr99
/homeassistant/components/event/ @home-assistant/core

View File

@@ -1,15 +1,7 @@
FROM mcr.microsoft.com/devcontainers/python:1-3.13
FROM mcr.microsoft.com/vscode/devcontainers/base:debian
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# Uninstall pre-installed formatting and linting tools
# They would conflict with our pinned versions
RUN \
pipx uninstall pydocstyle \
&& pipx uninstall pycodestyle \
&& pipx uninstall mypy \
&& pipx uninstall pylint
RUN \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
&& apt-get update \
@@ -32,21 +24,18 @@ RUN \
libxml2 \
git \
cmake \
autoconf \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Add go2rtc binary
COPY --from=ghcr.io/alexxit/go2rtc:latest /usr/local/bin/go2rtc /bin/go2rtc
# Install uv
RUN pip3 install uv
WORKDIR /usr/src
# Setup hass-release
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
&& uv pip install --system -e hass-release/ \
&& chown -R vscode /usr/src/hass-release/data
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
RUN uv python install 3.13.2
USER vscode
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
@@ -55,6 +44,10 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
WORKDIR /tmp
# Setup hass-release
RUN git clone --depth 1 https://github.com/home-assistant/hass-release ~/hass-release \
&& uv pip install -e ~/hass-release/
# Install Python dependencies from requirements
COPY requirements.txt ./
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
@@ -65,4 +58,4 @@ RUN uv pip install -r requirements_test.txt
WORKDIR /workspaces
# Set the default shell to bash instead of sh
ENV SHELL /bin/bash
ENV SHELL=/bin/bash

View File

@@ -75,8 +75,8 @@ from .core_config import async_process_ha_core_config
from .exceptions import HomeAssistantError
from .helpers import (
area_registry,
backup,
category_registry,
condition,
config_validation as cv,
device_registry,
entity,
@@ -332,6 +332,9 @@ async def async_setup_hass(
if not is_virtual_env():
await async_mount_local_lib_path(runtime_config.config_dir)
if hass.config.safe_mode:
_LOGGER.info("Starting in safe mode")
basic_setup_success = (
await async_from_config_dict(config_dict, hass) is not None
)
@@ -384,8 +387,6 @@ async def async_setup_hass(
{"recovery_mode": {}, "http": http_conf},
hass,
)
elif hass.config.safe_mode:
_LOGGER.info("Starting in safe mode")
if runtime_config.open_ui:
hass.add_job(open_hass_ui, hass)
@@ -453,6 +454,7 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
create_eager_task(restore_state.async_load(hass)),
create_eager_task(hass.config_entries.async_initialize()),
create_eager_task(async_get_system_info(hass)),
create_eager_task(condition.async_setup(hass)),
create_eager_task(trigger.async_setup(hass)),
)
@@ -607,7 +609,7 @@ async def async_enable_logging(
)
threading.excepthook = lambda args: logging.getLogger().exception(
"Uncaught thread exception",
exc_info=( # type: ignore[arg-type] # noqa: LOG014
exc_info=( # type: ignore[arg-type]
args.exc_type,
args.exc_value,
args.exc_traceback,
@@ -869,9 +871,9 @@ async def _async_set_up_integrations(
domains = set(integrations) & all_domains
_LOGGER.info(
"Domains to be set up: %s | %s",
domains,
all_domains - domains,
"Domains to be set up: %s\nDependencies: %s",
domains or "{}",
(all_domains - domains) or "{}",
)
async_set_domains_to_be_loaded(hass, all_domains)
@@ -880,10 +882,6 @@ async def _async_set_up_integrations(
if "recorder" in all_domains:
recorder.async_initialize_recorder(hass)
# Initialize backup
if "backup" in all_domains:
backup.async_initialize_backup(hass)
stages: list[tuple[str, set[str], int | None]] = [
*(
(name, domain_group, timeout)
@@ -916,12 +914,13 @@ async def _async_set_up_integrations(
stage_all_domains = stage_domains | stage_dep_domains
_LOGGER.info(
"Setting up stage %s: %s | %s\nDependencies: %s | %s",
"Setting up stage %s: %s; already set up: %s\n"
"Dependencies: %s; already set up: %s",
name,
stage_domains,
stage_domains_unfiltered - stage_domains,
stage_dep_domains,
stage_dep_domains_unfiltered - stage_dep_domains,
(stage_domains_unfiltered - stage_domains) or "{}",
stage_dep_domains or "{}",
(stage_dep_domains_unfiltered - stage_dep_domains) or "{}",
)
if timeout is None:
@@ -1061,5 +1060,5 @@ async def _async_setup_multi_components(
_LOGGER.error(
"Error setting up integration %s - received exception",
domain,
exc_info=(type(result), result, result.__traceback__), # noqa: LOG014
exc_info=(type(result), result, result.__traceback__),
)

View File

@@ -1,11 +1,12 @@
"""Integration to offer AI tasks to Home Assistant."""
import logging
from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.const import ATTR_ENTITY_ID, CONF_DESCRIPTION, CONF_SELECTOR
from homeassistant.core import (
HassJobType,
HomeAssistant,
@@ -14,12 +15,15 @@ from homeassistant.core import (
SupportsResponse,
callback,
)
from homeassistant.helpers import config_validation as cv, storage
from homeassistant.helpers import config_validation as cv, selector, storage
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
from .const import (
ATTR_ATTACHMENTS,
ATTR_INSTRUCTIONS,
ATTR_REQUIRED,
ATTR_STRUCTURE,
ATTR_TASK_NAME,
DATA_COMPONENT,
DATA_PREFERENCES,
@@ -29,7 +33,7 @@ from .const import (
)
from .entity import AITaskEntity
from .http import async_setup as async_setup_http
from .task import GenDataTask, GenDataTaskResult, async_generate_data
from .task import GenDataTask, GenDataTaskResult, PlayMediaWithId, async_generate_data
__all__ = [
"DOMAIN",
@@ -37,6 +41,7 @@ __all__ = [
"AITaskEntityFeature",
"GenDataTask",
"GenDataTaskResult",
"PlayMediaWithId",
"async_generate_data",
"async_setup",
"async_setup_entry",
@@ -47,6 +52,27 @@ _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
STRUCTURE_FIELD_SCHEMA = vol.Schema(
{
vol.Optional(CONF_DESCRIPTION): str,
vol.Optional(ATTR_REQUIRED): bool,
vol.Required(CONF_SELECTOR): selector.validate_selector,
}
)
def _validate_structure_fields(value: dict[str, Any]) -> vol.Schema:
"""Validate the structure fields as a voluptuous Schema."""
if not isinstance(value, dict):
raise vol.Invalid("Structure must be a dictionary")
fields = {}
for k, v in value.items():
field_class = vol.Required if v.get(ATTR_REQUIRED, False) else vol.Optional
fields[field_class(k, description=v.get(CONF_DESCRIPTION))] = selector.selector(
v[CONF_SELECTOR]
)
return vol.Schema(fields, extra=vol.PREVENT_EXTRA)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Register the process service."""
@@ -64,6 +90,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
vol.Required(ATTR_TASK_NAME): cv.string,
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(ATTR_INSTRUCTIONS): cv.string,
vol.Optional(ATTR_STRUCTURE): vol.All(
vol.Schema({str: STRUCTURE_FIELD_SCHEMA}),
_validate_structure_fields,
),
vol.Optional(ATTR_ATTACHMENTS): vol.All(
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
),
}
),
supports_response=SupportsResponse.ONLY,

View File

@@ -21,6 +21,9 @@ SERVICE_GENERATE_DATA = "generate_data"
ATTR_INSTRUCTIONS: Final = "instructions"
ATTR_TASK_NAME: Final = "task_name"
ATTR_STRUCTURE: Final = "structure"
ATTR_REQUIRED: Final = "required"
ATTR_ATTACHMENTS: Final = "attachments"
DEFAULT_SYSTEM_PROMPT = (
"You are a Home Assistant expert and help users with their tasks."
@@ -32,3 +35,6 @@ class AITaskEntityFeature(IntFlag):
GENERATE_DATA = 1
"""Generate data based on instructions."""
SUPPORT_ATTACHMENTS = 2
"""Support attachments with generate data."""

View File

@@ -2,7 +2,7 @@
"domain": "ai_task",
"name": "AI Task",
"codeowners": ["@home-assistant/core"],
"dependencies": ["conversation"],
"dependencies": ["conversation", "media_source"],
"documentation": "https://www.home-assistant.io/integrations/ai_task",
"integration_type": "system",
"quality_scale": "internal"

View File

@@ -17,3 +17,15 @@ generate_data:
domain: ai_task
supported_features:
- ai_task.AITaskEntityFeature.GENERATE_DATA
structure:
advanced: true
required: false
example: '{ "name": { "selector": { "text": }, "description": "Name of the user", "required": "True" } } }, "age": { "selector": { "number": }, "description": "Age of the user" } }'
selector:
object:
attachments:
required: false
selector:
media:
accept:
- "*"

View File

@@ -15,6 +15,14 @@
"entity_id": {
"name": "Entity ID",
"description": "Entity ID to run the task on. If not provided, the preferred entity will be used."
},
"structure": {
"name": "Structured output",
"description": "When set, the AI Task will output fields with this in structure. The structure is a dictionary where the keys are the field names and the values contain a 'description', a 'selector', and an optional 'required' field."
},
"attachments": {
"name": "Attachments",
"description": "List of files to attach for multi-modal AI analysis."
}
}
}

View File

@@ -2,21 +2,38 @@
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import dataclass, fields
from typing import Any
import voluptuous as vol
from homeassistant.components import media_source
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .const import DATA_COMPONENT, DATA_PREFERENCES, AITaskEntityFeature
@dataclass(slots=True)
class PlayMediaWithId(media_source.PlayMedia):
"""Play media with a media content ID."""
media_content_id: str
"""Media source ID to play."""
def __str__(self) -> str:
"""Return media source ID as a string."""
return f"<PlayMediaWithId {self.media_content_id}>"
async def async_generate_data(
hass: HomeAssistant,
*,
task_name: str,
entity_id: str | None = None,
instructions: str,
structure: vol.Schema | None = None,
attachments: list[dict] | None = None,
) -> GenDataTaskResult:
"""Run a task in the AI Task integration."""
if entity_id is None:
@@ -34,10 +51,37 @@ async def async_generate_data(
f"AI Task entity {entity_id} does not support generating data"
)
# Resolve attachments
resolved_attachments: list[PlayMediaWithId] | None = None
if attachments:
if AITaskEntityFeature.SUPPORT_ATTACHMENTS not in entity.supported_features:
raise HomeAssistantError(
f"AI Task entity {entity_id} does not support attachments"
)
resolved_attachments = []
for attachment in attachments:
media = await media_source.async_resolve_media(
hass, attachment["media_content_id"], None
)
resolved_attachments.append(
PlayMediaWithId(
**{
field.name: getattr(media, field.name)
for field in fields(media)
},
media_content_id=attachment["media_content_id"],
)
)
return await entity.internal_async_generate_data(
GenDataTask(
name=task_name,
instructions=instructions,
structure=structure,
attachments=resolved_attachments,
)
)
@@ -52,6 +96,12 @@ class GenDataTask:
instructions: str
"""Instructions on what needs to be done."""
structure: vol.Schema | None = None
"""Optional structure for the data to be generated."""
attachments: list[PlayMediaWithId] | None = None
"""List of attachments to go along the instructions."""
def __str__(self) -> str:
"""Return task as a string."""
return f"<GenDataTask {self.name}: {id(self)}>"

View File

@@ -2,19 +2,50 @@
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from aioamazondevices.api import AmazonEchoApi
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect, WrongCountry
from aioamazondevices.exceptions import (
CannotAuthenticate,
CannotConnect,
CannotRetrieveData,
WrongCountry,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.selector import CountrySelector
from .const import CONF_LOGIN_DATA, DOMAIN
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_CODE): cv.string,
}
)
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
"""Validate the user input allows us to connect."""
api = AmazonEchoApi(
data[CONF_COUNTRY],
data[CONF_USERNAME],
data[CONF_PASSWORD],
)
try:
data = await api.login_mode_interactive(data[CONF_CODE])
finally:
await api.close()
return data
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Alexa Devices."""
@@ -25,17 +56,14 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the initial step."""
errors = {}
if user_input:
client = AmazonEchoApi(
user_input[CONF_COUNTRY],
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
)
try:
data = await client.login_mode_interactive(user_input[CONF_CODE])
data = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except CannotAuthenticate:
errors["base"] = "invalid_auth"
except CannotRetrieveData:
errors["base"] = "cannot_retrieve_data"
except WrongCountry:
errors["base"] = "wrong_country"
else:
@@ -46,8 +74,6 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
title=user_input[CONF_USERNAME],
data=user_input | {CONF_LOGIN_DATA: data},
)
finally:
await client.close()
return self.async_show_form(
step_id="user",
@@ -63,3 +89,45 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
}
),
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauth flow."""
self.context["title_placeholders"] = {CONF_USERNAME: entry_data[CONF_USERNAME]}
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reauth confirm."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
entry_data = reauth_entry.data
if user_input is not None:
try:
await validate_input(self.hass, {**reauth_entry.data, **user_input})
except CannotConnect:
errors["base"] = "cannot_connect"
except CannotAuthenticate:
errors["base"] = "invalid_auth"
except CannotRetrieveData:
errors["base"] = "cannot_retrieve_data"
else:
return self.async_update_reload_and_abort(
reauth_entry,
data={
CONF_USERNAME: entry_data[CONF_USERNAME],
CONF_PASSWORD: entry_data[CONF_PASSWORD],
CONF_CODE: user_input[CONF_CODE],
},
)
return self.async_show_form(
step_id="reauth_confirm",
description_placeholders={CONF_USERNAME: entry_data[CONF_USERNAME]},
data_schema=STEP_REAUTH_DATA_SCHEMA,
errors=errors,
)

View File

@@ -12,10 +12,10 @@ from aioamazondevices.exceptions import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import _LOGGER, CONF_LOGIN_DATA
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
SCAN_INTERVAL = 30
@@ -52,7 +52,21 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
try:
await self.api.login_mode_stored_data()
return await self.api.get_devices_data()
except (CannotConnect, CannotRetrieveData) as err:
raise UpdateFailed(f"Error occurred while updating {self.name}") from err
except CannotConnect as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="cannot_connect_with_error",
translation_placeholders={"error": repr(err)},
) from err
except CannotRetrieveData as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="cannot_retrieve_data_with_error",
translation_placeholders={"error": repr(err)},
) from err
except CannotAuthenticate as err:
raise ConfigEntryError("Could not authenticate") from err
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
translation_placeholders={"error": repr(err)},
) from err

View File

@@ -7,6 +7,6 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "bronze",
"requirements": ["aioamazondevices==3.2.1"]
"quality_scale": "silver",
"requirements": ["aioamazondevices==3.2.8"]
}

View File

@@ -28,33 +28,31 @@ rules:
# Silver
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
test-coverage:
status: todo
comment: all tests missing
reauthentication-flow: done
test-coverage: done
# Gold
devices: done
diagnostics: todo
diagnostics: done
discovery-update-info:
status: exempt
comment: Network information not relevant
discovery:
status: exempt
comment: There are a ton of mac address ranges in use, but also by kindles which are not supported by this integration
docs-data-update: todo
docs-examples: todo
docs-data-update: done
docs-examples: done
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: todo
docs-use-cases: todo
docs-use-cases: done
dynamic-devices: todo
entity-category: done
entity-device-class: done

View File

@@ -22,16 +22,28 @@
"password": "[%key:component::alexa_devices::common::data_description_password%]",
"code": "[%key:component::alexa_devices::common::data_description_code%]"
}
},
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"code": "[%key:component::alexa_devices::common::data_code%]"
},
"data_description": {
"password": "[%key:component::alexa_devices::common::data_description_password%]",
"code": "[%key:component::alexa_devices::common::data_description_code%]"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"cannot_retrieve_data": "Unable to retrieve data from Amazon. Please try again later.",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"wrong_country": "Wrong country selected. Please select the country where your Amazon account is registered.",
"unknown": "[%key:common::config_flow::error::unknown%]"
@@ -73,10 +85,10 @@
}
},
"exceptions": {
"cannot_connect": {
"cannot_connect_with_error": {
"message": "Error connecting: {error}"
},
"cannot_retrieve_data": {
"cannot_retrieve_data_with_error": {
"message": "Error retrieving data: {error}"
}
}

View File

@@ -26,14 +26,14 @@ def alexa_api_call[_T: AmazonEntity, **_P](
self.coordinator.last_update_success = False
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="cannot_connect",
translation_key="cannot_connect_with_error",
translation_placeholders={"error": repr(err)},
) from err
except CannotRetrieveData as err:
self.coordinator.last_update_success = False
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="cannot_retrieve_data",
translation_key="cannot_retrieve_data_with_error",
translation_placeholders={"error": repr(err)},
) from err

View File

@@ -56,7 +56,7 @@ SERVICE_UPLOAD = "upload"
ANDROIDTV_STATES = {
"off": MediaPlayerState.OFF,
"idle": MediaPlayerState.IDLE,
"standby": MediaPlayerState.STANDBY,
"standby": MediaPlayerState.IDLE,
"playing": MediaPlayerState.PLAYING,
"paused": MediaPlayerState.PAUSED,
}

View File

@@ -5,26 +5,18 @@ from __future__ import annotations
from asyncio import timeout
import logging
from androidtvremote2 import (
AndroidTVRemote,
CannotConnect,
ConnectionClosed,
InvalidAuth,
)
from androidtvremote2 import CannotConnect, ConnectionClosed, InvalidAuth
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .helpers import create_api, get_enable_ime
from .helpers import AndroidTVRemoteConfigEntry, create_api, get_enable_ime
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER, Platform.REMOTE]
AndroidTVRemoteConfigEntry = ConfigEntry[AndroidTVRemote]
async def async_setup_entry(
hass: HomeAssistant, entry: AndroidTVRemoteConfigEntry
@@ -82,13 +74,17 @@ async def async_setup_entry(
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: AndroidTVRemoteConfigEntry
) -> bool:
"""Unload a config entry."""
_LOGGER.debug("async_unload_entry: %s", entry.data)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def async_update_options(
hass: HomeAssistant, entry: AndroidTVRemoteConfigEntry
) -> None:
"""Handle options update."""
_LOGGER.debug(
"async_update_options: data: %s options: %s", entry.data, entry.options

View File

@@ -16,7 +16,7 @@ import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_REAUTH,
ConfigEntry,
SOURCE_RECONFIGURE,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
@@ -33,7 +33,7 @@ from homeassistant.helpers.selector import (
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import CONF_APP_ICON, CONF_APP_NAME, CONF_APPS, CONF_ENABLE_IME, DOMAIN
from .helpers import create_api, get_enable_ime
from .helpers import AndroidTVRemoteConfigEntry, create_api, get_enable_ime
_LOGGER = logging.getLogger(__name__)
@@ -41,12 +41,6 @@ APPS_NEW_ID = "NewApp"
CONF_APP_DELETE = "app_delete"
CONF_APP_ID = "app_id"
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required("host"): str,
}
)
STEP_PAIR_DATA_SCHEMA = vol.Schema(
{
vol.Required("pin"): str,
@@ -67,7 +61,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
"""Handle the initial and reconfigure step."""
errors: dict[str, str] = {}
if user_input is not None:
self.host = user_input[CONF_HOST]
@@ -76,15 +70,32 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
await api.async_generate_cert_if_missing()
self.name, self.mac = await api.async_get_name_and_mac()
await self.async_set_unique_id(format_mac(self.mac))
if self.source == SOURCE_RECONFIGURE:
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(),
data={
CONF_HOST: self.host,
CONF_NAME: self.name,
CONF_MAC: self.mac,
},
)
self._abort_if_unique_id_configured(updates={CONF_HOST: self.host})
return await self._async_start_pair()
except (CannotConnect, ConnectionClosed):
# Likely invalid IP address or device is network unreachable. Stay
# in the user step allowing the user to enter a different host.
errors["base"] = "cannot_connect"
else:
user_input = {}
default_host = user_input.get(CONF_HOST, vol.UNDEFINED)
if self.source == SOURCE_RECONFIGURE:
default_host = self._get_reconfigure_entry().data[CONF_HOST]
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
step_id="reconfigure" if self.source == SOURCE_RECONFIGURE else "user",
data_schema=vol.Schema(
{vol.Required(CONF_HOST, default=default_host): str}
),
errors=errors,
)
@@ -217,10 +228,16 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration."""
return await self.async_step_user(user_input)
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
config_entry: AndroidTVRemoteConfigEntry,
) -> AndroidTVRemoteOptionsFlowHandler:
"""Create the options flow."""
return AndroidTVRemoteOptionsFlowHandler(config_entry)
@@ -229,7 +246,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
class AndroidTVRemoteOptionsFlowHandler(OptionsFlow):
"""Android TV Remote options flow."""
def __init__(self, config_entry: ConfigEntry) -> None:
def __init__(self, config_entry: AndroidTVRemoteConfigEntry) -> None:
"""Initialize options flow."""
self._apps: dict[str, Any] = dict(config_entry.options.get(CONF_APPS, {}))
self._conf_app_id: str | None = None

View File

@@ -8,7 +8,7 @@ from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_HOST, CONF_MAC
from homeassistant.core import HomeAssistant
from . import AndroidTVRemoteConfigEntry
from .helpers import AndroidTVRemoteConfigEntry
TO_REDACT = {CONF_HOST, CONF_MAC}

View File

@@ -6,7 +6,6 @@ from typing import Any
from androidtvremote2 import AndroidTVRemote, ConnectionClosed
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
@@ -14,6 +13,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, Device
from homeassistant.helpers.entity import Entity
from .const import CONF_APPS, DOMAIN
from .helpers import AndroidTVRemoteConfigEntry
class AndroidTVRemoteBaseEntity(Entity):
@@ -23,7 +23,9 @@ class AndroidTVRemoteBaseEntity(Entity):
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(self, api: AndroidTVRemote, config_entry: ConfigEntry) -> None:
def __init__(
self, api: AndroidTVRemote, config_entry: AndroidTVRemoteConfigEntry
) -> None:
"""Initialize the entity."""
self._api = api
self._host = config_entry.data[CONF_HOST]

View File

@@ -10,6 +10,8 @@ from homeassistant.helpers.storage import STORAGE_DIR
from .const import CONF_ENABLE_IME, CONF_ENABLE_IME_DEFAULT_VALUE
AndroidTVRemoteConfigEntry = ConfigEntry[AndroidTVRemote]
def create_api(hass: HomeAssistant, host: str, enable_ime: bool) -> AndroidTVRemote:
"""Create an AndroidTVRemote instance."""
@@ -23,6 +25,6 @@ def create_api(hass: HomeAssistant, host: str, enable_ime: bool) -> AndroidTVRem
)
def get_enable_ime(entry: ConfigEntry) -> bool:
def get_enable_ime(entry: AndroidTVRemoteConfigEntry) -> bool:
"""Get value of enable_ime option or its default value."""
return entry.options.get(CONF_ENABLE_IME, CONF_ENABLE_IME_DEFAULT_VALUE)

View File

@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["androidtvremote2"],
"requirements": ["androidtvremote2==0.2.2"],
"requirements": ["androidtvremote2==0.2.3"],
"zeroconf": ["_androidtvremote2._tcp.local."]
}

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
import asyncio
from typing import Any
from androidtvremote2 import AndroidTVRemote, ConnectionClosed
from androidtvremote2 import AndroidTVRemote, ConnectionClosed, VolumeInfo
from homeassistant.components.media_player import (
BrowseMedia,
@@ -20,9 +20,9 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AndroidTVRemoteConfigEntry
from .const import CONF_APP_ICON, CONF_APP_NAME, DOMAIN
from .entity import AndroidTVRemoteBaseEntity
from .helpers import AndroidTVRemoteConfigEntry
PARALLEL_UPDATES = 0
@@ -75,13 +75,11 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
else current_app
)
def _update_volume_info(self, volume_info: dict[str, str | bool]) -> None:
def _update_volume_info(self, volume_info: VolumeInfo) -> None:
"""Update volume info."""
if volume_info.get("max"):
self._attr_volume_level = int(volume_info["level"]) / int(
volume_info["max"]
)
self._attr_is_volume_muted = bool(volume_info["muted"])
self._attr_volume_level = volume_info["level"] / volume_info["max"]
self._attr_is_volume_muted = volume_info["muted"]
else:
self._attr_volume_level = None
self._attr_is_volume_muted = None
@@ -93,7 +91,7 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
self.async_write_ha_state()
@callback
def _volume_info_updated(self, volume_info: dict[str, str | bool]) -> None:
def _volume_info_updated(self, volume_info: VolumeInfo) -> None:
"""Update the state when the volume info changes."""
self._update_volume_info(volume_info)
self.async_write_ha_state()
@@ -102,8 +100,10 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
"""Register callbacks."""
await super().async_added_to_hass()
self._update_current_app(self._api.current_app)
self._update_volume_info(self._api.volume_info)
if self._api.current_app is not None:
self._update_current_app(self._api.current_app)
if self._api.volume_info is not None:
self._update_volume_info(self._api.volume_info)
self._api.add_current_app_updated_callback(self._current_app_updated)
self._api.add_volume_info_updated_callback(self._volume_info_updated)

View File

@@ -20,9 +20,9 @@ from homeassistant.components.remote import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AndroidTVRemoteConfigEntry
from .const import CONF_APP_NAME
from .entity import AndroidTVRemoteBaseEntity
from .helpers import AndroidTVRemoteConfigEntry
PARALLEL_UPDATES = 0
@@ -63,7 +63,8 @@ class AndroidTVRemoteEntity(AndroidTVRemoteBaseEntity, RemoteEntity):
self._attr_activity_list = [
app.get(CONF_APP_NAME, "") for app in self._apps.values()
]
self._update_current_app(self._api.current_app)
if self._api.current_app is not None:
self._update_current_app(self._api.current_app)
self._api.add_current_app_updated_callback(self._current_app_updated)
async def async_will_remove_from_hass(self) -> None:

View File

@@ -6,6 +6,18 @@
"description": "Enter the IP address of the Android TV you want to add to Home Assistant. It will turn on and a pairing code will be displayed on it that you will need to enter in the next screen.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The hostname or IP address of the Android TV device."
}
},
"reconfigure": {
"description": "Update the IP address of this previously configured Android TV device.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The hostname or IP address of the Android TV device."
}
},
"zeroconf_confirm": {
@@ -16,6 +28,9 @@
"description": "Enter the pairing code displayed on the Android TV ({name}).",
"data": {
"pin": "[%key:common::config_flow::data::pin%]"
},
"data_description": {
"pin": "Pairing code displayed on the Android TV device."
}
},
"reauth_confirm": {
@@ -32,7 +47,9 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"unique_id_mismatch": "Please ensure you reconfigure against the same device."
}
},
"options": {
@@ -40,7 +57,11 @@
"init": {
"data": {
"apps": "Configure applications list",
"enable_ime": "Enable IME. Needed for getting the current app. Disable for devices that show 'Use keyboard on mobile device screen' instead of the on screen keyboard."
"enable_ime": "Enable IME"
},
"data_description": {
"apps": "Here you can define the list of applications, specify names and icons that will be displayed in the UI.",
"enable_ime": "Enable this option to be able to get the current app name and send text as keyboard input. Disable it for devices that show 'Use keyboard on mobile device screen' instead of the on-screen keyboard."
}
},
"apps": {
@@ -53,8 +74,10 @@
"app_delete": "Check to delete this application"
},
"data_description": {
"app_name": "Name of the application as you would like it to be displayed in Home Assistant.",
"app_id": "E.g. com.plexapp.android for https://play.google.com/store/apps/details?id=com.plexapp.android",
"app_icon": "Image URL. From the Play Store app page, right click on the icon and select 'Copy image address' and then paste it here. Alternatively, download the image, upload it under /config/www/ and use the URL /local/filename"
"app_icon": "Image URL. From the Play Store app page, right click on the icon and select 'Copy image address' and then paste it here. Alternatively, download the image, upload it under /config/www/ and use the URL /local/filename",
"app_delete": "Check this box to delete the application from the list."
}
}
}

View File

@@ -61,6 +61,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_update_options))
return True
@@ -69,6 +71,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_update_options(
hass: HomeAssistant, entry: AnthropicConfigEntry
) -> None:
"""Update options."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_migrate_integration(hass: HomeAssistant) -> None:
"""Migrate integration entry structure."""

View File

@@ -1,69 +1,17 @@
"""Conversation support for Anthropic."""
from collections.abc import AsyncGenerator, Callable, Iterable
import json
from typing import Any, Literal, cast
import anthropic
from anthropic import AsyncStream
from anthropic._types import NOT_GIVEN
from anthropic.types import (
InputJSONDelta,
MessageDeltaUsage,
MessageParam,
MessageStreamEvent,
RawContentBlockDeltaEvent,
RawContentBlockStartEvent,
RawContentBlockStopEvent,
RawMessageDeltaEvent,
RawMessageStartEvent,
RawMessageStopEvent,
RedactedThinkingBlock,
RedactedThinkingBlockParam,
SignatureDelta,
TextBlock,
TextBlockParam,
TextDelta,
ThinkingBlock,
ThinkingBlockParam,
ThinkingConfigDisabledParam,
ThinkingConfigEnabledParam,
ThinkingDelta,
ToolParam,
ToolResultBlockParam,
ToolUseBlock,
ToolUseBlockParam,
Usage,
)
from voluptuous_openapi import convert
from typing import Literal
from homeassistant.components import conversation
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
from homeassistant.config_entries import ConfigSubentry
from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, intent, llm
from homeassistant.helpers import intent
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AnthropicConfigEntry
from .const import (
CONF_CHAT_MODEL,
CONF_MAX_TOKENS,
CONF_PROMPT,
CONF_TEMPERATURE,
CONF_THINKING_BUDGET,
DOMAIN,
LOGGER,
MIN_THINKING_BUDGET,
RECOMMENDED_CHAT_MODEL,
RECOMMENDED_MAX_TOKENS,
RECOMMENDED_TEMPERATURE,
RECOMMENDED_THINKING_BUDGET,
THINKING_MODELS,
)
# Max number of back and forth with the LLM to generate a response
MAX_TOOL_ITERATIONS = 10
from .const import CONF_PROMPT, DOMAIN
from .entity import AnthropicBaseLLMEntity
async def async_setup_entry(
@@ -82,253 +30,10 @@ async def async_setup_entry(
)
def _format_tool(
tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None
) -> ToolParam:
"""Format tool specification."""
return ToolParam(
name=tool.name,
description=tool.description or "",
input_schema=convert(tool.parameters, custom_serializer=custom_serializer),
)
def _convert_content(
chat_content: Iterable[conversation.Content],
) -> list[MessageParam]:
"""Transform HA chat_log content into Anthropic API format."""
messages: list[MessageParam] = []
for content in chat_content:
if isinstance(content, conversation.ToolResultContent):
tool_result_block = ToolResultBlockParam(
type="tool_result",
tool_use_id=content.tool_call_id,
content=json.dumps(content.tool_result),
)
if not messages or messages[-1]["role"] != "user":
messages.append(
MessageParam(
role="user",
content=[tool_result_block],
)
)
elif isinstance(messages[-1]["content"], str):
messages[-1]["content"] = [
TextBlockParam(type="text", text=messages[-1]["content"]),
tool_result_block,
]
else:
messages[-1]["content"].append(tool_result_block) # type: ignore[attr-defined]
elif isinstance(content, conversation.UserContent):
# Combine consequent user messages
if not messages or messages[-1]["role"] != "user":
messages.append(
MessageParam(
role="user",
content=content.content,
)
)
elif isinstance(messages[-1]["content"], str):
messages[-1]["content"] = [
TextBlockParam(type="text", text=messages[-1]["content"]),
TextBlockParam(type="text", text=content.content),
]
else:
messages[-1]["content"].append( # type: ignore[attr-defined]
TextBlockParam(type="text", text=content.content)
)
elif isinstance(content, conversation.AssistantContent):
# Combine consequent assistant messages
if not messages or messages[-1]["role"] != "assistant":
messages.append(
MessageParam(
role="assistant",
content=[],
)
)
if content.content:
messages[-1]["content"].append( # type: ignore[union-attr]
TextBlockParam(type="text", text=content.content)
)
if content.tool_calls:
messages[-1]["content"].extend( # type: ignore[union-attr]
[
ToolUseBlockParam(
type="tool_use",
id=tool_call.id,
name=tool_call.tool_name,
input=tool_call.tool_args,
)
for tool_call in content.tool_calls
]
)
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)}")
return messages
async def _transform_stream( # noqa: C901 - This is complex, but better to have it in one place
chat_log: conversation.ChatLog,
result: AsyncStream[MessageStreamEvent],
messages: list[MessageParam],
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
"""Transform the response stream into HA format.
A typical stream of responses might look something like the following:
- RawMessageStartEvent with no content
- RawContentBlockStartEvent with an empty ThinkingBlock (if extended thinking is enabled)
- RawContentBlockDeltaEvent with a ThinkingDelta
- RawContentBlockDeltaEvent with a ThinkingDelta
- RawContentBlockDeltaEvent with a ThinkingDelta
- ...
- RawContentBlockDeltaEvent with a SignatureDelta
- RawContentBlockStopEvent
- RawContentBlockStartEvent with a RedactedThinkingBlock (occasionally)
- RawContentBlockStopEvent (RedactedThinkingBlock does not have a delta)
- RawContentBlockStartEvent with an empty TextBlock
- RawContentBlockDeltaEvent with a TextDelta
- RawContentBlockDeltaEvent with a TextDelta
- RawContentBlockDeltaEvent with a TextDelta
- ...
- RawContentBlockStopEvent
- RawContentBlockStartEvent with ToolUseBlock specifying the function name
- RawContentBlockDeltaEvent with a InputJSONDelta
- RawContentBlockDeltaEvent with a InputJSONDelta
- ...
- RawContentBlockStopEvent
- RawMessageDeltaEvent with a stop_reason='tool_use'
- RawMessageStopEvent(type='message_stop')
Each message could contain multiple blocks of the same type.
"""
if result is None:
raise TypeError("Expected a stream of messages")
current_message: MessageParam | None = None
current_block: (
TextBlockParam
| ToolUseBlockParam
| ThinkingBlockParam
| RedactedThinkingBlockParam
| None
) = None
current_tool_args: str
input_usage: Usage | None = None
async for response in result:
LOGGER.debug("Received response: %s", response)
if isinstance(response, RawMessageStartEvent):
if response.message.role != "assistant":
raise ValueError("Unexpected message role")
current_message = MessageParam(role=response.message.role, content=[])
input_usage = response.message.usage
elif isinstance(response, RawContentBlockStartEvent):
if isinstance(response.content_block, ToolUseBlock):
current_block = ToolUseBlockParam(
type="tool_use",
id=response.content_block.id,
name=response.content_block.name,
input="",
)
current_tool_args = ""
elif isinstance(response.content_block, TextBlock):
current_block = TextBlockParam(
type="text", text=response.content_block.text
)
yield {"role": "assistant"}
if response.content_block.text:
yield {"content": response.content_block.text}
elif isinstance(response.content_block, ThinkingBlock):
current_block = ThinkingBlockParam(
type="thinking",
thinking=response.content_block.thinking,
signature=response.content_block.signature,
)
elif isinstance(response.content_block, RedactedThinkingBlock):
current_block = RedactedThinkingBlockParam(
type="redacted_thinking", data=response.content_block.data
)
LOGGER.debug(
"Some of Claudes internal reasoning has been automatically "
"encrypted for safety reasons. This doesnt affect the quality of "
"responses"
)
elif isinstance(response, RawContentBlockDeltaEvent):
if current_block is None:
raise ValueError("Unexpected delta without a block")
if isinstance(response.delta, InputJSONDelta):
current_tool_args += response.delta.partial_json
elif isinstance(response.delta, TextDelta):
text_block = cast(TextBlockParam, current_block)
text_block["text"] += response.delta.text
yield {"content": response.delta.text}
elif isinstance(response.delta, ThinkingDelta):
thinking_block = cast(ThinkingBlockParam, current_block)
thinking_block["thinking"] += response.delta.thinking
elif isinstance(response.delta, SignatureDelta):
thinking_block = cast(ThinkingBlockParam, current_block)
thinking_block["signature"] += response.delta.signature
elif isinstance(response, RawContentBlockStopEvent):
if current_block is None:
raise ValueError("Unexpected stop event without a current block")
if current_block["type"] == "tool_use":
# tool block
tool_args = json.loads(current_tool_args) if current_tool_args else {}
current_block["input"] = tool_args
yield {
"tool_calls": [
llm.ToolInput(
id=current_block["id"],
tool_name=current_block["name"],
tool_args=tool_args,
)
]
}
elif current_block["type"] == "thinking":
# thinking block
LOGGER.debug("Thinking: %s", current_block["thinking"])
if current_message is None:
raise ValueError("Unexpected stop event without a current message")
current_message["content"].append(current_block) # type: ignore[union-attr]
current_block = None
elif isinstance(response, RawMessageDeltaEvent):
if (usage := response.usage) is not None:
chat_log.async_trace(_create_token_stats(input_usage, usage))
if response.delta.stop_reason == "refusal":
raise HomeAssistantError("Potential policy violation detected")
elif isinstance(response, RawMessageStopEvent):
if current_message is not None:
messages.append(current_message)
current_message = None
def _create_token_stats(
input_usage: Usage | None, response_usage: MessageDeltaUsage
) -> dict[str, Any]:
"""Create token stats for conversation agent tracing."""
input_tokens = 0
cached_input_tokens = 0
if input_usage:
input_tokens = input_usage.input_tokens
cached_input_tokens = input_usage.cache_creation_input_tokens or 0
output_tokens = response_usage.output_tokens
return {
"stats": {
"input_tokens": input_tokens,
"cached_input_tokens": cached_input_tokens,
"output_tokens": output_tokens,
}
}
class AnthropicConversationEntity(
conversation.ConversationEntity, conversation.AbstractConversationAgent
conversation.ConversationEntity,
conversation.AbstractConversationAgent,
AnthropicBaseLLMEntity,
):
"""Anthropic conversation agent."""
@@ -336,17 +41,7 @@ class AnthropicConversationEntity(
def __init__(self, entry: AnthropicConfigEntry, subentry: ConfigSubentry) -> None:
"""Initialize the agent."""
self.entry = entry
self.subentry = subentry
self._attr_name = subentry.title
self._attr_unique_id = subentry.subentry_id
self._attr_device_info = dr.DeviceInfo(
identifiers={(DOMAIN, subentry.subentry_id)},
name=subentry.title,
manufacturer="Anthropic",
model="Claude",
entry_type=dr.DeviceEntryType.SERVICE,
)
super().__init__(entry, subentry)
if self.subentry.data.get(CONF_LLM_HASS_API):
self._attr_supported_features = (
conversation.ConversationEntityFeature.CONTROL
@@ -357,13 +52,6 @@ class AnthropicConversationEntity(
"""Return a list of supported languages."""
return MATCH_ALL
async def async_added_to_hass(self) -> None:
"""When entity is added to Home Assistant."""
await super().async_added_to_hass()
self.entry.async_on_unload(
self.entry.add_update_listener(self._async_entry_update_listener)
)
async def _async_handle_message(
self,
user_input: conversation.ConversationInput,
@@ -394,77 +82,3 @@ class AnthropicConversationEntity(
conversation_id=chat_log.conversation_id,
continue_conversation=chat_log.continue_conversation,
)
async def _async_handle_chat_log(
self,
chat_log: conversation.ChatLog,
) -> None:
"""Generate an answer for the chat log."""
options = self.subentry.data
tools: list[ToolParam] | None = None
if chat_log.llm_api:
tools = [
_format_tool(tool, chat_log.llm_api.custom_serializer)
for tool in chat_log.llm_api.tools
]
system = chat_log.content[0]
if not isinstance(system, conversation.SystemContent):
raise TypeError("First message must be a system message")
messages = _convert_content(chat_log.content[1:])
client = self.entry.runtime_data
thinking_budget = options.get(CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET)
model = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
# To prevent infinite loops, we limit the number of iterations
for _iteration in range(MAX_TOOL_ITERATIONS):
model_args = {
"model": model,
"messages": messages,
"tools": tools or NOT_GIVEN,
"max_tokens": options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
"system": system.content,
"stream": True,
}
if model in THINKING_MODELS and thinking_budget >= MIN_THINKING_BUDGET:
model_args["thinking"] = ThinkingConfigEnabledParam(
type="enabled", budget_tokens=thinking_budget
)
else:
model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled")
model_args["temperature"] = options.get(
CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE
)
try:
stream = await client.messages.create(**model_args)
except anthropic.AnthropicError as err:
raise HomeAssistantError(
f"Sorry, I had a problem talking to Anthropic: {err}"
) from err
messages.extend(
_convert_content(
[
content
async for content in chat_log.async_add_delta_content_stream(
self.entity_id,
_transform_stream(chat_log, stream, messages),
)
if not isinstance(content, conversation.AssistantContent)
]
)
)
if not chat_log.unresponded_tool_results:
break
async def _async_entry_update_listener(
self, hass: HomeAssistant, entry: ConfigEntry
) -> None:
"""Handle options update."""
# Reload as we update device info + entity name + supported features
await hass.config_entries.async_reload(entry.entry_id)

View File

@@ -0,0 +1,393 @@
"""Base entity for Anthropic."""
from collections.abc import AsyncGenerator, Callable, Iterable
import json
from typing import Any, cast
import anthropic
from anthropic import AsyncStream
from anthropic._types import NOT_GIVEN
from anthropic.types import (
InputJSONDelta,
MessageDeltaUsage,
MessageParam,
MessageStreamEvent,
RawContentBlockDeltaEvent,
RawContentBlockStartEvent,
RawContentBlockStopEvent,
RawMessageDeltaEvent,
RawMessageStartEvent,
RawMessageStopEvent,
RedactedThinkingBlock,
RedactedThinkingBlockParam,
SignatureDelta,
TextBlock,
TextBlockParam,
TextDelta,
ThinkingBlock,
ThinkingBlockParam,
ThinkingConfigDisabledParam,
ThinkingConfigEnabledParam,
ThinkingDelta,
ToolParam,
ToolResultBlockParam,
ToolUseBlock,
ToolUseBlockParam,
Usage,
)
from voluptuous_openapi import convert
from homeassistant.components import conversation
from homeassistant.config_entries import ConfigSubentry
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, llm
from homeassistant.helpers.entity import Entity
from . import AnthropicConfigEntry
from .const import (
CONF_CHAT_MODEL,
CONF_MAX_TOKENS,
CONF_TEMPERATURE,
CONF_THINKING_BUDGET,
DOMAIN,
LOGGER,
MIN_THINKING_BUDGET,
RECOMMENDED_CHAT_MODEL,
RECOMMENDED_MAX_TOKENS,
RECOMMENDED_TEMPERATURE,
RECOMMENDED_THINKING_BUDGET,
THINKING_MODELS,
)
# Max number of back and forth with the LLM to generate a response
MAX_TOOL_ITERATIONS = 10
def _format_tool(
tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None
) -> ToolParam:
"""Format tool specification."""
return ToolParam(
name=tool.name,
description=tool.description or "",
input_schema=convert(tool.parameters, custom_serializer=custom_serializer),
)
def _convert_content(
chat_content: Iterable[conversation.Content],
) -> list[MessageParam]:
"""Transform HA chat_log content into Anthropic API format."""
messages: list[MessageParam] = []
for content in chat_content:
if isinstance(content, conversation.ToolResultContent):
tool_result_block = ToolResultBlockParam(
type="tool_result",
tool_use_id=content.tool_call_id,
content=json.dumps(content.tool_result),
)
if not messages or messages[-1]["role"] != "user":
messages.append(
MessageParam(
role="user",
content=[tool_result_block],
)
)
elif isinstance(messages[-1]["content"], str):
messages[-1]["content"] = [
TextBlockParam(type="text", text=messages[-1]["content"]),
tool_result_block,
]
else:
messages[-1]["content"].append(tool_result_block) # type: ignore[attr-defined]
elif isinstance(content, conversation.UserContent):
# Combine consequent user messages
if not messages or messages[-1]["role"] != "user":
messages.append(
MessageParam(
role="user",
content=content.content,
)
)
elif isinstance(messages[-1]["content"], str):
messages[-1]["content"] = [
TextBlockParam(type="text", text=messages[-1]["content"]),
TextBlockParam(type="text", text=content.content),
]
else:
messages[-1]["content"].append( # type: ignore[attr-defined]
TextBlockParam(type="text", text=content.content)
)
elif isinstance(content, conversation.AssistantContent):
# Combine consequent assistant messages
if not messages or messages[-1]["role"] != "assistant":
messages.append(
MessageParam(
role="assistant",
content=[],
)
)
if content.content:
messages[-1]["content"].append( # type: ignore[union-attr]
TextBlockParam(type="text", text=content.content)
)
if content.tool_calls:
messages[-1]["content"].extend( # type: ignore[union-attr]
[
ToolUseBlockParam(
type="tool_use",
id=tool_call.id,
name=tool_call.tool_name,
input=tool_call.tool_args,
)
for tool_call in content.tool_calls
]
)
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)}")
return messages
async def _transform_stream( # noqa: C901 - This is complex, but better to have it in one place
chat_log: conversation.ChatLog,
result: AsyncStream[MessageStreamEvent],
messages: list[MessageParam],
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
"""Transform the response stream into HA format.
A typical stream of responses might look something like the following:
- RawMessageStartEvent with no content
- RawContentBlockStartEvent with an empty ThinkingBlock (if extended thinking is enabled)
- RawContentBlockDeltaEvent with a ThinkingDelta
- RawContentBlockDeltaEvent with a ThinkingDelta
- RawContentBlockDeltaEvent with a ThinkingDelta
- ...
- RawContentBlockDeltaEvent with a SignatureDelta
- RawContentBlockStopEvent
- RawContentBlockStartEvent with a RedactedThinkingBlock (occasionally)
- RawContentBlockStopEvent (RedactedThinkingBlock does not have a delta)
- RawContentBlockStartEvent with an empty TextBlock
- RawContentBlockDeltaEvent with a TextDelta
- RawContentBlockDeltaEvent with a TextDelta
- RawContentBlockDeltaEvent with a TextDelta
- ...
- RawContentBlockStopEvent
- RawContentBlockStartEvent with ToolUseBlock specifying the function name
- RawContentBlockDeltaEvent with a InputJSONDelta
- RawContentBlockDeltaEvent with a InputJSONDelta
- ...
- RawContentBlockStopEvent
- RawMessageDeltaEvent with a stop_reason='tool_use'
- RawMessageStopEvent(type='message_stop')
Each message could contain multiple blocks of the same type.
"""
if result is None:
raise TypeError("Expected a stream of messages")
current_message: MessageParam | None = None
current_block: (
TextBlockParam
| ToolUseBlockParam
| ThinkingBlockParam
| RedactedThinkingBlockParam
| None
) = None
current_tool_args: str
input_usage: Usage | None = None
async for response in result:
LOGGER.debug("Received response: %s", response)
if isinstance(response, RawMessageStartEvent):
if response.message.role != "assistant":
raise ValueError("Unexpected message role")
current_message = MessageParam(role=response.message.role, content=[])
input_usage = response.message.usage
elif isinstance(response, RawContentBlockStartEvent):
if isinstance(response.content_block, ToolUseBlock):
current_block = ToolUseBlockParam(
type="tool_use",
id=response.content_block.id,
name=response.content_block.name,
input="",
)
current_tool_args = ""
elif isinstance(response.content_block, TextBlock):
current_block = TextBlockParam(
type="text", text=response.content_block.text
)
yield {"role": "assistant"}
if response.content_block.text:
yield {"content": response.content_block.text}
elif isinstance(response.content_block, ThinkingBlock):
current_block = ThinkingBlockParam(
type="thinking",
thinking=response.content_block.thinking,
signature=response.content_block.signature,
)
elif isinstance(response.content_block, RedactedThinkingBlock):
current_block = RedactedThinkingBlockParam(
type="redacted_thinking", data=response.content_block.data
)
LOGGER.debug(
"Some of Claudes internal reasoning has been automatically "
"encrypted for safety reasons. This doesnt affect the quality of "
"responses"
)
elif isinstance(response, RawContentBlockDeltaEvent):
if current_block is None:
raise ValueError("Unexpected delta without a block")
if isinstance(response.delta, InputJSONDelta):
current_tool_args += response.delta.partial_json
elif isinstance(response.delta, TextDelta):
text_block = cast(TextBlockParam, current_block)
text_block["text"] += response.delta.text
yield {"content": response.delta.text}
elif isinstance(response.delta, ThinkingDelta):
thinking_block = cast(ThinkingBlockParam, current_block)
thinking_block["thinking"] += response.delta.thinking
elif isinstance(response.delta, SignatureDelta):
thinking_block = cast(ThinkingBlockParam, current_block)
thinking_block["signature"] += response.delta.signature
elif isinstance(response, RawContentBlockStopEvent):
if current_block is None:
raise ValueError("Unexpected stop event without a current block")
if current_block["type"] == "tool_use":
# tool block
tool_args = json.loads(current_tool_args) if current_tool_args else {}
current_block["input"] = tool_args
yield {
"tool_calls": [
llm.ToolInput(
id=current_block["id"],
tool_name=current_block["name"],
tool_args=tool_args,
)
]
}
elif current_block["type"] == "thinking":
# thinking block
LOGGER.debug("Thinking: %s", current_block["thinking"])
if current_message is None:
raise ValueError("Unexpected stop event without a current message")
current_message["content"].append(current_block) # type: ignore[union-attr]
current_block = None
elif isinstance(response, RawMessageDeltaEvent):
if (usage := response.usage) is not None:
chat_log.async_trace(_create_token_stats(input_usage, usage))
if response.delta.stop_reason == "refusal":
raise HomeAssistantError("Potential policy violation detected")
elif isinstance(response, RawMessageStopEvent):
if current_message is not None:
messages.append(current_message)
current_message = None
def _create_token_stats(
input_usage: Usage | None, response_usage: MessageDeltaUsage
) -> dict[str, Any]:
"""Create token stats for conversation agent tracing."""
input_tokens = 0
cached_input_tokens = 0
if input_usage:
input_tokens = input_usage.input_tokens
cached_input_tokens = input_usage.cache_creation_input_tokens or 0
output_tokens = response_usage.output_tokens
return {
"stats": {
"input_tokens": input_tokens,
"cached_input_tokens": cached_input_tokens,
"output_tokens": output_tokens,
}
}
class AnthropicBaseLLMEntity(Entity):
"""Anthropic base LLM entity."""
def __init__(self, entry: AnthropicConfigEntry, subentry: ConfigSubentry) -> None:
"""Initialize the entity."""
self.entry = entry
self.subentry = subentry
self._attr_name = subentry.title
self._attr_unique_id = subentry.subentry_id
self._attr_device_info = dr.DeviceInfo(
identifiers={(DOMAIN, subentry.subentry_id)},
name=subentry.title,
manufacturer="Anthropic",
model="Claude",
entry_type=dr.DeviceEntryType.SERVICE,
)
async def _async_handle_chat_log(
self,
chat_log: conversation.ChatLog,
) -> None:
"""Generate an answer for the chat log."""
options = self.subentry.data
tools: list[ToolParam] | None = None
if chat_log.llm_api:
tools = [
_format_tool(tool, chat_log.llm_api.custom_serializer)
for tool in chat_log.llm_api.tools
]
system = chat_log.content[0]
if not isinstance(system, conversation.SystemContent):
raise TypeError("First message must be a system message")
messages = _convert_content(chat_log.content[1:])
client = self.entry.runtime_data
thinking_budget = options.get(CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET)
model = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
# To prevent infinite loops, we limit the number of iterations
for _iteration in range(MAX_TOOL_ITERATIONS):
model_args = {
"model": model,
"messages": messages,
"tools": tools or NOT_GIVEN,
"max_tokens": options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
"system": system.content,
"stream": True,
}
if model in THINKING_MODELS and thinking_budget >= MIN_THINKING_BUDGET:
model_args["thinking"] = ThinkingConfigEnabledParam(
type="enabled", budget_tokens=thinking_budget
)
else:
model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled")
model_args["temperature"] = options.get(
CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE
)
try:
stream = await client.messages.create(**model_args)
except anthropic.AnthropicError as err:
raise HomeAssistantError(
f"Sorry, I had a problem talking to Anthropic: {err}"
) from err
messages.extend(
_convert_content(
[
content
async for content in chat_log.async_add_delta_content_stream(
self.entity_id,
_transform_stream(chat_log, stream, messages),
)
if not isinstance(content, conversation.AssistantContent)
]
)
)
if not chat_log.unresponded_tool_results:
break

View File

@@ -191,7 +191,7 @@ class AppleTvMediaPlayer(
self._is_feature_available(FeatureName.PowerState)
and self.atv.power.power_state == PowerState.Off
):
return MediaPlayerState.STANDBY
return MediaPlayerState.OFF
if self._playing:
state = self._playing.device_state
if state in (DeviceState.Idle, DeviceState.Loading):
@@ -200,7 +200,7 @@ class AppleTvMediaPlayer(
return MediaPlayerState.PLAYING
if state in (DeviceState.Paused, DeviceState.Seeking, DeviceState.Stopped):
return MediaPlayerState.PAUSED
return MediaPlayerState.STANDBY # Bad or unknown state?
return MediaPlayerState.IDLE # Bad or unknown state?
return None
@callback

View File

@@ -72,7 +72,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
{
vol.Optional("message"): str,
vol.Optional("media_id"): _media_id_validator,
vol.Optional("preannounce"): bool,
vol.Optional("preannounce", default=True): bool,
vol.Optional("preannounce_media_id"): _media_id_validator,
}
),
@@ -89,7 +89,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
{
vol.Optional("start_message"): str,
vol.Optional("start_media_id"): _media_id_validator,
vol.Optional("preannounce"): bool,
vol.Optional("preannounce", default=True): bool,
vol.Optional("preannounce_media_id"): _media_id_validator,
vol.Optional("extra_system_prompt"): str,
}
@@ -114,7 +114,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
ask_question_args = {
"question": call.data.get("question"),
"question_media_id": call.data.get("question_media_id"),
"preannounce": call.data.get("preannounce", False),
"preannounce": call.data.get("preannounce", True),
"answers": call.data.get("answers"),
}
@@ -137,7 +137,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
vol.Required(ATTR_ENTITY_ID): cv.entity_domain(DOMAIN),
vol.Optional("question"): str,
vol.Optional("question_media_id"): _media_id_validator,
vol.Optional("preannounce"): bool,
vol.Optional("preannounce", default=True): bool,
vol.Optional("preannounce_media_id"): _media_id_validator,
vol.Optional("answers"): [
{

View File

@@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==8.10.0", "yalexs-ble==2.6.0"]
"requirements": ["yalexs==8.10.0", "yalexs-ble==3.0.0"]
}

View File

@@ -6,6 +6,7 @@ from datetime import timedelta
import logging
API_CO2 = "carbon_dioxide"
API_DEW_POINT = "dew_point"
API_DUST = "dust"
API_HUMID = "humidity"
API_LUX = "illuminance"

View File

@@ -34,6 +34,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
API_CO2,
API_DEW_POINT,
API_DUST,
API_HUMID,
API_LUX,
@@ -110,6 +111,15 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = (
unique_id_tag="CO2", # matches legacy format
state_class=SensorStateClass.MEASUREMENT,
),
AwairSensorEntityDescription(
key=API_DEW_POINT,
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
translation_key="dew_point",
unique_id_tag="dew_point",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
)
SENSOR_TYPES_DUST: tuple[AwairSensorEntityDescription, ...] = (

View File

@@ -57,6 +57,9 @@
},
"sound_level": {
"name": "Sound level"
},
"dew_point": {
"name": "Dew point"
}
}
}

View File

@@ -30,7 +30,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: AxisConfigEntry)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
hub.setup()
config_entry.add_update_listener(hub.async_new_address_callback)
config_entry.async_on_unload(
config_entry.add_update_listener(hub.async_new_address_callback)
)
config_entry.async_on_unload(hub.teardown)
config_entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hub.shutdown)

View File

@@ -2,9 +2,9 @@
from homeassistant.config_entries import SOURCE_SYSTEM
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, discovery_flow
from homeassistant.helpers.backup import DATA_BACKUP
from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.typing import ConfigType
@@ -37,7 +37,6 @@ from .manager import (
IdleEvent,
IncorrectPasswordError,
ManagerBackup,
ManagerStateEvent,
NewBackup,
RestoreBackupEvent,
RestoreBackupStage,
@@ -45,6 +44,7 @@ from .manager import (
WrittenBackup,
)
from .models import AddonInfo, AgentBackup, BackupNotFound, Folder
from .services import async_setup_services
from .util import suggested_filename, suggested_filename_from_name_date
from .websocket import async_register_websocket_handlers
@@ -71,12 +71,12 @@ __all__ = [
"IncorrectPasswordError",
"LocalBackupAgent",
"ManagerBackup",
"ManagerStateEvent",
"NewBackup",
"RestoreBackupEvent",
"RestoreBackupStage",
"RestoreBackupState",
"WrittenBackup",
"async_get_manager",
"suggested_filename",
"suggested_filename_from_name_date",
]
@@ -103,39 +103,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
backup_manager = BackupManager(hass, reader_writer)
hass.data[DATA_MANAGER] = backup_manager
try:
await backup_manager.async_setup()
except Exception as err:
hass.data[DATA_BACKUP].manager_ready.set_exception(err)
raise
else:
hass.data[DATA_BACKUP].manager_ready.set_result(None)
await backup_manager.async_setup()
async_register_websocket_handlers(hass, with_hassio)
async def async_handle_create_service(call: ServiceCall) -> None:
"""Service handler for creating backups."""
agent_id = list(backup_manager.local_backup_agents)[0]
await backup_manager.async_create_backup(
agent_ids=[agent_id],
include_addons=None,
include_all_addons=False,
include_database=True,
include_folders=None,
include_homeassistant=True,
name=None,
password=None,
)
async def async_handle_create_automatic_service(call: ServiceCall) -> None:
"""Service handler for creating automatic backups."""
await backup_manager.async_create_automatic_backup()
if not with_hassio:
hass.services.async_register(DOMAIN, "create", async_handle_create_service)
hass.services.async_register(
DOMAIN, "create_automatic", async_handle_create_automatic_service
)
async_setup_services(hass)
async_register_http_views(hass)
@@ -164,3 +136,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: BackupConfigEntry) -> bo
async def async_unload_entry(hass: HomeAssistant, entry: BackupConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@callback
def async_get_manager(hass: HomeAssistant) -> BackupManager:
"""Get the backup manager instance.
Raises HomeAssistantError if the backup integration is not available.
"""
if DATA_MANAGER not in hass.data:
raise HomeAssistantError("Backup integration is not available")
return hass.data[DATA_MANAGER]

View File

@@ -1,38 +0,0 @@
"""Websocket commands for the Backup integration."""
from typing import Any
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.backup import async_subscribe_events
from .const import DATA_MANAGER
from .manager import ManagerStateEvent
@callback
def async_register_websocket_handlers(hass: HomeAssistant) -> None:
"""Register websocket commands."""
websocket_api.async_register_command(hass, handle_subscribe_events)
@websocket_api.require_admin
@websocket_api.websocket_command({vol.Required("type"): "backup/subscribe_events"})
@websocket_api.async_response
async def handle_subscribe_events(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Subscribe to backup events."""
def on_event(event: ManagerStateEvent) -> None:
connection.send_message(websocket_api.event_message(msg["id"], event))
if DATA_MANAGER in hass.data:
manager = hass.data[DATA_MANAGER]
on_event(manager.last_event)
connection.subscriptions[msg["id"]] = async_subscribe_events(hass, on_event)
connection.send_result(msg["id"])

View File

@@ -8,10 +8,6 @@ from datetime import datetime
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.backup import (
async_subscribe_events,
async_subscribe_platform_events,
)
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, LOGGER
@@ -56,8 +52,8 @@ class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]):
update_interval=None,
)
self.unsubscribe: list[Callable[[], None]] = [
async_subscribe_events(hass, self._on_event),
async_subscribe_platform_events(hass, self._on_event),
backup_manager.async_subscribe_events(self._on_event),
backup_manager.async_subscribe_platform_events(self._on_event),
]
self.backup_manager = backup_manager

View File

@@ -36,7 +36,6 @@ from homeassistant.helpers import (
issue_registry as ir,
start,
)
from homeassistant.helpers.backup import DATA_BACKUP
from homeassistant.helpers.json import json_bytes
from homeassistant.util import dt as dt_util, json as json_util
@@ -372,12 +371,10 @@ class BackupManager:
# Latest backup event and backup event subscribers
self.last_event: ManagerStateEvent = BlockedEvent()
self.last_action_event: ManagerStateEvent | None = None
self._backup_event_subscriptions = hass.data[
DATA_BACKUP
].backup_event_subscriptions
self._backup_platform_event_subscriptions = hass.data[
DATA_BACKUP
].backup_platform_event_subscriptions
self._backup_event_subscriptions: list[Callable[[ManagerStateEvent], None]] = []
self._backup_platform_event_subscriptions: list[
Callable[[BackupPlatformEvent], None]
] = []
async def async_setup(self) -> None:
"""Set up the backup manager."""
@@ -1385,6 +1382,32 @@ class BackupManager:
for subscription in self._backup_event_subscriptions:
subscription(event)
@callback
def async_subscribe_events(
self,
on_event: Callable[[ManagerStateEvent], None],
) -> Callable[[], None]:
"""Subscribe events."""
def remove_subscription() -> None:
self._backup_event_subscriptions.remove(on_event)
self._backup_event_subscriptions.append(on_event)
return remove_subscription
@callback
def async_subscribe_platform_events(
self,
on_event: Callable[[BackupPlatformEvent], None],
) -> Callable[[], None]:
"""Subscribe to backup platform events."""
def remove_subscription() -> None:
self._backup_platform_event_subscriptions.remove(on_event)
self._backup_platform_event_subscriptions.append(on_event)
return remove_subscription
def _create_automatic_backup_failed_issue(
self, translation_key: str, translation_placeholders: dict[str, str] | None
) -> None:

View File

@@ -19,9 +19,14 @@ from homeassistant.components.onboarding import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.backup import async_get_manager as async_get_backup_manager
from . import BackupManager, Folder, IncorrectPasswordError, http as backup_http
from . import (
BackupManager,
Folder,
IncorrectPasswordError,
async_get_manager,
http as backup_http,
)
if TYPE_CHECKING:
from homeassistant.components.onboarding import OnboardingStoreData
@@ -54,7 +59,7 @@ def with_backup_manager[_ViewT: BaseOnboardingView, **_P](
if self._data["done"]:
raise HTTPUnauthorized
manager = await async_get_backup_manager(request.app[KEY_HASS])
manager = async_get_manager(request.app[KEY_HASS])
return await func(self, manager, request, *args, **kwargs)
return with_backup

View File

@@ -0,0 +1,36 @@
"""The Backup integration."""
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers.hassio import is_hassio
from .const import DATA_MANAGER, DOMAIN
async def _async_handle_create_service(call: ServiceCall) -> None:
"""Service handler for creating backups."""
backup_manager = call.hass.data[DATA_MANAGER]
agent_id = list(backup_manager.local_backup_agents)[0]
await backup_manager.async_create_backup(
agent_ids=[agent_id],
include_addons=None,
include_all_addons=False,
include_database=True,
include_folders=None,
include_homeassistant=True,
name=None,
password=None,
)
async def _async_handle_create_automatic_service(call: ServiceCall) -> None:
"""Service handler for creating automatic backups."""
await call.hass.data[DATA_MANAGER].async_create_automatic_backup()
def async_setup_services(hass: HomeAssistant) -> None:
"""Register services."""
if not is_hassio(hass):
hass.services.async_register(DOMAIN, "create", _async_handle_create_service)
hass.services.async_register(
DOMAIN, "create_automatic", _async_handle_create_automatic_service
)

View File

@@ -10,7 +10,11 @@ from homeassistant.helpers import config_validation as cv
from .config import Day, ScheduleRecurrence
from .const import DATA_MANAGER, LOGGER
from .manager import DecryptOnDowloadNotSupported, IncorrectPasswordError
from .manager import (
DecryptOnDowloadNotSupported,
IncorrectPasswordError,
ManagerStateEvent,
)
from .models import BackupNotFound, Folder
@@ -30,6 +34,7 @@ def async_register_websocket_handlers(hass: HomeAssistant, with_hassio: bool) ->
websocket_api.async_register_command(hass, handle_create_with_automatic_settings)
websocket_api.async_register_command(hass, handle_delete)
websocket_api.async_register_command(hass, handle_restore)
websocket_api.async_register_command(hass, handle_subscribe_events)
websocket_api.async_register_command(hass, handle_config_info)
websocket_api.async_register_command(hass, handle_config_update)
@@ -417,3 +422,22 @@ def handle_config_update(
changes.pop("type")
manager.config.update(**changes)
connection.send_result(msg["id"])
@websocket_api.require_admin
@websocket_api.websocket_command({vol.Required("type"): "backup/subscribe_events"})
@websocket_api.async_response
async def handle_subscribe_events(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Subscribe to backup events."""
def on_event(event: ManagerStateEvent) -> None:
connection.send_message(websocket_api.event_message(msg["id"], event))
manager = hass.data[DATA_MANAGER]
on_event(manager.last_event)
connection.subscriptions[msg["id"]] = manager.async_subscribe_events(on_event)
connection.send_result(msg["id"])

View File

@@ -14,7 +14,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.typing import ConfigType
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
from .services import setup_services
from .services import async_setup_services
from .types import BoschAlarmConfigEntry
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -29,7 +29,7 @@ PLATFORMS: list[Platform] = [
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up bosch alarm services."""
setup_services(hass)
async_setup_services(hass)
return True

View File

@@ -9,7 +9,7 @@ from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.util import dt as dt_util
@@ -66,7 +66,8 @@ async def async_set_panel_date(call: ServiceCall) -> None:
) from err
def setup_services(hass: HomeAssistant) -> None:
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the services for the bosch alarm integration."""
hass.services.async_register(

View File

@@ -107,7 +107,7 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
"""Return the state of the device."""
media_state = self.client.play_state.state
if media_state == "NETWORK":
return MediaPlayerState.STANDBY
return MediaPlayerState.OFF
if self.client.state.power:
if media_state == "play":
return MediaPlayerState.PLAYING

View File

@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==0.104.0"],
"requirements": ["hass-nabucasa==0.106.0"],
"single_config_entry": true
}

View File

@@ -3,8 +3,8 @@
from __future__ import annotations
import asyncio
from typing import Any
from hass_nabucasa.payments_api import SubscriptionInfo
import voluptuous as vol
from homeassistant.components.repairs import (
@@ -26,7 +26,7 @@ MAX_RETRIES = 60 # This allows for 10 minutes of retries
@callback
def async_manage_legacy_subscription_issue(
hass: HomeAssistant,
subscription_info: dict[str, Any],
subscription_info: SubscriptionInfo,
) -> None:
"""Manage the legacy subscription issue.
@@ -50,7 +50,7 @@ class LegacySubscriptionRepairFlow(RepairsFlow):
"""Handler for an issue fixing flow."""
wait_task: asyncio.Task | None = None
_data: dict[str, Any] | None = None
_data: SubscriptionInfo | None = None
async def async_step_init(self, _: None = None) -> FlowResult:
"""Handle the first step of a fix flow."""

View File

@@ -8,6 +8,7 @@ from typing import Any
from aiohttp.client_exceptions import ClientError
from hass_nabucasa import Cloud, cloud_api
from hass_nabucasa.payments_api import PaymentsApiError, SubscriptionInfo
from .client import CloudClient
from .const import REQUEST_TIMEOUT
@@ -15,21 +16,13 @@ from .const import REQUEST_TIMEOUT
_LOGGER = logging.getLogger(__name__)
async def async_subscription_info(cloud: Cloud[CloudClient]) -> dict[str, Any] | None:
async def async_subscription_info(cloud: Cloud[CloudClient]) -> SubscriptionInfo | None:
"""Fetch the subscription info."""
try:
async with asyncio.timeout(REQUEST_TIMEOUT):
return await cloud_api.async_subscription_info(cloud)
except TimeoutError:
_LOGGER.error(
(
"A timeout of %s was reached while trying to fetch subscription"
" information"
),
REQUEST_TIMEOUT,
)
except ClientError:
_LOGGER.error("Failed to fetch subscription information")
return await cloud.payments.subscription_info()
except PaymentsApiError as exception:
_LOGGER.error("Failed to fetch subscription information - %s", exception)
return None

View File

@@ -6,11 +6,18 @@ from operator import itemgetter
import numpy as np
import voluptuous as vol
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
DOMAIN as SENSOR_DOMAIN,
STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
)
from homeassistant.const import (
CONF_ATTRIBUTE,
CONF_DEVICE_CLASS,
CONF_MAXIMUM,
CONF_MINIMUM,
CONF_NAME,
CONF_SOURCE,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
@@ -50,20 +57,23 @@ def datapoints_greater_than_degree(value: dict) -> dict:
COMPENSATION_SCHEMA = vol.Schema(
{
vol.Required(CONF_SOURCE): cv.entity_id,
vol.Optional(CONF_ATTRIBUTE): cv.string,
vol.Required(CONF_DATAPOINTS): [
vol.ExactSequence([vol.Coerce(float), vol.Coerce(float)])
],
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_ATTRIBUTE): cv.string,
vol.Optional(CONF_UPPER_LIMIT, default=False): cv.boolean,
vol.Optional(CONF_LOWER_LIMIT, default=False): cv.boolean,
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int,
vol.Optional(CONF_DEGREE, default=DEFAULT_DEGREE): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=7),
),
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_LOWER_LIMIT, default=False): cv.boolean,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int,
vol.Required(CONF_SOURCE): cv.entity_id,
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_UPPER_LIMIT, default=False): cv.boolean,
}
)

View File

@@ -7,15 +7,23 @@ from typing import Any
import numpy as np
from homeassistant.components.sensor import SensorEntity
from homeassistant.components.sensor import (
ATTR_STATE_CLASS,
CONF_STATE_CLASS,
SensorEntity,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
CONF_ATTRIBUTE,
CONF_DEVICE_CLASS,
CONF_MAXIMUM,
CONF_MINIMUM,
CONF_NAME,
CONF_SOURCE,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import (
@@ -59,24 +67,13 @@ async def async_setup_platform(
source: str = conf[CONF_SOURCE]
attribute: str | None = conf.get(CONF_ATTRIBUTE)
name = f"{DEFAULT_NAME} {source}"
if attribute is not None:
name = f"{name} {attribute}"
if not (name := conf.get(CONF_NAME)):
name = f"{DEFAULT_NAME} {source}"
if attribute is not None:
name = f"{name} {attribute}"
async_add_entities(
[
CompensationSensor(
conf.get(CONF_UNIQUE_ID),
name,
source,
attribute,
conf[CONF_PRECISION],
conf[CONF_POLYNOMIAL],
conf.get(CONF_UNIT_OF_MEASUREMENT),
conf[CONF_MINIMUM],
conf[CONF_MAXIMUM],
)
]
[CompensationSensor(conf.get(CONF_UNIQUE_ID), name, source, attribute, conf)]
)
@@ -91,23 +88,27 @@ class CompensationSensor(SensorEntity):
name: str,
source: str,
attribute: str | None,
precision: int,
polynomial: np.poly1d,
unit_of_measurement: str | None,
minimum: tuple[float, float] | None,
maximum: tuple[float, float] | None,
config: dict[str, Any],
) -> None:
"""Initialize the Compensation sensor."""
self._attr_name = name
self._source_entity_id = source
self._precision = precision
self._source_attribute = attribute
self._attr_native_unit_of_measurement = unit_of_measurement
self._precision = config[CONF_PRECISION]
self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
polynomial: np.poly1d = config[CONF_POLYNOMIAL]
self._poly = polynomial
self._coefficients = polynomial.coefficients.tolist()
self._attr_unique_id = unique_id
self._attr_name = name
self._minimum = minimum
self._maximum = maximum
self._minimum = config[CONF_MINIMUM]
self._maximum = config[CONF_MAXIMUM]
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
self._attr_state_class = config.get(CONF_STATE_CLASS)
async def async_added_to_hass(self) -> None:
"""Handle added to Hass."""
@@ -137,13 +138,40 @@ class CompensationSensor(SensorEntity):
"""Handle sensor state changes."""
new_state: State | None
if (new_state := event.data["new_state"]) is None:
_LOGGER.warning(
"While updating compensation %s, the new_state is None", self.name
)
self._attr_native_value = None
self.async_write_ha_state()
return
if new_state.state == STATE_UNKNOWN:
self._attr_native_value = None
self.async_write_ha_state()
return
if new_state.state == STATE_UNAVAILABLE:
self._attr_available = False
self.async_write_ha_state()
return
self._attr_available = True
if self.native_unit_of_measurement is None and self._source_attribute is None:
self._attr_native_unit_of_measurement = new_state.attributes.get(
ATTR_UNIT_OF_MEASUREMENT
)
if self._attr_device_class is None and (
device_class := new_state.attributes.get(ATTR_DEVICE_CLASS)
):
self._attr_device_class = device_class
if self._attr_state_class is None and (
state_class := new_state.attributes.get(ATTR_STATE_CLASS)
):
self._attr_state_class = state_class
if self._source_attribute:
value = new_state.attributes.get(self._source_attribute)
else:

View File

@@ -5,8 +5,9 @@ from pycoolmasternet_async import CoolMasterNet
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from .const import CONF_SWING_SUPPORT
from .const import CONF_SWING_SUPPORT, DOMAIN
from .coordinator import CoolmasterConfigEntry, CoolmasterDataUpdateCoordinator
PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CLIMATE, Platform.SENSOR]
@@ -48,3 +49,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: CoolmasterConfigEntry) -
async def async_unload_entry(hass: HomeAssistant, entry: CoolmasterConfigEntry) -> bool:
"""Unload a Coolmaster config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_remove_config_entry_device(
hass: HomeAssistant,
config_entry: CoolmasterConfigEntry,
device_entry: dr.DeviceEntry,
) -> bool:
"""Remove a config entry from a device."""
return not device_entry.identifiers.intersection(
(DOMAIN, unit_id) for unit_id in config_entry.runtime_data.data
)

View File

@@ -19,10 +19,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
SUPPORT_MINIMAL_SERVICES = VacuumEntityFeature.TURN_ON | VacuumEntityFeature.TURN_OFF
SUPPORT_BASIC_SERVICES = (
VacuumEntityFeature.STATE
| VacuumEntityFeature.START
| VacuumEntityFeature.STOP
| VacuumEntityFeature.BATTERY
VacuumEntityFeature.STATE | VacuumEntityFeature.START | VacuumEntityFeature.STOP
)
SUPPORT_MOST_SERVICES = (
@@ -31,7 +28,6 @@ SUPPORT_MOST_SERVICES = (
| VacuumEntityFeature.STOP
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.BATTERY
| VacuumEntityFeature.FAN_SPEED
)
@@ -46,7 +42,6 @@ SUPPORT_ALL_SERVICES = (
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.STATUS
| VacuumEntityFeature.BATTERY
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.MAP
| VacuumEntityFeature.CLEAN_SPOT
@@ -90,12 +85,6 @@ class StateDemoVacuum(StateVacuumEntity):
self._attr_activity = VacuumActivity.DOCKED
self._fan_speed = FAN_SPEEDS[1]
self._cleaned_area: float = 0
self._battery_level = 100
@property
def battery_level(self) -> int:
"""Return the current battery level of the vacuum."""
return max(0, min(100, self._battery_level))
@property
def fan_speed(self) -> str:
@@ -117,7 +106,6 @@ class StateDemoVacuum(StateVacuumEntity):
if self._attr_activity != VacuumActivity.CLEANING:
self._attr_activity = VacuumActivity.CLEANING
self._cleaned_area += 1.32
self._battery_level -= 1
self.schedule_update_ha_state()
def pause(self) -> None:
@@ -142,7 +130,6 @@ class StateDemoVacuum(StateVacuumEntity):
"""Perform a spot clean-up."""
self._attr_activity = VacuumActivity.CLEANING
self._cleaned_area += 1.32
self._battery_level -= 1
self.schedule_update_ha_state()
def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:

View File

@@ -94,6 +94,7 @@ async def _get_options_dict(handler: SchemaCommonFlowHandler | None) -> dict:
max=6,
mode=selector.NumberSelectorMode.BOX,
unit_of_measurement="decimals",
translation_key="round",
),
),
vol.Required(CONF_TIME_WINDOW): selector.DurationSelector(),

View File

@@ -198,6 +198,7 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
self._attr_native_value = round(Decimal(0), round_digits)
# List of tuples with (timestamp_start, timestamp_end, derivative)
self._state_list: list[tuple[datetime, datetime, Decimal]] = []
self._last_valid_state_time: tuple[str, datetime] | None = None
self._attr_name = name if name is not None else f"{source_entity} derivative"
self._attr_extra_state_attributes = {ATTR_SOURCE_ID: source_entity}
@@ -242,6 +243,25 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
if (current_time - time_end).total_seconds() < self._time_window
]
def _handle_invalid_source_state(self, state: State | None) -> bool:
# Check the source state for unknown/unavailable condition. If unusable, write unknown/unavailable state and return false.
if not state or state.state == STATE_UNAVAILABLE:
self._attr_available = False
self.async_write_ha_state()
return False
if not _is_decimal_state(state.state):
self._attr_available = True
self._write_native_value(None)
return False
self._attr_available = True
return True
def _write_native_value(self, derivative: Decimal | None) -> None:
self._attr_native_value = (
None if derivative is None else round(derivative, self._round_digits)
)
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
await super().async_added_to_hass()
@@ -255,8 +275,8 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
Decimal(restored_data.native_value), # type: ignore[arg-type]
self._round_digits,
)
except SyntaxError as err:
_LOGGER.warning("Could not restore last state: %s", err)
except (InvalidOperation, TypeError):
self._attr_native_value = None
def schedule_max_sub_interval_exceeded(source_state: State | None) -> None:
"""Schedule calculation using the source state and max_sub_interval.
@@ -280,9 +300,7 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
self._prune_state_list(now)
derivative = self._calc_derivative_from_state_list(now)
self._attr_native_value = round(derivative, self._round_digits)
self.async_write_ha_state()
self._write_native_value(derivative)
# If derivative is now zero, don't schedule another timeout callback, as it will have no effect
if derivative != 0:
@@ -299,36 +317,46 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
"""Handle constant sensor state."""
self._cancel_max_sub_interval_exceeded_callback()
new_state = event.data["new_state"]
if not self._handle_invalid_source_state(new_state):
return
assert new_state
if self._attr_native_value == Decimal(0):
# If the derivative is zero, and the source sensor hasn't
# changed state, then we know it will still be zero.
return
schedule_max_sub_interval_exceeded(new_state)
new_state = event.data["new_state"]
if new_state is not None:
calc_derivative(
new_state, new_state.state, event.data["old_last_reported"]
)
calc_derivative(new_state, new_state.state, event.data["old_last_reported"])
@callback
def on_state_changed(event: Event[EventStateChangedData]) -> None:
"""Handle changed sensor state."""
self._cancel_max_sub_interval_exceeded_callback()
new_state = event.data["new_state"]
if not self._handle_invalid_source_state(new_state):
return
assert new_state
schedule_max_sub_interval_exceeded(new_state)
old_state = event.data["old_state"]
if new_state is not None and old_state is not None:
if old_state is not None:
calc_derivative(new_state, old_state.state, old_state.last_reported)
else:
# On first state change from none, update availability
self.async_write_ha_state()
def calc_derivative(
new_state: State, old_value: str, old_last_reported: datetime
) -> None:
"""Handle the sensor state changes."""
if old_value in (STATE_UNKNOWN, STATE_UNAVAILABLE) or new_state.state in (
STATE_UNKNOWN,
STATE_UNAVAILABLE,
):
return
if not _is_decimal_state(old_value):
if self._last_valid_state_time:
old_value = self._last_valid_state_time[0]
old_last_reported = self._last_valid_state_time[1]
else:
# Sensor becomes valid for the first time, just keep the restored value
self.async_write_ha_state()
return
if self.native_unit_of_measurement is None:
unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
@@ -373,6 +401,10 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
self._state_list.append(
(old_last_reported, new_state.last_reported, new_derivative)
)
self._last_valid_state_time = (
new_state.state,
new_state.last_reported,
)
# If outside of time window just report derivative (is the same as modeling it in the window),
# otherwise take the weighted average with the previous derivatives
@@ -382,11 +414,16 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
derivative = self._calc_derivative_from_state_list(
new_state.last_reported
)
self._attr_native_value = round(derivative, self._round_digits)
self.async_write_ha_state()
self._write_native_value(derivative)
source_state = self.hass.states.get(self._sensor_source_id)
if source_state is None or source_state.state in [
STATE_UNAVAILABLE,
STATE_UNKNOWN,
]:
self._attr_available = False
if self._max_sub_interval is not None:
source_state = self.hass.states.get(self._sensor_source_id)
schedule_max_sub_interval_exceeded(source_state)
@callback

View File

@@ -52,6 +52,11 @@
"h": "Hours",
"d": "Days"
}
},
"round": {
"unit_of_measurement": {
"decimals": "decimals"
}
}
}
}

View File

@@ -9,7 +9,7 @@ from devolo_home_control_api.devices.zwave import Zwave
from devolo_home_control_api.homecontrol import HomeControl
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import Entity
from .const import DOMAIN
@@ -35,7 +35,7 @@ class DevoloDeviceEntity(Entity):
) # This is not doing I/O. It fetches an internal state of the API
self._attr_should_poll = False
self._attr_unique_id = element_uid
self._attr_device_info = DeviceInfo(
self._attr_device_info = dr.DeviceInfo(
configuration_url=f"https://{urlparse(device_instance.href).netloc}",
identifiers={(DOMAIN, self._device_instance.uid)},
manufacturer=device_instance.brand,
@@ -87,7 +87,32 @@ class DevoloDeviceEntity(Entity):
self._value = message[1]
elif len(message) == 3 and message[2] == "status":
# Maybe the API wants to tell us, that the device went on- or offline.
self._attr_available = self._device_instance.is_online()
state = self._device_instance.is_online()
if state != self.available and not state:
_LOGGER.info(
"Device %s is unavailable",
self._device_instance.settings_property[
"general_device_settings"
].name,
)
if state != self.available and state:
_LOGGER.info(
"Device %s is back online",
self._device_instance.settings_property[
"general_device_settings"
].name,
)
self._attr_available = state
elif message[1] == "del" and self.platform.config_entry:
device_registry = dr.async_get(self.hass)
device = device_registry.async_get_device(
identifiers={(DOMAIN, self._device_instance.uid)}
)
if device:
device_registry.async_update_device(
device.id,
remove_config_entry_id=self.platform.config_entry.entry_id,
)
else:
_LOGGER.debug("No valid message received: %s", message)

View File

@@ -207,7 +207,7 @@ class DevoloUptimeGetCoordinator(DevoloDataUpdateCoordinator[int]):
class DevoloWifiConnectedStationsGetCoordinator(
DevoloDataUpdateCoordinator[list[ConnectedStationInfo]]
DevoloDataUpdateCoordinator[dict[str, ConnectedStationInfo]]
):
"""Class to manage fetching data from the WifiGuestAccessGet endpoint."""
@@ -230,10 +230,11 @@ class DevoloWifiConnectedStationsGetCoordinator(
)
self.update_method = self.async_get_wifi_connected_station
async def async_get_wifi_connected_station(self) -> list[ConnectedStationInfo]:
async def async_get_wifi_connected_station(self) -> dict[str, ConnectedStationInfo]:
"""Fetch data from API endpoint."""
assert self.device.device
return await self.device.device.async_get_wifi_connected_station()
clients = await self.device.device.async_get_wifi_connected_station()
return {client.mac_address: client for client in clients}
class DevoloWifiGuestAccessGetCoordinator(

View File

@@ -28,9 +28,9 @@ async def async_setup_entry(
) -> None:
"""Get all devices and sensors and setup them via config entry."""
device = entry.runtime_data.device
coordinators: dict[str, DevoloDataUpdateCoordinator[list[ConnectedStationInfo]]] = (
entry.runtime_data.coordinators
)
coordinators: dict[
str, DevoloDataUpdateCoordinator[dict[str, ConnectedStationInfo]]
] = entry.runtime_data.coordinators
registry = er.async_get(hass)
tracked = set()
@@ -38,16 +38,16 @@ async def async_setup_entry(
def new_device_callback() -> None:
"""Add new devices if needed."""
new_entities = []
for station in coordinators[CONNECTED_WIFI_CLIENTS].data:
if station.mac_address in tracked:
for mac_address in coordinators[CONNECTED_WIFI_CLIENTS].data:
if mac_address in tracked:
continue
new_entities.append(
DevoloScannerEntity(
coordinators[CONNECTED_WIFI_CLIENTS], device, station.mac_address
coordinators[CONNECTED_WIFI_CLIENTS], device, mac_address
)
)
tracked.add(station.mac_address)
tracked.add(mac_address)
async_add_entities(new_entities)
@callback
@@ -82,7 +82,7 @@ async def async_setup_entry(
# The pylint disable is needed because of https://github.com/pylint-dev/pylint/issues/9138
class DevoloScannerEntity( # pylint: disable=hass-enforce-class-module
CoordinatorEntity[DevoloDataUpdateCoordinator[list[ConnectedStationInfo]]],
CoordinatorEntity[DevoloDataUpdateCoordinator[dict[str, ConnectedStationInfo]]],
ScannerEntity,
):
"""Representation of a devolo device tracker."""
@@ -92,7 +92,7 @@ class DevoloScannerEntity( # pylint: disable=hass-enforce-class-module
def __init__(
self,
coordinator: DevoloDataUpdateCoordinator[list[ConnectedStationInfo]],
coordinator: DevoloDataUpdateCoordinator[dict[str, ConnectedStationInfo]],
device: Device,
mac: str,
) -> None:
@@ -109,14 +109,8 @@ class DevoloScannerEntity( # pylint: disable=hass-enforce-class-module
if not self.coordinator.data:
return {}
station = next(
(
station
for station in self.coordinator.data
if station.mac_address == self.mac_address
),
None,
)
assert self.mac_address
station = self.coordinator.data.get(self.mac_address)
if station:
attrs["wifi"] = WIFI_APTYPE.get(station.vap_type, STATE_UNKNOWN)
attrs["band"] = (
@@ -129,11 +123,8 @@ class DevoloScannerEntity( # pylint: disable=hass-enforce-class-module
@property
def is_connected(self) -> bool:
"""Return true if the device is connected to the network."""
return any(
station
for station in self.coordinator.data
if station.mac_address == self.mac_address
)
assert self.mac_address
return self.coordinator.data.get(self.mac_address) is not None
@property
def unique_id(self) -> str:

View File

@@ -21,7 +21,7 @@ from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEnt
type _DataType = (
LogicalNetwork
| DataRate
| list[ConnectedStationInfo]
| dict[str, ConnectedStationInfo]
| list[NeighborAPInfo]
| WifiGuestAccessGet
| bool

View File

@@ -47,7 +47,11 @@ def _last_restart(runtime: int) -> datetime:
type _CoordinatorDataType = (
LogicalNetwork | DataRate | list[ConnectedStationInfo] | list[NeighborAPInfo] | int
LogicalNetwork
| DataRate
| dict[str, ConnectedStationInfo]
| list[NeighborAPInfo]
| int
)
type _SensorDataType = int | float | datetime
@@ -79,7 +83,7 @@ SENSOR_TYPES: dict[str, DevoloSensorEntityDescription[Any, Any]] = {
),
),
CONNECTED_WIFI_CLIENTS: DevoloSensorEntityDescription[
list[ConnectedStationInfo], int
dict[str, ConnectedStationInfo], int
](
key=CONNECTED_WIFI_CLIENTS,
state_class=SensorStateClass.MEASUREMENT,

View File

@@ -172,6 +172,9 @@ class DnsIPOptionsFlowHandler(OptionsFlow):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the options."""
if self.config_entry.data[CONF_HOSTNAME] == DEFAULT_HOSTNAME:
return self.async_abort(reason="no_options")
errors = {}
if user_input is not None:
resolver = user_input.get(CONF_RESOLVER, DEFAULT_RESOLVER)

View File

@@ -30,7 +30,8 @@
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"no_options": "The myip hostname requires the default resolvers and therefore cannot be configured."
},
"error": {
"invalid_resolver": "Invalid IP address or port for resolver"

View File

@@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pydoods"],
"quality_scale": "legacy",
"requirements": ["pydoods==1.0.2", "Pillow==11.2.1"]
"requirements": ["pydoods==1.0.2", "Pillow==11.3.0"]
}

View File

@@ -65,12 +65,10 @@ def download_file(service: ServiceCall) -> None:
else:
if filename is None and "content-disposition" in req.headers:
match = re.findall(
if match := re.search(
r"filename=(\S+)", req.headers["content-disposition"]
)
if match:
filename = match[0].strip("'\" ")
):
filename = match.group(1).strip("'\" ")
if not filename:
filename = os.path.basename(url).strip()

View File

@@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"description": "To identify the desired region, either the warncell ID / name or device tracker is required. The provided device tracker has to contain the attributes 'latitude' and 'longitude'.",
"description": "To identify the desired region, either the warncell ID / name or device tracker is required. The provided device tracker has to contain the attributes 'Latitude' and 'Longitude'.",
"data": {
"region_identifier": "Warncell ID or name",
"region_device_tracker": "Device tracker entity"
@@ -14,7 +14,7 @@
"ambiguous_identifier": "The region identifier and device tracker can not be specified together.",
"invalid_identifier": "The specified region identifier / device tracker is invalid.",
"entity_not_found": "The specified device tracker entity was not found.",
"attribute_not_found": "The required `latitude` or `longitude` attribute was not found in the specified device tracker."
"attribute_not_found": "The required attributes 'Latitude' and 'Longitude' were not found in the specified device tracker."
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",

View File

@@ -12,7 +12,7 @@ from .bridge import DynaliteBridge
from .const import DOMAIN, LOGGER, PLATFORMS
from .convert_config import convert_config
from .panel import async_register_dynalite_frontend
from .services import setup_services
from .services import async_setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -21,7 +21,7 @@ type DynaliteConfigEntry = ConfigEntry[DynaliteBridge]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Dynalite platform."""
setup_services(hass)
async_setup_services(hass)
await async_register_dynalite_frontend(hass)

View File

@@ -50,7 +50,7 @@ async def _request_channel_level(service_call: ServiceCall) -> None:
@callback
def setup_services(hass: HomeAssistant) -> None:
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the Dynalite platform."""
hass.services.async_register(
DOMAIN,

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==13.4.0"]
"requirements": ["py-sucks==0.9.11", "deebot-client==13.5.0"]
}

View File

@@ -10,7 +10,12 @@ from eheimdigital.device import EheimDigitalDevice
from eheimdigital.hub import EheimDigitalHub
import voluptuous as vol
from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import (
SOURCE_RECONFIGURE,
SOURCE_USER,
ConfigFlow,
ConfigFlowResult,
)
from homeassistant.const import CONF_HOST
from homeassistant.helpers import selector
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -126,3 +131,52 @@ class EheimDigitalConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=CONFIG_SCHEMA,
errors=errors,
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of the config entry."""
if user_input is None:
return self.async_show_form(
step_id=SOURCE_RECONFIGURE, data_schema=CONFIG_SCHEMA
)
self._async_abort_entries_match(user_input)
errors: dict[str, str] = {}
hub = EheimDigitalHub(
host=user_input[CONF_HOST],
session=async_get_clientsession(self.hass),
loop=self.hass.loop,
main_device_added_event=self.main_device_added_event,
)
try:
await hub.connect()
async with asyncio.timeout(2):
# This event gets triggered when the first message is received from
# the device, it contains the data necessary to create the main device.
# This removes the race condition where the main device is accessed
# before the response from the device is parsed.
await self.main_device_added_event.wait()
if TYPE_CHECKING:
# At this point the main device is always set
assert isinstance(hub.main, EheimDigitalDevice)
await self.async_set_unique_id(hub.main.mac_address)
await hub.close()
except (ClientError, TimeoutError):
errors["base"] = "cannot_connect"
except Exception: # noqa: BLE001
errors["base"] = "unknown"
LOGGER.exception("Unknown exception occurred")
else:
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(),
data_updates=user_input,
)
return self.async_show_form(
step_id=SOURCE_RECONFIGURE,
data_schema=CONFIG_SCHEMA,
errors=errors,
)

View File

@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["eheimdigital"],
"quality_scale": "bronze",
"requirements": ["eheimdigital==1.2.0"],
"requirements": ["eheimdigital==1.3.0"],
"zeroconf": [
{ "type": "_http._tcp.local.", "name": "eheimdigital._http._tcp.local." }
]

View File

@@ -60,7 +60,7 @@ rules:
entity-translations: done
exception-translations: done
icon-translations: todo
reconfiguration-flow: todo
reconfiguration-flow: done
repair-issues: todo
stale-devices: done

View File

@@ -4,6 +4,14 @@
"discovery_confirm": {
"description": "[%key:common::config_flow::description::confirm_setup%]"
},
"reconfigure": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "[%key:component::eheimdigital::config::step::user::data_description::host%]"
}
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
@@ -15,7 +23,9 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"unique_id_mismatch": "The identifier does not match the previous identifier"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",

View File

@@ -16,7 +16,12 @@ from homeassistant.config_entries import (
from homeassistant.const import CONF_API_KEY, CONF_URL
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import selector
from homeassistant.helpers.selector import (
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
selector,
)
from .const import (
CONF_MESSAGE,
@@ -26,6 +31,9 @@ from .const import (
FEED_ID,
FEED_NAME,
FEED_TAG,
SYNC_MODE,
SYNC_MODE_AUTO,
SYNC_MODE_MANUAL,
)
@@ -102,6 +110,17 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
"mode": "dropdown",
"multiple": True,
}
if user_input.get(SYNC_MODE) == SYNC_MODE_AUTO:
return self.async_create_entry(
title=sensor_name(self.url),
data={
CONF_URL: self.url,
CONF_API_KEY: self.api_key,
CONF_ONLY_INCLUDE_FEEDID: [
feed[FEED_ID] for feed in result[CONF_MESSAGE]
],
},
)
return await self.async_step_choose_feeds()
return self.async_show_form(
step_id="user",
@@ -110,6 +129,15 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
{
vol.Required(CONF_URL): str,
vol.Required(CONF_API_KEY): str,
vol.Required(
SYNC_MODE, default=SYNC_MODE_MANUAL
): SelectSelector(
SelectSelectorConfig(
options=[SYNC_MODE_MANUAL, SYNC_MODE_AUTO],
mode=SelectSelectorMode.DROPDOWN,
translation_key=SYNC_MODE,
)
),
}
),
user_input,
@@ -151,6 +179,47 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Reconfigure the entry."""
errors: dict[str, str] = {}
description_placeholders = {}
reconfig_entry = self._get_reconfigure_entry()
if user_input is not None:
url = user_input[CONF_URL]
api_key = user_input[CONF_API_KEY]
emoncms_client = EmoncmsClient(
url, api_key, session=async_get_clientsession(self.hass)
)
result = await get_feed_list(emoncms_client)
if not result[CONF_SUCCESS]:
errors["base"] = "api_error"
description_placeholders = {"details": result[CONF_MESSAGE]}
else:
await self.async_set_unique_id(await emoncms_client.async_get_uuid())
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
reconfig_entry,
title=sensor_name(url),
data=user_input,
reload_even_if_entry_is_unchanged=False,
)
return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
vol.Schema(
{
vol.Required(CONF_URL): str,
vol.Required(CONF_API_KEY): str,
}
),
user_input or reconfig_entry.data,
),
errors=errors,
description_placeholders=description_placeholders,
)
class EmoncmsOptionsFlow(OptionsFlow):
"""Emoncms Options flow handler."""

View File

@@ -14,6 +14,9 @@ EMONCMS_UUID_DOC_URL = (
FEED_ID = "id"
FEED_NAME = "name"
FEED_TAG = "tag"
SYNC_MODE = "sync_mode"
SYNC_MODE_AUTO = "auto"
SYNC_MODE_MANUAL = "manual"
LOGGER = logging.getLogger(__package__)

View File

@@ -7,7 +7,8 @@
"user": {
"data": {
"url": "[%key:common::config_flow::data::url%]",
"api_key": "[%key:common::config_flow::data::api_key%]"
"api_key": "[%key:common::config_flow::data::api_key%]",
"sync_mode": "Synchronization mode"
},
"data_description": {
"url": "Server URL starting with the protocol (http or https)",
@@ -21,7 +22,17 @@
}
},
"abort": {
"already_configured": "This server is already configured"
"already_configured": "This server is already configured",
"unique_id_mismatch": "This emoncms serial number does not match the previous serial number",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
}
},
"selector": {
"sync_mode": {
"options": {
"auto": "Synchronize all available Feeds",
"manual": "Select which Feeds to synchronize"
}
}
},
"entity": {

View File

@@ -63,6 +63,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) ->
coordinator = entry.runtime_data
coordinator.async_cancel_token_refresh()
coordinator.async_cancel_firmware_refresh()
coordinator.async_cancel_mac_verification()
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -126,6 +126,7 @@ class EnvoyEnchargeBinarySensorEntity(EnvoyBaseBinarySensorEntity):
name=f"Encharge {serial_number}",
sw_version=str(encharge_inventory[self._serial_number].firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
serial_number=serial_number,
)
@property
@@ -158,6 +159,7 @@ class EnvoyEnpowerBinarySensorEntity(EnvoyBaseBinarySensorEntity):
name=f"Enpower {enpower.serial_number}",
sw_version=str(enpower.firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
serial_number=enpower.serial_number,
)
@property

View File

@@ -220,6 +220,7 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
await envoy.setup()
assert envoy.serial_number is not None
self.envoy_serial_number = envoy.serial_number
_LOGGER.debug("Envoy setup complete for serial: %s", self.envoy_serial_number)
if token := self.config_entry.data.get(CONF_TOKEN):
with contextlib.suppress(*INVALID_AUTH_ERRORS):
# Always set the username and password
@@ -227,6 +228,7 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
await envoy.authenticate(
username=self.username, password=self.password, token=token
)
_LOGGER.debug("Authorized, validating token lifetime")
# The token is valid, but we still want
# to refresh it if it's stale right away
self._async_refresh_token_if_needed(dt_util.utcnow())
@@ -234,6 +236,8 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
# token likely expired or firmware changed
# so we fall through to authenticate with
# username/password
_LOGGER.debug("setup and auth got INVALID_AUTH_ERRORS")
_LOGGER.debug("Authenticate with username/password only")
await self.envoy.authenticate(username=self.username, password=self.password)
# Password auth succeeded, so we can update the token
# if we are using EnvoyTokenAuth
@@ -262,13 +266,16 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
for tries in range(2):
try:
if not self._setup_complete:
_LOGGER.debug("update on try %s, setup not complete", tries)
await self._async_setup_and_authenticate()
self._async_mark_setup_complete()
# dump all received data in debug mode to assist troubleshooting
envoy_data = await envoy.update()
except INVALID_AUTH_ERRORS as err:
_LOGGER.debug("update on try %s, INVALID_AUTH_ERRORS %s", tries, err)
if self._setup_complete and tries == 0:
# token likely expired or firmware changed, try to re-authenticate
_LOGGER.debug("update on try %s, setup was complete, retry", tries)
self._setup_complete = False
continue
raise ConfigEntryAuthFailed(
@@ -280,6 +287,7 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
},
) from err
except EnvoyError as err:
_LOGGER.debug("update on try %s, EnvoyError %s", tries, err)
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="envoy_error",

View File

@@ -7,7 +7,7 @@
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"quality_scale": "platinum",
"requirements": ["pyenphase==2.1.0"],
"requirements": ["pyenphase==2.2.1"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."

View File

@@ -165,6 +165,7 @@ class EnvoyStorageSettingsNumberEntity(EnvoyBaseEntity, NumberEntity):
name=f"Enpower {self._serial_number}",
sw_version=str(enpower.firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
serial_number=self._serial_number,
)
else:
# If no enpower device assign numbers to Envoy itself

View File

@@ -223,6 +223,7 @@ class EnvoyStorageSettingsSelectEntity(EnvoyBaseEntity, SelectEntity):
name=f"Enpower {self._serial_number}",
sw_version=str(enpower.firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
serial_number=self._serial_number,
)
else:
# If no enpower device assign selects to Envoy itself

View File

@@ -1313,6 +1313,7 @@ class EnvoyInverterEntity(EnvoySensorBaseEntity):
manufacturer="Enphase",
model="Inverter",
via_device=(DOMAIN, self.envoy_serial_num),
serial_number=serial_number,
)
@property
@@ -1356,6 +1357,7 @@ class EnvoyEnchargeEntity(EnvoySensorBaseEntity):
name=f"Encharge {serial_number}",
sw_version=str(encharge_inventory[self._serial_number].firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
serial_number=serial_number,
)
@@ -1420,6 +1422,7 @@ class EnvoyEnpowerEntity(EnvoySensorBaseEntity):
name=f"Enpower {enpower_data.serial_number}",
sw_version=str(enpower_data.firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
serial_number=enpower_data.serial_number,
)
@property

View File

@@ -363,7 +363,7 @@
"discharging": "[%key:common::state::discharging%]",
"idle": "[%key:common::state::idle%]",
"charging": "[%key:common::state::charging%]",
"full": "Full"
"full": "[%key:common::state::full%]"
}
},
"acb_available_energy": {

View File

@@ -138,6 +138,7 @@ class EnvoyEnpowerSwitchEntity(EnvoyBaseEntity, SwitchEntity):
name=f"Enpower {self._serial_number}",
sw_version=str(enpower.firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
serial_number=self._serial_number,
)
@property
@@ -235,6 +236,7 @@ class EnvoyStorageSettingsSwitchEntity(EnvoyBaseEntity, SwitchEntity):
name=f"Enpower {self._serial_number}",
sw_version=str(enpower.firmware_version),
via_device=(DOMAIN, self.envoy_serial_num),
serial_number=self._serial_number,
)
else:
# If no enpower device assign switches to Envoy itself

View File

@@ -33,7 +33,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
# Import config flow so that it's added to the registry
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData, build_device_unique_id
from .entry_data import (
DeviceEntityKey,
ESPHomeConfigEntry,
RuntimeEntryData,
build_device_unique_id,
)
from .enum_mapper import EsphomeEnumMapper
_LOGGER = logging.getLogger(__name__)
@@ -59,17 +64,32 @@ def async_static_info_updated(
device_info = entry_data.device_info
if TYPE_CHECKING:
assert device_info is not None
new_infos: dict[int, EntityInfo] = {}
new_infos: dict[DeviceEntityKey, EntityInfo] = {}
add_entities: list[_EntityT] = []
ent_reg = er.async_get(hass)
dev_reg = dr.async_get(hass)
# Track info by (info.device_id, info.key) to properly handle entities
# moving between devices and support sub-devices with overlapping keys
for info in infos:
new_infos[info.key] = info
info_key = (info.device_id, info.key)
new_infos[info_key] = info
# Try to find existing entity - first with current device_id
old_info = current_infos.pop(info_key, None)
# If not found, search for entity with same key but different device_id
# This handles the case where entity moved between devices
if not old_info:
for existing_device_id, existing_key in list(current_infos):
if existing_key == info.key:
# Found entity with same key but different device_id
old_info = current_infos.pop((existing_device_id, existing_key))
break
# Create new entity if it doesn't exist
if not (old_info := current_infos.pop(info.key, None)):
if not old_info:
entity = entity_type(entry_data, platform.domain, info, state_type)
add_entities.append(entity)
continue
@@ -78,7 +98,7 @@ def async_static_info_updated(
if old_info.device_id == info.device_id:
continue
# Entity has switched devices, need to migrate unique_id
# Entity has switched devices, need to migrate unique_id and handle state subscriptions
old_unique_id = build_device_unique_id(device_info.mac_address, old_info)
entity_id = ent_reg.async_get_entity_id(platform.domain, DOMAIN, old_unique_id)
@@ -103,7 +123,7 @@ def async_static_info_updated(
if old_unique_id != new_unique_id:
updates["new_unique_id"] = new_unique_id
# Update device assignment
# Update device assignment in registry
if info.device_id:
# Entity now belongs to a sub device
new_device = dev_reg.async_get_device(
@@ -118,10 +138,32 @@ def async_static_info_updated(
if new_device:
updates["device_id"] = new_device.id
# Apply all updates at once
# Apply all registry updates at once
if updates:
ent_reg.async_update_entity(entity_id, **updates)
# IMPORTANT: The entity's device assignment in Home Assistant is only read when the entity
# is first added. Updating the registry alone won't move the entity to the new device
# in the UI. Additionally, the entity's state subscription is tied to the old device_id,
# so it won't receive state updates for the new device_id.
#
# We must remove the old entity and re-add it to ensure:
# 1. The entity appears under the correct device in the UI
# 2. The entity's state subscription is updated to use the new device_id
_LOGGER.debug(
"Entity %s moving from device_id %s to %s",
info.key,
old_info.device_id,
info.device_id,
)
# Signal the existing entity to remove itself
# The entity is registered with the old device_id, so we signal with that
entry_data.async_signal_entity_removal(info_type, old_info.device_id, info.key)
# Create new entity with the new device_id
add_entities.append(entity_type(entry_data, platform.domain, info, state_type))
# Anything still in current_infos is now gone
if current_infos:
entry_data.async_remove_entities(
@@ -341,7 +383,10 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
)
self.async_on_remove(
entry_data.async_subscribe_state_update(
self._state_type, self._key, self._on_state_update
self._static_info.device_id,
self._state_type,
self._key,
self._on_state_update,
)
)
self.async_on_remove(
@@ -349,8 +394,29 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
self._static_info, self._on_static_info_update
)
)
# Register to be notified when this entity should remove itself
# This happens when the entity moves to a different device
self.async_on_remove(
entry_data.async_register_entity_removal_callback(
type(self._static_info),
self._static_info.device_id,
self._key,
self._on_removal_signal,
)
)
self._update_state_from_entry_data()
@callback
def _on_removal_signal(self) -> None:
"""Handle signal to remove this entity."""
_LOGGER.debug(
"Entity %s received removal signal due to device_id change",
self.entity_id,
)
# Schedule the entity to be removed
# This must be done as a task since we're in a callback
self.hass.async_create_task(self.async_remove())
@callback
def _on_static_info_update(self, static_info: EntityInfo) -> None:
"""Save the static info for this entity when it changes.

View File

@@ -60,7 +60,9 @@ from .const import DOMAIN
from .dashboard import async_get_dashboard
type ESPHomeConfigEntry = ConfigEntry[RuntimeEntryData]
type EntityStateKey = tuple[type[EntityState], int, int] # (state_type, device_id, key)
type EntityInfoKey = tuple[type[EntityInfo], int, int] # (info_type, device_id, key)
type DeviceEntityKey = tuple[int, int] # (device_id, key)
INFO_TO_COMPONENT_TYPE: Final = {v: k for k, v in COMPONENT_TYPE_TO_INFO.items()}
@@ -137,8 +139,10 @@ class RuntimeEntryData:
# When the disconnect callback is called, we mark all states
# as stale so we will always dispatch a state update when the
# device reconnects. This is the same format as state_subscriptions.
stale_state: set[tuple[type[EntityState], int]] = field(default_factory=set)
info: dict[type[EntityInfo], dict[int, EntityInfo]] = field(default_factory=dict)
stale_state: set[EntityStateKey] = field(default_factory=set)
info: dict[type[EntityInfo], dict[DeviceEntityKey, EntityInfo]] = field(
default_factory=dict
)
services: dict[int, UserService] = field(default_factory=dict)
available: bool = False
expected_disconnect: bool = False # Last disconnect was expected (e.g. deep sleep)
@@ -147,7 +151,7 @@ class RuntimeEntryData:
api_version: APIVersion = field(default_factory=APIVersion)
cleanup_callbacks: list[CALLBACK_TYPE] = field(default_factory=list)
disconnect_callbacks: set[CALLBACK_TYPE] = field(default_factory=set)
state_subscriptions: dict[tuple[type[EntityState], int], CALLBACK_TYPE] = field(
state_subscriptions: dict[EntityStateKey, CALLBACK_TYPE] = field(
default_factory=dict
)
device_update_subscriptions: set[CALLBACK_TYPE] = field(default_factory=set)
@@ -164,7 +168,7 @@ class RuntimeEntryData:
type[EntityInfo], list[Callable[[list[EntityInfo]], None]]
] = field(default_factory=dict)
entity_info_key_updated_callbacks: dict[
tuple[type[EntityInfo], int], list[Callable[[EntityInfo], None]]
EntityInfoKey, list[Callable[[EntityInfo], None]]
] = field(default_factory=dict)
original_options: dict[str, Any] = field(default_factory=dict)
media_player_formats: dict[str, list[MediaPlayerSupportedFormat]] = field(
@@ -177,6 +181,9 @@ class RuntimeEntryData:
default_factory=list
)
device_id_to_name: dict[int, str] = field(default_factory=dict)
entity_removal_callbacks: dict[EntityInfoKey, list[CALLBACK_TYPE]] = field(
default_factory=dict
)
@property
def name(self) -> str:
@@ -210,7 +217,7 @@ class RuntimeEntryData:
callback_: Callable[[EntityInfo], None],
) -> CALLBACK_TYPE:
"""Register to receive callbacks when static info is updated for a specific key."""
callback_key = (type(static_info), static_info.key)
callback_key = (type(static_info), static_info.device_id, static_info.key)
callbacks = self.entity_info_key_updated_callbacks.setdefault(callback_key, [])
callbacks.append(callback_)
return partial(callbacks.remove, callback_)
@@ -250,7 +257,9 @@ class RuntimeEntryData:
"""Call static info updated callbacks."""
callbacks = self.entity_info_key_updated_callbacks
for static_info in static_infos:
for callback_ in callbacks.get((type(static_info), static_info.key), ()):
for callback_ in callbacks.get(
(type(static_info), static_info.device_id, static_info.key), ()
):
callback_(static_info)
async def _ensure_platforms_loaded(
@@ -342,12 +351,13 @@ class RuntimeEntryData:
@callback
def async_subscribe_state_update(
self,
device_id: int,
state_type: type[EntityState],
state_key: int,
entity_callback: CALLBACK_TYPE,
) -> CALLBACK_TYPE:
"""Subscribe to state updates."""
subscription_key = (state_type, state_key)
subscription_key = (state_type, device_id, state_key)
self.state_subscriptions[subscription_key] = entity_callback
return partial(delitem, self.state_subscriptions, subscription_key)
@@ -359,7 +369,7 @@ class RuntimeEntryData:
stale_state = self.stale_state
current_state_by_type = self.state[state_type]
current_state = current_state_by_type.get(key, _SENTINEL)
subscription_key = (state_type, key)
subscription_key = (state_type, state.device_id, key)
if (
current_state == state
and subscription_key not in stale_state
@@ -367,7 +377,7 @@ class RuntimeEntryData:
and not (
state_type is SensorState
and (platform_info := self.info.get(SensorInfo))
and (entity_info := platform_info.get(state.key))
and (entity_info := platform_info.get((state.device_id, state.key)))
and (cast(SensorInfo, entity_info)).force_update
)
):
@@ -520,3 +530,26 @@ class RuntimeEntryData:
"""Notify listeners that the Assist satellite wake word has been set."""
for callback_ in self.assist_satellite_set_wake_word_callbacks.copy():
callback_(wake_word_id)
@callback
def async_register_entity_removal_callback(
self,
info_type: type[EntityInfo],
device_id: int,
key: int,
callback_: CALLBACK_TYPE,
) -> CALLBACK_TYPE:
"""Register to receive a callback when the entity should remove itself."""
callback_key = (info_type, device_id, key)
callbacks = self.entity_removal_callbacks.setdefault(callback_key, [])
callbacks.append(callback_)
return partial(callbacks.remove, callback_)
@callback
def async_signal_entity_removal(
self, info_type: type[EntityInfo], device_id: int, key: int
) -> None:
"""Signal that an entity should remove itself."""
callback_key = (info_type, device_id, key)
for callback_ in self.entity_removal_callbacks.get(callback_key, []).copy():
callback_()

View File

@@ -588,7 +588,7 @@ class ESPHomeManager:
# Mark state as stale so that we will always dispatch
# the next state update of that type when the device reconnects
entry_data.stale_state = {
(type(entity_state), key)
(type(entity_state), entity_state.device_id, key)
for state_dict in entry_data.state.values()
for key, entity_state in state_dict.items()
}

View File

@@ -2,7 +2,7 @@
"domain": "esphome",
"name": "ESPHome",
"after_dependencies": ["hassio", "zeroconf", "tag"],
"codeowners": ["@OttoWinter", "@jesserockz", "@kbx81", "@bdraco"],
"codeowners": ["@jesserockz", "@kbx81", "@bdraco"],
"config_flow": true,
"dependencies": ["assist_pipeline", "bluetooth", "intent", "ffmpeg", "http"],
"dhcp": [
@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==33.1.1",
"aioesphomeapi==34.2.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==2.16.0"
],

View File

@@ -19,7 +19,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="chlorine",
translation_key="chlorine",
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
native_unit_of_measurement="mg/L",
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250701.0"]
"requirements": ["home-assistant-frontend==20250702.1"]
}

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