Compare commits

..

285 Commits

Author SHA1 Message Date
Paulus Schoutsen
e1383d30e7 Add websocket command to interact with chat logs 2025-10-25 22:36:31 -04:00
Joost Lekkerkerker
4c9810a10e Bump yt-dlp to 2025.10.22 (#155174) 2025-10-26 01:34:37 +03:00
johanzander
83e9fca6a2 Adds support for controlling Growatt MIN/TLX inverters through number platform and entities (#153886)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-26 00:23:52 +02:00
Matthias Alphart
fc9313f7ef Support KNX climate entity configuration from UI (#154162)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-25 23:50:14 +02:00
Matthias Alphart
278f32285a Allow KNX UI BinarySensors to disable state synchronisation (#155054) 2025-10-25 23:49:21 +02:00
Erwin Douna
8c360908ef Bump Pyportainer to 1.0.9 (#155171) 2025-10-25 23:43:30 +02:00
Simone Chemelli
82c5337fcf Bump awesomeversion to 25.8.0 (#155172) 2025-10-26 00:42:15 +03:00
Ludovic BOUÉ
7950f9ab38 Add Matter service actions for water_heater (#153577)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-25 23:14:29 +02:00
Duco Sebel
66eeb41e56 Add product name to title of HomeWizard v2 API migration repair (#155097)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-25 14:04:07 -07:00
Retha Runolfsson
1bef707cd1 Add support for switchbot climate panel (#155124) 2025-10-25 22:56:50 +02:00
Shay Levy
2125a4123d Add zones support to Shelly Irrigation controller (#152382) 2025-10-25 22:33:20 +02:00
Niracler Li
27516dee6a Add DALI Center integration (#151479)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-25 22:15:56 +02:00
G Johansson
40c9e5356e _abort_if_unique_id_configured no automatic reload in deconz (#155141) 2025-10-25 22:13:34 +02:00
hahn-th
2521920376 Bump homematicip to 2.3.1 (#155165) 2025-10-25 22:10:36 +02:00
dependabot[bot]
16eb8315ee Bump actions/upload-artifact from 4.6.2 to 5.0.0 (#155137)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-25 22:03:04 +02:00
dependabot[bot]
2c3d65b461 Bump actions/download-artifact from 5.0.0 to 6.0.0 (#155138)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-25 22:02:56 +02:00
dependabot[bot]
7e938f4f13 Bump github/codeql-action from 4.30.9 to 4.31.0 (#155139)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-25 21:56:38 +02:00
G Johansson
c5c4cf0284 Fix double reloading in axis (#155144) 2025-10-25 21:56:22 +02:00
Maciej Bieniek
68c38ac047 Improve client mock for Brother tests (#155037) 2025-10-25 21:51:36 +02:00
hanwg
f5a6fa8be1 Bump python-telegram-bot to 22.5 (#155134) 2025-10-25 21:49:47 +02:00
Maciej Bieniek
3c751918fd Catch ConnectionResetError when updating data in Cert expiry integration (#155149) 2025-10-25 21:49:09 +02:00
Erwin Douna
a3c8760b3f Portainer bump 1.0.8 (#155161) 2025-10-25 20:16:15 +02:00
Maciej Bieniek
7bceaf74be Support reconfigure flow in NextDNS integration (#154936) 2025-10-25 19:13:58 +02:00
Shai Ungar
750f06327a Bump israel-rail-api to 0.1.4 (#155153) 2025-10-25 17:03:14 +02:00
Maciej Bieniek
98bffdb9d3 Bump aioshelly to version 13.15.0 (#155150) 2025-10-25 16:20:04 +02:00
G Johansson
174b0f7c01 Use async_update_and_abort in mqtt (#155140) 2025-10-25 14:41:13 +02:00
G-Two
807edc9f47 Bump subarulink to 0.7.15 (#155121) 2025-10-25 09:04:54 +02:00
G Johansson
9b0f67fbde Recreate resolver also on DNSError in dnsip (#155120) 2025-10-25 01:00:47 +01:00
avee87
27390647ff Show current day/hour in metoffice forecasts (#152689) 2025-10-25 00:14:46 +01:00
Erwin Douna
d9f6a6bf99 Portainer bump 1.0.7 (#155111) 2025-10-24 23:12:09 +02:00
G Johansson
67be4f863f Fail creating config entry in reauth or reconfigure flows (#154035) 2025-10-24 22:12:32 +02:00
Ludovic BOUÉ
447fb68085 Add Matter OperationalError sensor (#151991)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2025-10-24 21:44:16 +02:00
Ludovic BOUÉ
750a7c9797 Add support for Matter thermostat PIHeatingDemand attribute (#154942)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2025-10-24 21:31:59 +02:00
G Johansson
3b7e1a2610 Bump holidays to 0.83 (#155107) 2025-10-24 21:18:10 +02:00
Manu
c11cacbb58 Improve migration to Uptime Kuma v2.0.0 (#155055) 2025-10-24 20:43:18 +02:00
Mike Degatano
d8d6490fb4 Add repair for deprecated addon issue (#151287) 2025-10-24 20:23:52 +02:00
Erwin Douna
2341f53ac0 Portainer bump to 1.0.6 (#155105) 2025-10-24 20:21:36 +02:00
Franck Nijhof
94e8ffadd2 Add .serena folder to gitignore (#155104)
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-24 20:06:11 +02:00
Jonathan Keslin
5186c402e7 Update hassfest for new selector translation schema (#155102) 2025-10-24 19:41:55 +02:00
David Recordon
4403447d53 Add support for climate devices (e.g. thermostats) to the Control4 component (#154502)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-24 19:41:02 +02:00
Sarah Seidman
9a8a2bfebc Bump pydroplet version to 2.3.4 (#155103) 2025-10-24 19:31:08 +02:00
Angel Nunez Mencias
a907a34f6e Bump ttn_client to 1.2.2 (#155100) 2025-10-24 18:44:59 +02:00
karwosts
03b15f1dba Log script condition warnings with the instance logger (#154966) 2025-10-24 18:42:17 +02:00
Glenn Vandeuren (aka Iondependent)
a48b915d90 Add scene platform support to Niko Home Control integration (#152712)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-24 18:02:37 +02:00
Amadeusz Wieczorek
e8227baa50 Add temperature number entity to set Tool and Bed temperatures to Octoprint (#153712)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-24 18:01:27 +02:00
MoonDevLT
4d525dee48 Add dimming functionality to the Lunatone light entity (#154508)
Co-authored-by: Dave T <17680170+davet2001@users.noreply.github.com>
2025-10-24 16:40:42 +02:00
Felipe Santos
c38e02072e Set Prettier as default formatter in VS Code for JSON and YAML (#154484) 2025-10-24 16:33:54 +02:00
hanwg
2d84bd65fe Fix send_poll action for Telegram bot (#155076) 2025-10-24 16:29:27 +02:00
Shay Levy
1d8eaeb8af Fix OpenRGB tests failing CI (#155095) 2025-10-24 16:19:45 +02:00
Paulus Schoutsen
33ed851477 Increase AI Task default tokens for Google Gemini (#155065) 2025-10-24 16:11:54 +02:00
Lorenzo Gasparini
a571271f6a Add Fing integration (#126058)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-24 15:45:00 +02:00
Joost Lekkerkerker
43445f5945 Add tests for Yardian switch (#155089) 2025-10-24 15:37:40 +02:00
Álvaro Fernández Rojas
3c098db35e Update aioairzone to v1.0.2 (#155088) 2025-10-24 15:20:21 +02:00
Aidan Timson
c72072fbfb Use icon translations for system bridge entities (#155090) 2025-10-24 13:36:05 +02:00
TheJulianJES
10e2c7ec95 Translate Z-Wave "Socket device path" in config flow (#154931) 2025-10-24 13:25:09 +02:00
Vincent Wolsink
64493ca578 Return default temp range if API responds 0 in Huum. (#153871) 2025-10-24 13:18:03 +02:00
Christopher Fenner
5f008dcae5 Correct serial number for Zigbee devices in ViCare integration (#155057)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2025-10-24 13:17:14 +02:00
MichaelMKKelly
80c06c689c Move URL out of system_bridge strings.json (#155067) 2025-10-24 13:13:10 +02:00
Brett Adams
b19070fa4b Fix history coordinator in Tesla Fleet and Teslemetry (#153068)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-10-24 13:03:24 +02:00
Retha Runolfsson
53984f7abc Bump PySwitchbot to 0.72.0 (#155073) 2025-10-24 12:57:57 +02:00
James
38620a0cda Yardian: add binary sensors (#152654)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-24 12:57:44 +02:00
Jordan Harvey
b09927bb53 Add shared BleakScanner to probe_plus (#155051) 2025-10-24 12:13:41 +02:00
Anuj Soni
11b9f7915b Moved non-translatable elements out of strings.json for nuki (#154682)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-10-24 10:40:30 +02:00
Brett Adams
3ddb520693 Bump stream to 0.7.10 in Teslemetry (#155071) 2025-10-24 08:56:36 +02:00
Bouwe Westerdijk
ffc54c699d Bump plugwise to v1.8.2 (#155072) 2025-10-24 08:56:25 +02:00
Joakim Plate
91b516d739 Respect hdmi isActiveInput for chromecast devices (#149150)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-10-24 08:55:25 +02:00
Erwin Douna
25a9948020 Portainer fix ephemeral coordinator ID (#155056) 2025-10-24 08:55:08 +02:00
Simone Chemelli
f0f420ebe2 Bump aiovodafone to 3.0.0 (#154751) 2025-10-24 08:24:58 +02:00
Petro31
312812dd8b Fix variables in icon, picture, and name for state based template entities (#154994) 2025-10-23 20:02:18 +01:00
MizterB
e0d404456b Add cavity-aware oven sensors for Whirlpool (#145145)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-23 19:03:32 +01:00
Christopher Fenner
439fc18860 Add supply temperature for FHT devices in ViCare integration (#155026) 2025-10-23 19:25:54 +02:00
NANI
774ab06206 Add energy platform to Victron Remote Monitoring (#155046) 2025-10-23 19:25:10 +02:00
Tom
f484db8f0e Bump airOS version further preparing for v6 support (#155039) 2025-10-23 19:13:41 +02:00
Christopher Fenner
4af3c4f720 Fix empty via_device in ViCare integration (#155032)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2025-10-23 17:35:42 +02:00
Erik Montnemery
a020a32d8a Remove translations from WS get_services and REST /api/services (#147120) 2025-10-23 17:26:33 +02:00
Maciej Bieniek
1ac2ae3443 Improve client mock for NextDNS tests (#155036)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-23 17:19:56 +02:00
epdevlab
2fce7db132 Add iNELS integration (#125595) 2025-10-23 16:55:29 +02:00
Heindrich Paul
6e49911e1c Bump nsapi version to 3.1.3 (#155045) 2025-10-23 16:44:55 +02:00
Andre Lengwenus
4215a16285 Add SensorDeviceClass and unit for LCN humidity sensor. (#155044) 2025-10-23 16:38:54 +02:00
Stefan Agner
65ff4fe10e Container build: Remove codenotary configuration (#155043) 2025-10-23 16:14:59 +02:00
Shay Levy
5b7675e389 Add Shelly Irrigation controller weather sensors (#155041) 2025-10-23 16:33:42 +03:00
peteS-UK
3019744035 Add exception handling for library calls in Squeezebox (#154946) 2025-10-23 15:13:22 +02:00
Maciej Bieniek
21ab630380 Update the quality scale rules list for NextDNS (#155030) 2025-10-23 12:54:20 +02:00
DeerMaximum
564ff12db0 Make NINA area filter accessible also in the config flow (#147514) 2025-10-23 11:37:22 +02:00
tronikos
6c919e698f Add sql.query action (#147260) 2025-10-23 11:08:37 +02:00
Robert Resch
5d644815fa Bump go2rtc to 1.9.11 (#155028) 2025-10-23 11:53:35 +03:00
ildar170975
8dfa0f2f65 Starline: remove device_class for fuel level (#154964) 2025-10-23 09:40:41 +02:00
Ondřej Sluka
f9484acbfa Set has_entity_name=True on Goodwe InverterSensor (#154209) 2025-10-23 08:44:56 +02:00
sairamsharan
d0c0247086 Move PS4 URLs out of translatable strings (#154969)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-10-22 23:11:29 +02:00
Johann Kellerman
b116619af1 Bump pysma to 1.0.2 and enable type checking (#154977)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-23 00:01:51 +03:00
Justus
a3d760156f Adding test for IOmeter __init__.py (#155006) 2025-10-22 23:57:58 +03:00
Felipe Santos
6e194ad6ef Bump openrgb-python to 0.3.6 (#155009) 2025-10-22 21:44:22 +01:00
Nathan Spencer
1e2a21b69f Bump pylitterbot to 2024.2.7 (#155017) 2025-10-22 21:41:09 +01:00
Ville Skyttä
e90fe96b4e huawei_lte test cleanups (#154961) 2025-10-22 22:36:48 +02:00
yohaybn
4774ed508a Add Hebrew language support to Google Generative AI TTS (#154860) 2025-10-22 20:44:53 +03:00
Ludovic BOUÉ
8f4a4d4c47 Remove UserLabelCluster from Matter mock devices fixtures (#154174)
Remove UserLabelCluster room and orientation data ONLY for mock devices fixtures.
The bad implementations have been removed/corrected in the SDK. That doesn't make sense to keep this for fake devices.
2025-10-22 18:51:12 +02:00
Marc Mueller
a83bbe2332 Update uv to 0.9.5 (#154990) 2025-10-22 17:40:12 +02:00
jvmahon
e5b93d3275 Add Matter entity labeling capabilities (#154173)
Co-authored-by: Ludovic BOUÉ <lboue@users.noreply.github.com>
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
2025-10-22 17:31:13 +02:00
Manu
1c024f58af Refactor media_player and remote platforms in Xbox integration (#154986)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-22 15:47:59 +02:00
Erwin Douna
fa86148df0 Lametric remove translatable URL (#154991) 2025-10-22 15:01:21 +02:00
Erwin Douna
7c6bbb97ea MCP remove translatable URL (#154995) 2025-10-22 14:55:25 +02:00
Nathan Spencer
a5af501da4 Bump pylitterbot to 2024.2.6 (#154898)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-22 14:43:38 +02:00
wimb0
f23cfb5594 Fix BrowseError import in yamaha_musiccast media_player.py (#154980) 2025-10-22 13:15:00 +02:00
Marc Mueller
67a12dc007 Remove async-modbus exception from hassfest requirements check (#154988) 2025-10-22 13:11:53 +02:00
Thomas D
5783b3a576 Add engine start/stop buttons to Volvo integration (#154610) 2025-10-22 10:57:15 +02:00
Guido Schmitz
7bc43039bd Remove unneded pylint disable in devolo Home Network (#154927) 2025-10-22 09:47:34 +03:00
Manu
23e2316c36 Add media_player and remote snapshot tests for Xbox integration (#154943) 2025-10-22 08:26:37 +02:00
Erwin Douna
9e9c8f5724 SMA: add sensor availability and expand tests (#154953) 2025-10-22 08:13:46 +02:00
Shay Levy
11772dbc46 Bump bthome-ble to 3.15.0 (#154956) 2025-10-22 00:10:03 +03:00
Maciej Bieniek
c12df5d776 Replace duplicate strings with translation reference keys in Shelly integration (#154940) 2025-10-21 20:23:20 +02:00
Christopher Fenner
b57ca143e6 Show underfloor heating devices in ViCare integration (#154541) 2025-10-21 18:39:27 +02:00
Erwin Douna
b3e16bd4fa Refactor the SMA integration to use a dedicated DataUpdateCoordinator (#154863) 2025-10-21 18:26:22 +02:00
Marc Mueller
18d5035877 Update syrupy to 5.0.0 (#154925) 2025-10-21 17:50:29 +02:00
Manu
d6db50fcc7 Add discovery support to Xbox integration (#154912) 2025-10-21 15:29:49 +02:00
Manu
84d9fa3bd7 Refactor coordinator data update and exception handling in Xbox integration (#154848) 2025-10-21 15:07:37 +02:00
Maciej Bieniek
b08eb3a201 Refactor NextDNS tests (#154901) 2025-10-21 14:45:10 +02:00
Marc Mueller
c74c317922 Update slixmpp to 1.12.0 (#154872) 2025-10-21 12:06:40 +02:00
Matrix
9edc6249ca YoLink remove unsupported remoters (#154918) 2025-10-21 10:18:18 +02:00
Marc Mueller
4fbcb79889 Update mcstatus to 12.0.6 (#154910) 2025-10-21 08:59:15 +02:00
hanwg
68fd5bc67e Group URL options for Telegram bot actions (#154914) 2025-10-21 08:58:51 +02:00
Thomas55555
882d047bb5 Bump aioautomower to 2.5.0 (#154900) 2025-10-21 08:52:49 +02:00
Manu
5c070c8f03 Add new entities to Xbox integration (#154911) 2025-10-21 08:51:30 +02:00
Aviad Levy
854882d612 Fix Jewish calendar month semantic to "standard order" (#154905) 2025-10-21 09:50:12 +03:00
Simone Chemelli
b078c0ee7e Use common variables in platform tests for UptimeRobot (#154909) 2025-10-21 08:12:09 +02:00
Simone Chemelli
080b16a33d Cleanup code for UptimeRobot (#154892) 2025-10-20 20:42:15 +02:00
Aviad Levy
6a1cf9827c Add month order attributes to Jewish calendar sensor (#154809) 2025-10-20 20:06:31 +03:00
Marc Mueller
23e7b14eae Update RestrictedPython to 8.1 (#154870) 2025-10-20 14:46:41 +02:00
Jordan Harvey
2a5cf83f50 Disable parallel updates for Nintendo Parental Controls (#154866) 2025-10-20 14:45:15 +02:00
Jordan Harvey
5dcb68cdf6 Add device model information for Nintendo Parental Controls (#154867) 2025-10-20 15:27:50 +03:00
Simone Chemelli
fedeca107a Bump aioamazondevices to 6.4.6 (#154865) 2025-10-20 15:20:02 +03:00
Manu
4fef19c7bc Bump bring-api to v1.1.1 (#154854) 2025-10-20 11:56:27 +02:00
dependabot[bot]
8c953b0c4e Bump github/codeql-action from 4.30.8 to 4.30.9 (#154858)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-20 09:22:32 +02:00
Kinachi249
949544874f Bump PyCync to 0.4.2 (#154856) 2025-10-20 07:02:10 +02:00
Jordan Harvey
237407010a Add number platform to nintendo_parental_controls integration (#154548) 2025-10-20 07:00:29 +02:00
Manu
64e48816c7 Rename Xbox Live to Xbox Network in NextDNS (#154855) 2025-10-20 06:55:06 +02:00
Manu
6b76b3e729 Fix typos in exception translations of Xbox integration (#154849) 2025-10-20 01:09:03 +03:00
Erwin Douna
4912280193 Portainer add endoint sensors (#154676)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-10-19 22:19:57 +02:00
Manu
d4e72ad2cf Refactor Xbox integration setup and exception handling (#154823) 2025-10-19 22:18:56 +02:00
Thomas55555
711526fc6c Remove brackets from decorator in Husqvarna Automower (#154042) 2025-10-19 22:13:20 +02:00
Felipe Santos
4be428fce7 Set Pyright level as basic by default for VS Code (#154495) 2025-10-19 22:04:01 +02:00
asafhas
ea226806a0 Tuya Alarm-Control: Ignore low-battery warnings (#152888)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-10-19 22:01:59 +02:00
Whitney Young
bc77daf2ce OpenUV: Add protection window tests (#154498) 2025-10-19 21:57:26 +02:00
Benjamin Michaelis
acead56bd5 Enhance check_config script with JSON output and fail on warnings (#152575) 2025-10-19 21:55:55 +02:00
johnmschoonover
fd08c55b79 declaraing typing fixes handling for agents (#154833) 2025-10-19 21:53:44 +02:00
cdnninja
0c342c4750 vesync show fan speed for smart tower fans (#154842) 2025-10-19 21:53:16 +02:00
Alex Hermann
da6986e58c Allow overriding recipients per message in XMPP (#149375)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-19 21:50:46 +02:00
Jan-Philipp Benecke
2f5fbc1f0e Add instance ID (mDNS) conflict detection and repair flow for zeroconf integration (#151487)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-19 21:37:10 +02:00
tronikos
e79c76cd35 Add reconfigure flow in SolarEdge (#154189) 2025-10-19 21:33:23 +02:00
Sebastian Faul
6edafd8965 Fix incorrect forward header handling (#154793) 2025-10-19 21:26:12 +02:00
Shay Levy
204ff5d45f Add valve group support (#154749)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-10-19 21:01:15 +02:00
Anuj
591eb94515 Moved non-translatable URL out of strings.json for plex (#154826) 2025-10-19 19:49:57 +02:00
Manu
0f3de627c5 Refactor sensors and binary sensors in Xbox integration (#154719) 2025-10-19 19:49:36 +02:00
Thomas55555
b2699d8a03 Bump aioautomower to v2.3.1 (#151795) 2025-10-19 19:48:42 +02:00
Markus Adrario
769a770cf1 Code quality followup to Homee stale devices (#154741)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-19 19:42:45 +02:00
Felipe Santos
2d96e8ac4d Bump OpenRGB to Silver (#154690) 2025-10-19 19:42:20 +02:00
ElectricSteve
354cacdcae Fix pterodactyl server config link (#154758) 2025-10-19 18:18:31 +02:00
Marc Mueller
d999dd05d1 Improve bluesound conftest function (#154828) 2025-10-19 18:20:16 +03:00
Marc Mueller
81572c6a84 Build aarch64 wheels on ubuntu-arm (#154819) 2025-10-19 14:15:21 +02:00
Andrew Jackson
8165ac196f Move url out of nightscout strings and change to field descriptions (#154812)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-19 13:18:49 +02:00
Chris Carini
41c95247ec Fix typo in test function name for invalid URL (#154810)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-19 13:07:59 +02:00
J. Nick Koston
2eb3360e8c Bump aioesphomeapi to 42.2.0 (#154803) 2025-10-19 13:04:23 +02:00
tronikos
fcd07902b0 Bump opower to 0.15.8 (#154811) 2025-10-19 12:55:54 +02:00
Shay Levy
71f94cad97 Fix Todoist test failure (#154808) 2025-10-19 12:54:25 +02:00
Maciej Bieniek
05277aa708 Fix Shelly enum sensors (#154814) 2025-10-19 12:51:18 +02:00
Maciej Bieniek
9f74471d22 Rename the Shelly switch from Start Charging to Charging (#154815) 2025-10-19 12:40:33 +02:00
Manu
1c8487a7e7 Fix wrong in game sensor state in Xbox integration (#154799) 2025-10-19 08:44:11 +02:00
Jesse Hills
3c8612b6fd Add responses for action calls from ESPHome devices (#153233)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-18 17:57:38 -10:00
J. Nick Koston
f28892c526 Bump aioesphomeapi to 42.1.0 (#154796)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-10-18 17:27:19 -10:00
Aarni Koskela
24b7cf261c Streamline template tests (#154586) 2025-10-19 01:24:58 +02:00
Maciej Bieniek
ef69e6d54b Improve entity names for powered by Shelly devices (#154592) 2025-10-19 00:57:48 +02:00
Marc Mueller
ca31a279fa Remove opower violation from hassfest requirements check (#154797) 2025-10-19 00:54:06 +02:00
Maciej Bieniek
e50c4c4787 Fix units for Shelly TopAC EVE01-11 sensors (#154740) 2025-10-19 00:46:57 +02:00
Marc Mueller
3ecddda8dd Build wheels for Python 3.14 (#154794) 2025-10-19 00:42:39 +02:00
G Johansson
af77f835a5 Add beufort as valid wind speed unit in weather (#153572) 2025-10-19 00:40:41 +02:00
Shay Levy
6de2016aa3 Add Demo valves with position support (#154657) 2025-10-18 23:54:02 +02:00
ehendrix23
f1e72c1616 Add streaming to Elevenlabs TTS (#154663) 2025-10-18 23:50:01 +02:00
Keith Burzinski
7af3eb638b [esphome] Implement feature_flags for climate (#153507)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-18 11:47:11 -10:00
Mick Vleeshouwer
363e5f088c Improve error message for unsupported hardware in Overkiz (#154314) 2025-10-18 23:43:49 +02:00
Manu
5b1e3ef574 Set xuid as unique_id and gamertag as title in Xbox config flow (#154693) 2025-10-18 23:25:44 +02:00
Manu
d607323731 Add support for tracking stats of party members in Habitica integration (#151885) 2025-10-18 23:24:34 +02:00
Manu
31f595a3f8 Set integration_type to service in Sleep as Android (#154765) 2025-10-18 23:11:07 +02:00
Oliver Gründel
9a27805349 Correctly calculate average color for light groups in HS Color Mode (#154678) 2025-10-18 23:04:00 +02:00
Marc Mueller
477cdbb711 Use yaml anchors in ci workflow (2) (#154680) 2025-10-18 22:45:43 +02:00
Manu
62b39fdd10 Remove unused repair string and update quality scale in Habitica integration (#154775) 2025-10-18 21:32:54 +02:00
Akanksha
f806cc8b4b Move translatable URL out of strings.json for airnow integration (#154557)
Co-authored-by: jbouwh <jan@jbsoft.nl>
2025-10-18 20:58:29 +02:00
Jan Bouwhuis
b6108001e4 Move URLs out of strings.json for auth (#154769) 2025-10-18 21:54:54 +03:00
Manu
56f33a8a5f Set integration_type to service in ntfy integration (#154767) 2025-10-18 21:53:11 +03:00
Joakim Plate
1e91ad6e23 Make sure user flow replace ignored in gardena_blueooth (#154778) 2025-10-18 21:49:26 +03:00
Joakim Plate
9032de4b26 Make sure user flow replace ignored in togrill (#154780) 2025-10-18 21:47:39 +03:00
Andrew Jackson
553fcb5156 Move url out of FreedomPro strings.json (#154786) 2025-10-18 21:46:58 +03:00
Andrew Jackson
378295e1cc Move url out of Flume strings.json (#154787) 2025-10-18 21:46:10 +03:00
Jan Bouwhuis
ff95c6235f Move URLs out of strings.json for androidtv_remote (#154739)
Co-authored-by: tronikos <tronikos@users.noreply.github.com>
2025-10-18 20:38:45 +02:00
Manu
d398a13899 Set integration_type to service in Habitica (#154763) 2025-10-18 19:38:27 +01:00
Manu
10b300e573 Set integration_type to service in Uptime Kuma integration (#154764) 2025-10-18 19:20:59 +01:00
Jan Bouwhuis
e95c0ef3a8 Move translatable URL out of strings.json for compit (#154771) 2025-10-18 18:54:27 +01:00
Joakim Plate
3b09adb360 Remove workaround in togrill to trigger coordinator (#154784) 2025-10-18 20:41:02 +03:00
Andrew Jackson
d2380608e1 Move url out of rachio strings.json (#154781) 2025-10-18 19:30:48 +02:00
Andrew Jackson
37188a0832 Move url out of motionblinds strings.json (#154777) 2025-10-18 19:29:47 +02:00
Andrew Jackson
3134fd75e8 Move url out of sensorpush_cloud strings.json (#154768) 2025-10-18 19:28:27 +02:00
Andrew Jackson
861f4a0578 Move url out of orsoenergy strings.json (#154776) 2025-10-18 19:26:32 +02:00
Andrew Jackson
a82c512472 Move url out of starline strings.json (#154773) 2025-10-18 19:25:07 +02:00
Andrew Jackson
10392d9719 Move URL out of Tomorrow.io strings.json (#154759) 2025-10-18 19:54:11 +03:00
Andrew Jackson
b7acc66153 Move url out of simplisafe strings (#154762) 2025-10-18 19:51:01 +03:00
Erwin Douna
6249cabcba Firefly III add diagnostics (#154743)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-18 18:02:50 +02:00
Andrew Jackson
84f2fd106d Move URL out of TheThingsNetwork strings.json (#154760) 2025-10-18 18:13:53 +03:00
Åke Strandberg
45cc68d3e4 Set myuplink integration_type explicitly (#154742) 2025-10-18 17:22:17 +03:00
Luke Lashley
7fd75c7742 Fix bug where Roborock loading map in cleaning causes a crash (#153011) 2025-10-18 07:08:56 -07:00
Erwin Douna
9522b11042 Portainer bump 1.0.4 (#154736) 2025-10-18 11:26:43 +02:00
Matthias Alphart
c874c4ac73 Improve KNX config-UI group address labels and descriptions (#154716) 2025-10-18 10:08:26 +02:00
Michael
907ef8fa15 Set integration type for feedreader (#154712) 2025-10-18 09:59:53 +02:00
Michael
bc93153c40 Set integration type for FRITZ!Tools (#154711) 2025-10-18 09:59:27 +02:00
Abestanis
6964829699 Add the dial action to the FRITZ!Box Tools integration (#151095)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>
2025-10-18 09:53:54 +02:00
steinmn
62e59608b0 Bump Adax-local to 0.2.0 (#154720) 2025-10-18 09:41:39 +02:00
Felipe Santos
9507b3f3aa Allow to remove OpenRGB devices that are disconnected (#154730) 2025-10-18 09:30:53 +02:00
Brett Adams
1d187abe10 Handle location scope in Tesla Fleet vehicle coordinator (#154731) 2025-10-18 09:28:14 +02:00
Ludovic BOUÉ
0464cb8929 Add Matter fixture for Six buttons Haijai Switch from DK-AI (#154734) 2025-10-18 09:12:06 +02:00
Paulus Schoutsen
f410d94f80 ESPHome to subscribe Z-Wave Proxy HOME ID changes (#154696)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-17 20:46:43 -10:00
J. Nick Koston
dee3c11203 Bump aiohttp to 3.13.1 (#154723) 2025-10-17 19:28:57 -10:00
Jordan Harvey
06e4b0a798 Bump pyprobeplus to 1.1.1 (#154523) 2025-10-18 06:45:02 +02:00
Vasil Iliev
2fd55a49cb Update whirlpool-sixth-sense to 1.0.2 (#154704) 2025-10-18 00:45:34 +01:00
Michael
80d7224dcf Set integration type for Synology DSM (#154714) 2025-10-17 23:41:01 +02:00
Michael
9d03b1b9b4 Set integration type for immich (#154710) 2025-10-17 23:25:10 +02:00
Michael
cecdf553f3 Set integration type for nextcloud (#154709) 2025-10-17 23:24:52 +02:00
Michael
54e6fbc042 Set integration type for ecovacs (#154713) 2025-10-17 23:24:04 +02:00
Michael
9c098d3471 Set integration type for tankerkoenig (#154715) 2025-10-17 23:23:33 +02:00
Christopher Fenner
394575e4f7 Fix test cases in ViCare integration (#154687) 2025-10-17 20:34:00 +02:00
Manu
effc33d0d2 Add snapshot tests for binary_sensor platform of Xbox integration (#154694) 2025-10-17 20:32:24 +02:00
Raphael Hehl
7af4c337c6 Bump uiprotect to version 7.23.0 (#154692) 2025-10-17 20:31:25 +02:00
Ludovic BOUÉ
4f222d7adf Add Matter SwitchBot K11+ fixture (#154691) 2025-10-17 20:03:31 +02:00
Ludovic BOUÉ
00f16812e4 Add Matter fixture for Silabs light switch (#154701) 2025-10-17 20:02:19 +02:00
Whitney Young
0efaf7efe8 OpenUV: Fix update by skipping when protection window is null (#154487) 2025-10-17 18:26:27 +02:00
epenet
55643f0632 Remove async_setup/async_setup_entry/async_unload_entry from __all__ (#154674) 2025-10-17 16:37:16 +02:00
Magnus
36f4723f6e Component asuswrt: Improve get_bridge parameters typing in asuswrt (#154540) 2025-10-17 16:00:56 +02:00
Alistair Francis
03bc698936 husqvarna_automower_ble: Log errors if the mower isn't pairable (#151768)
Signed-off-by: Alistair Francis <alistair@alistair23.me>
2025-10-17 15:49:53 +02:00
Manu
0c1dc73422 Add snapshot tests of sensor platform to Xbox integration (#154684) 2025-10-17 15:46:51 +02:00
Anuj Soni
c31537081b Move translatable URLs out of strings.json for tautulli (#154681) 2025-10-17 15:35:05 +02:00
epenet
d13067abb3 Remove rest from _IGNORE_ROOT_IMPORT in pylint plugin (#154662) 2025-10-17 09:21:53 -04:00
epenet
64da32b5f9 Revert "Adding __all__ export to device_tracker" (#154675) 2025-10-17 09:20:18 -04:00
Paulus Schoutsen
3990fc6ab2 LLM: skip local handling of search media query (#154496) 2025-10-17 09:18:47 -04:00
Alistair Francis
e4071bd305 Bump automower-ble to 0.2.8 (#154683)
Signed-off-by: Alistair Francis <alistair@alistair23.me>
2025-10-17 15:08:11 +02:00
Magnus
8dda26c227 Component asuswrt: Type hint for aioasuswrt returns (#154594) 2025-10-17 15:04:36 +02:00
johanzander
b182d5ce87 Add additional unit tests for Growatt Server integration (#154644) 2025-10-17 14:22:16 +02:00
Thomas55555
175365bdea Add integration_type to Husqvarna Automower (#154642) 2025-10-17 14:18:32 +02:00
Bouwe Westerdijk
cbe52cbfca Bump plugwise to v1.8.1 (#154679) 2025-10-17 15:13:35 +03:00
Felipe Santos
9251dde2c6 Add OpenRGB reconfiguration flow (#154478) 2025-10-17 12:27:11 +02:00
Andrew Jackson
24d77cc453 Bump aiomealie to 1.0.1 (#154672) 2025-10-17 12:23:55 +03:00
johanzander
a1f98abe49 Add CODEOWNERS entry for Growatt Server integration (#154647) 2025-10-17 11:20:11 +03:00
cdnninja
d25dde1d11 Bump pyvesync version to 3.1.2 (#154650)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-10-17 10:19:48 +02:00
hanwg
8ec483b38b Fix Telegram bot bug where message is sent to wrong recipient (#154658) 2025-10-17 11:15:41 +03:00
epenet
bf14caca69 Fix behavior spelling for public facing strings (#154665) 2025-10-17 11:07:05 +03:00
Ludovic BOUÉ
e5fb6b2fb2 Remove duplicated Matter powersource cluster from Mock device fixture files (#154668) 2025-10-17 11:06:01 +03:00
epenet
7dfeb3a3f6 Improve metoffice typing (#154670) 2025-10-17 10:05:27 +02:00
epenet
9d3b1562c4 Remove more components from _IGNORE_ROOT_IMPORT in pylint plugin (#154667) 2025-10-17 09:46:53 +02:00
epenet
e14407f066 Remove HomeAssistantRemoteScanner from __all__ in bluetooth (#154669) 2025-10-17 09:31:30 +02:00
epenet
67872e3746 Adjust onewire strings (#154664) 2025-10-17 09:28:37 +02:00
Manu
06bd1a2003 Migrate Xbox to runtime_data (#154652) 2025-10-17 09:25:49 +02:00
dependabot[bot]
37ea360304 Bump sigstore/cosign-installer from 3.10.0 to 4.0.0 (#154661)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-17 09:15:41 +02:00
epenet
25ce57424c Remove more components from _IGNORE_ROOT_IMPORT in pylint plugin (#154660) 2025-10-17 08:35:18 +02:00
Thomas55555
3d46ab549d Add serial number to IPP (#154648) 2025-10-16 23:58:57 +01:00
Thomas55555
567cc9f842 Bump colorlog to 6.10.1 (#154643) 2025-10-16 23:57:24 +01:00
Shay Levy
b5457a5abd Fix demo cover set position action (#154641) 2025-10-16 21:21:32 +03:00
Marc Mueller
e4b5e35d1d Update Pillow to 12.0.0 (#154637) 2025-10-16 18:25:36 +01:00
Ludovic BOUÉ
12023c33b5 Rename Mock Door Lock with unbolt fixture (#154627) 2025-10-16 13:01:46 -04:00
Jan Čermák
a28749937c Allow ignored rapt_ble devices to be set up from the user flow (#154606) 2025-10-16 12:54:24 -04:00
Jan Čermák
3fe37d651f Update Home Assistant base image to 2025.10.1 (#154609)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-16 12:53:25 -04:00
epenet
cb3424cdf0 Remove more components from _IGNORE_ROOT_IMPORT in pylint plugin (#154622) 2025-10-16 12:52:51 -04:00
Thomas D
a799f7ff91 Add service warning sensor to Volvo integration (#154613) 2025-10-16 18:52:12 +02:00
Louis Pré
34ab725b75 LLM prefix caching optimization using new GetDateTime tool (#152408)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Denis Shulyaka <Shulyaka@gmail.com>
2025-10-16 12:47:12 -04:00
Manu
2dfc7f02ba Bump habiticalib to v0.4.6 (#154566) 2025-10-16 17:15:13 +01:00
Jan Čermák
c8919222bd Mock network calls in comfoconnect tests to fix timeouts (#154620) 2025-10-16 11:42:04 -04:00
Ludovic BOUÉ
a888264d2f Add Matter fixture for Aqara Smart Lock U200 (#154623) 2025-10-16 16:25:16 +02:00
Joost Lekkerkerker
ae84c7e15d Add subentries to WAQI (#148966)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-16 14:11:52 +01:00
epenet
415c8b490b Add device diagnostics to onewire (#154617)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-16 14:56:19 +02:00
Aviad Levy
6038f15406 Add support for Telegram message attachments (#153216) 2025-10-16 14:54:50 +02:00
Justus
a8758253c4 Add config flow exceptions to IOMeter (#154604)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-16 14:52:51 +02:00
epenet
fa4eb2e820 The 1-wire integration has now reached silver on the quality scale (#154614) 2025-10-16 14:52:11 +02:00
Ludovic BOUÉ
58f35d0614 Add Matter Eve Energy 20ECN4101 fixture (#154608) 2025-10-16 14:07:29 +02:00
epenet
f72a91ca29 Remove assist_pipeline from _IGNORE_ROOT_IMPORT in pylint plugin (#154600) 2025-10-16 13:33:19 +02:00
Thomas D
5d99da6e1f The Volvo integration has now reached platinum on the quality scale (#154015)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-16 13:07:54 +02:00
Joost Lekkerkerker
64746eb99c Add new Dryer fixture to SmartThings (#154607) 2025-10-16 12:55:30 +02:00
Maciej Bieniek
70fc6df599 Make Shelly deprecated firmware issue more general (#154539) 2025-10-16 13:50:43 +03:00
epenet
8dc33ece7b Remove sensor from _IGNORE_ROOT_IMPORT in pylint plugin (#154602) 2025-10-16 11:28:29 +01:00
Carlos Gustavo Sarmiento
3d4d8e7f20 Make Speed optional for GoToPreset ONVIF command (#149636)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-16 11:21:02 +01:00
Joakim Sørensen
c92d319e12 Bump hass-nabucasa from 1.3.0 to 1.4.0 (#154599) 2025-10-16 11:18:55 +01:00
Christopher Fenner
1bdba7906a Add new sensors for Zigbee based devices in ViCare (#154271) 2025-10-16 11:11:08 +01:00
792 changed files with 57231 additions and 6513 deletions

View File

@@ -33,7 +33,7 @@
"GitHub.vscode-pull-request-github",
"GitHub.copilot"
],
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.jsonc
"settings": {
"python.experiments.optOutFrom": ["pythonTestAdapter"],
"python.defaultInterpreterPath": "/home/vscode/.local/ha-venv/bin/python",
@@ -41,6 +41,7 @@
"python.terminal.activateEnvInCurrentTerminal": true,
"python.testing.pytestArgs": ["--no-cov"],
"pylint.importStrategy": "fromEnvironment",
"python.analysis.typeCheckingMode": "basic",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
@@ -62,6 +63,9 @@
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
},
"[json][jsonc][yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"json.schemas": [
{
"fileMatch": ["homeassistant/components/*/manifest.json"],

View File

@@ -74,6 +74,7 @@ rules:
- **Formatting**: Ruff
- **Linting**: PyLint and Ruff
- **Type Checking**: MyPy
- **Lint/Type/Format Fixes**: Always prefer addressing the underlying issue (e.g., import the typed source, update shared stubs, align with Ruff expectations, or correct formatting at the source) before disabling a rule, adding `# type: ignore`, or skipping a formatter. Treat suppressions and `noqa` comments as a last resort once no compliant fix exists
- **Testing**: pytest with plain functions and fixtures
- **Language**: American English for all code, comments, and documentation (use sentence case, including titles)

View File

@@ -69,7 +69,7 @@ jobs:
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
- name: Upload translations
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: translations
path: translations.tar.gz
@@ -175,7 +175,7 @@ jobs:
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
- name: Download translations
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: translations
@@ -326,7 +326,7 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Cosign
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
with:
cosign-release: "v2.2.3"
@@ -464,7 +464,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download translations
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: translations

View File

@@ -37,7 +37,7 @@ on:
type: boolean
env:
CACHE_VERSION: 9
CACHE_VERSION: 1
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.11"
@@ -428,7 +428,7 @@ jobs:
timeout-minutes: 60
strategy:
matrix:
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
python-version: &matrix-python ${{ fromJson(needs.info.outputs.python_versions) }}
steps:
- *checkout
- &setup-python-matrix
@@ -514,9 +514,7 @@ jobs:
if: steps.cache-apt-check.outputs.cache-hit != 'true'
uses: &actions-cache-save actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
${{ env.APT_CACHE_DIR }}
${{ env.APT_LIST_CACHE_DIR }}
path: *path-apt-cache
key: *key-apt-cache
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
@@ -537,7 +535,7 @@ jobs:
python --version
uv pip freeze >> pip_freeze.txt
- name: Upload pip_freeze artifact
uses: &actions-upload-artifact actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: pip-freeze-${{ matrix.python-version }}
path: pip_freeze.txt
@@ -641,7 +639,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
python-version: *matrix-python
steps:
- *checkout
- *setup-python-matrix
@@ -689,14 +687,14 @@ jobs:
run: |
. venv/bin/activate
python --version
pylint homeassistant
pylint --ignore-missing-annotations=y homeassistant
- name: Run pylint (partially)
if: needs.info.outputs.test_full_suite == 'false'
shell: bash
run: |
. venv/bin/activate
python --version
pylint homeassistant/components/${{ needs.info.outputs.integrations_glob }}
pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.info.outputs.integrations_glob }}
pylint-tests:
name: Check pylint on tests
@@ -838,8 +836,8 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
group: ${{ fromJson(needs.info.outputs.test_groups) }}
python-version: *matrix-python
group: &matrix-group ${{ fromJson(needs.info.outputs.test_groups) }}
steps:
- *cache-restore-apt
- name: Install additional OS dependencies
@@ -869,7 +867,7 @@ jobs:
run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Download pytest_buckets
uses: &actions-download-artifact actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: &actions-download-artifact actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: pytest_buckets
- &compile-english-translations
@@ -964,7 +962,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
python-version: *matrix-python
mariadb-group: ${{ fromJson(needs.info.outputs.mariadb_groups) }}
steps:
- *cache-restore-apt
@@ -1081,7 +1079,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
python-version: *matrix-python
postgresql-group: ${{ fromJson(needs.info.outputs.postgresql_groups) }}
steps:
- *cache-restore-apt
@@ -1218,8 +1216,8 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
group: ${{ fromJson(needs.info.outputs.test_groups) }}
python-version: *matrix-python
group: *matrix-group
steps:
- *cache-restore-apt
- name: Install additional OS dependencies

View File

@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Initialize CodeQL
uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
with:
category: "/language:python"

View File

@@ -31,7 +31,8 @@ jobs:
outputs:
architectures: ${{ steps.info.outputs.architectures }}
steps:
- name: Checkout the repository
- &checkout
name: Checkout the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
@@ -91,7 +92,7 @@ jobs:
) > build_constraints.txt
- name: Upload env_file
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: env_file
path: ./.env_file
@@ -99,14 +100,14 @@ jobs:
overwrite: true
- name: Upload build_constraints
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: *actions-upload-artifact
with:
name: build_constraints
path: ./build_constraints.txt
overwrite: true
- name: Upload requirements_diff
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: *actions-upload-artifact
with:
name: requirements_diff
path: ./requirements_diff.txt
@@ -118,7 +119,7 @@ jobs:
python -m script.gen_requirements_all ci
- name: Upload requirements_all_wheels
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: *actions-upload-artifact
with:
name: requirements_all_wheels
path: ./requirements_all_wheels_*.txt
@@ -127,28 +128,41 @@ jobs:
name: Build Core wheels ${{ matrix.abi }} for ${{ matrix.arch }} (musllinux_1_2)
if: github.repository_owner == 'home-assistant'
needs: init
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
abi: ["cp313"]
matrix: &matrix-build
abi: ["cp313", "cp314"]
arch: ${{ fromJson(needs.init.outputs.architectures) }}
include:
- os: ubuntu-latest
- arch: aarch64
os: ubuntu-24.04-arm
exclude:
- abi: cp314
arch: armv7
- abi: cp314
arch: armhf
- abi: cp314
arch: i386
steps:
- name: Checkout the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- *checkout
- name: Download env_file
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
- &download-env-file
name: Download env_file
uses: &actions-download-artifact actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: env_file
- name: Download build_constraints
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
- &download-build-constraints
name: Download build_constraints
uses: *actions-download-artifact
with:
name: build_constraints
- name: Download requirements_diff
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
- &download-requirements-diff
name: Download requirements_diff
uses: *actions-download-artifact
with:
name: requirements_diff
@@ -160,7 +174,7 @@ jobs:
# home-assistant/wheels doesn't support sha pinning
- name: Build wheels
uses: home-assistant/wheels@2025.09.1
uses: &home-assistant-wheels home-assistant/wheels@2025.10.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -177,33 +191,19 @@ jobs:
name: Build wheels ${{ matrix.abi }} for ${{ matrix.arch }}
if: github.repository_owner == 'home-assistant'
needs: init
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
abi: ["cp313"]
arch: ${{ fromJson(needs.init.outputs.architectures) }}
matrix: *matrix-build
steps:
- name: Checkout the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- *checkout
- name: Download env_file
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: env_file
- name: Download build_constraints
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: build_constraints
- name: Download requirements_diff
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: requirements_diff
- *download-env-file
- *download-build-constraints
- *download-requirements-diff
- name: Download requirements_all_wheels
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: *actions-download-artifact
with:
name: requirements_all_wheels
@@ -221,7 +221,7 @@ jobs:
# home-assistant/wheels doesn't support sha pinning
- name: Build wheels
uses: home-assistant/wheels@2025.09.1
uses: *home-assistant-wheels
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2

2
.gitignore vendored
View File

@@ -111,6 +111,7 @@ virtualization/vagrant/config
!.vscode/cSpell.json
!.vscode/extensions.json
!.vscode/tasks.json
!.vscode/settings.default.jsonc
.env
# Windows Explorer
@@ -140,4 +141,5 @@ pytest_buckets.txt
# AI tooling
.claude/settings.local.json
.serena/

View File

@@ -182,7 +182,6 @@ homeassistant.components.efergy.*
homeassistant.components.eheimdigital.*
homeassistant.components.electrasmart.*
homeassistant.components.electric_kiwi.*
homeassistant.components.elevenlabs.*
homeassistant.components.elgato.*
homeassistant.components.elkm1.*
homeassistant.components.emulated_hue.*
@@ -279,6 +278,7 @@ homeassistant.components.imap.*
homeassistant.components.imgw_pib.*
homeassistant.components.immich.*
homeassistant.components.incomfort.*
homeassistant.components.inels.*
homeassistant.components.input_button.*
homeassistant.components.input_select.*
homeassistant.components.input_text.*
@@ -478,6 +478,7 @@ homeassistant.components.skybell.*
homeassistant.components.slack.*
homeassistant.components.sleep_as_android.*
homeassistant.components.sleepiq.*
homeassistant.components.sma.*
homeassistant.components.smhi.*
homeassistant.components.smlight.*
homeassistant.components.smtp.*

View File

@@ -7,13 +7,19 @@
"python.testing.pytestEnabled": false,
// https://code.visualstudio.com/docs/python/linting#_general-settings
"pylint.importStrategy": "fromEnvironment",
// Pyright is too pedantic for Home Assistant
"python.analysis.typeCheckingMode": "basic",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
},
"[json][jsonc][yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"json.schemas": [
{
"fileMatch": [
"homeassistant/components/*/manifest.json"
],
// This value differs between working with devcontainer and locally, therefor this value should NOT be in sync!
"url": "./script/json_schemas/manifest_schema.json"
}
]
{
"fileMatch": ["homeassistant/components/*/manifest.json"],
// This value differs between working with devcontainer and locally, therefore this value should NOT be in sync!
"url": "./script/json_schemas/manifest_schema.json"
}
]
}

8
CODEOWNERS generated
View File

@@ -494,6 +494,8 @@ build.json @home-assistant/supervisor
/tests/components/filesize/ @gjohansson-ST
/homeassistant/components/filter/ @dgomes
/tests/components/filter/ @dgomes
/homeassistant/components/fing/ @Lorenzo-Gasparini
/tests/components/fing/ @Lorenzo-Gasparini
/homeassistant/components/firefly_iii/ @erwindouna
/tests/components/firefly_iii/ @erwindouna
/homeassistant/components/fireservicerota/ @cyberjunky
@@ -619,6 +621,8 @@ build.json @home-assistant/supervisor
/tests/components/greeneye_monitor/ @jkeljo
/homeassistant/components/group/ @home-assistant/core
/tests/components/group/ @home-assistant/core
/homeassistant/components/growatt_server/ @johanzander
/tests/components/growatt_server/ @johanzander
/homeassistant/components/guardian/ @bachya
/tests/components/guardian/ @bachya
/homeassistant/components/habitica/ @tr4nt0r
@@ -739,6 +743,8 @@ build.json @home-assistant/supervisor
/tests/components/improv_ble/ @emontnemery
/homeassistant/components/incomfort/ @jbouwh
/tests/components/incomfort/ @jbouwh
/homeassistant/components/inels/ @epdevlab
/tests/components/inels/ @epdevlab
/homeassistant/components/influxdb/ @mdegat01
/tests/components/influxdb/ @mdegat01
/homeassistant/components/inkbird/ @bdraco
@@ -1537,6 +1543,8 @@ build.json @home-assistant/supervisor
/tests/components/suez_water/ @ooii @jb101010-2
/homeassistant/components/sun/ @home-assistant/core
/tests/components/sun/ @home-assistant/core
/homeassistant/components/sunricher_dali_center/ @niracler
/tests/components/sunricher_dali_center/ @niracler
/homeassistant/components/supla/ @mwegrzynek
/homeassistant/components/surepetcare/ @benleb @danielhiversen
/tests/components/surepetcare/ @benleb @danielhiversen

4
Dockerfile generated
View File

@@ -25,13 +25,13 @@ RUN \
"armv7") go2rtc_suffix='arm' ;; \
*) go2rtc_suffix=${BUILD_ARCH} ;; \
esac \
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.9/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.11/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
&& chmod +x /bin/go2rtc \
# Verify go2rtc can be executed
&& go2rtc --version
# Install uv
RUN pip3 install uv==0.8.9
RUN pip3 install uv==0.9.5
WORKDIR /usr/src

View File

@@ -1,13 +1,10 @@
image: ghcr.io/home-assistant/{arch}-homeassistant
build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.0
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.1
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.1
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.1
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.1
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.1
cosign:
base_identity: https://github.com/home-assistant/docker/.*
identity: https://github.com/home-assistant/core/.*

View File

@@ -34,6 +34,9 @@ INPUT_FIELD_CODE = "code"
DUMMY_SECRET = "FPPTH34D4E3MI2HG"
GOOGLE_AUTHENTICATOR_URL = "https://support.google.com/accounts/answer/1066447"
AUTHY_URL = "https://authy.com/"
def _generate_qr_code(data: str) -> str:
"""Generate a base64 PNG string represent QR Code image of data."""
@@ -229,6 +232,8 @@ class TotpSetupFlow(SetupFlow[TotpAuthModule]):
"code": self._ota_secret,
"url": self._url,
"qr_code": self._image,
"google_authenticator_url": GOOGLE_AUTHENTICATOR_URL,
"authy_url": AUTHY_URL,
},
errors=errors,
)

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/adax",
"iot_class": "local_polling",
"loggers": ["adax", "adax_local"],
"requirements": ["adax==0.4.0", "Adax-local==0.1.5"]
"requirements": ["adax==0.4.0", "Adax-local==0.2.0"]
}

View File

@@ -53,9 +53,6 @@ __all__ = [
"GenImageTaskResult",
"async_generate_data",
"async_generate_image",
"async_setup",
"async_setup_entry",
"async_unload_entry",
]
_LOGGER = logging.getLogger(__name__)

View File

@@ -26,6 +26,10 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
# Documentation URL for API key generation
_API_KEY_URL = "https://docs.airnowapi.org/account/request/"
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool:
"""Validate the user input allows us to connect.
@@ -114,6 +118,7 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
),
}
),
description_placeholders={"api_key_url": _API_KEY_URL},
errors=errors,
)

View File

@@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"description": "To generate API key go to https://docs.airnowapi.org/account/request/",
"description": "To generate API key go to {api_key_url}",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"latitude": "[%key:common::config_flow::data::latitude%]",

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["airos==0.5.6"]
"requirements": ["airos==0.6.0"]
}

View File

@@ -29,7 +29,7 @@
},
"data_description": {
"return_average": "air-Q allows to poll both the noisy sensor readings as well as the values averaged on the device (default)",
"clip_negatives": "For baseline calibration purposes, certain sensor values may briefly become negative. The default behaviour is to clip such values to 0"
"clip_negatives": "For baseline calibration purposes, certain sensor values may briefly become negative. The default behavior is to clip such values to 0"
}
}
}

View File

@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==1.0.1"]
"requirements": ["aioairzone==1.0.2"]
}

View File

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

View File

@@ -41,6 +41,11 @@ APPS_NEW_ID = "add_new"
CONF_APP_DELETE = "app_delete"
CONF_APP_ID = "app_id"
_EXAMPLE_APP_ID = "com.plexapp.android"
_EXAMPLE_APP_PLAY_STORE_URL = (
f"https://play.google.com/store/apps/details?id={_EXAMPLE_APP_ID}"
)
STEP_PAIR_DATA_SCHEMA = vol.Schema(
{
vol.Required("pin"): str,
@@ -355,5 +360,7 @@ class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithReload):
data_schema=data_schema,
description_placeholders={
"app_id": f"`{app_id}`" if app_id != APPS_NEW_ID else "",
"example_app_id": _EXAMPLE_APP_ID,
"example_app_play_store_url": _EXAMPLE_APP_PLAY_STORE_URL,
},
)

View File

@@ -75,7 +75,7 @@
},
"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_id": "E.g. {example_app_id} for {example_app_play_store_url}",
"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

@@ -41,6 +41,8 @@ from .pipeline import (
async_setup_pipeline_store,
async_update_pipeline,
)
from .select import AssistPipelineSelect, VadSensitivitySelect
from .vad import VadSensitivity
from .websocket_api import async_register_websocket_api
__all__ = (
@@ -51,16 +53,18 @@ __all__ = (
"SAMPLE_CHANNELS",
"SAMPLE_RATE",
"SAMPLE_WIDTH",
"AssistPipelineSelect",
"AudioSettings",
"Pipeline",
"PipelineEvent",
"PipelineEventType",
"PipelineNotFound",
"VadSensitivity",
"VadSensitivitySelect",
"WakeWordSettings",
"async_create_default_pipeline",
"async_get_pipelines",
"async_pipeline_from_audio_stream",
"async_setup",
"async_update_pipeline",
)

View File

@@ -19,7 +19,14 @@ import wave
import hass_nabucasa
import voluptuous as vol
from homeassistant.components import conversation, stt, tts, wake_word, websocket_api
from homeassistant.components import (
conversation,
media_player,
stt,
tts,
wake_word,
websocket_api,
)
from homeassistant.const import ATTR_SUPPORTED_FEATURES, MATCH_ALL
from homeassistant.core import Context, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
@@ -130,7 +137,10 @@ SAVE_DELAY = 10
@callback
def _async_local_fallback_intent_filter(result: RecognizeResult) -> bool:
"""Filter out intents that are not local fallback."""
return result.intent.name in (intent.INTENT_GET_STATE)
return result.intent.name in (
intent.INTENT_GET_STATE,
media_player.INTENT_MEDIA_SEARCH_AND_PLAY,
)
@callback

View File

@@ -72,7 +72,16 @@ class WrtDevice(NamedTuple):
_LOGGER = logging.getLogger(__name__)
type _FuncType[_T] = Callable[[_T], Awaitable[list[Any] | tuple[Any] | dict[str, Any]]]
type _FuncType[_T] = Callable[
[_T],
Awaitable[
list[str]
| tuple[float | None, float | None]
| list[float]
| dict[str, float | str | None]
| dict[str, float]
],
]
type _ReturnFuncType[_T] = Callable[[_T], Coroutine[Any, Any, dict[str, Any]]]
@@ -87,7 +96,9 @@ def handle_errors_and_zip[_AsusWrtBridgeT: AsusWrtBridge](
"""Run library methods and zip results or manage exceptions."""
@functools.wraps(func)
async def _wrapper(self: _AsusWrtBridgeT) -> dict[str, str]:
async def _wrapper(
self: _AsusWrtBridgeT,
) -> dict[str, float | str | None] | dict[str, float]:
try:
data = await func(self)
except exceptions as exc:
@@ -114,7 +125,9 @@ class AsusWrtBridge(ABC):
@staticmethod
def get_bridge(
hass: HomeAssistant, conf: dict[str, Any], options: dict[str, Any] | None = None
hass: HomeAssistant,
conf: dict[str, str | int],
options: dict[str, str | bool | int] | None = None,
) -> AsusWrtBridge:
"""Get Bridge instance."""
if conf[CONF_PROTOCOL] in (PROTOCOL_HTTPS, PROTOCOL_HTTP):
@@ -313,22 +326,22 @@ class AsusWrtLegacyBridge(AsusWrtBridge):
return [SENSORS_TEMPERATURES_LEGACY[i] for i in range(3) if availability[i]]
@handle_errors_and_zip((IndexError, OSError, ValueError), SENSORS_BYTES)
async def _get_bytes(self) -> Any:
async def _get_bytes(self) -> tuple[float | None, float | None]:
"""Fetch byte information from the router."""
return await self._api.async_get_bytes_total()
@handle_errors_and_zip((IndexError, OSError, ValueError), SENSORS_RATES)
async def _get_rates(self) -> Any:
async def _get_rates(self) -> tuple[float, float]:
"""Fetch rates information from the router."""
return await self._api.async_get_current_transfer_rates()
@handle_errors_and_zip((IndexError, OSError, ValueError), SENSORS_LOAD_AVG)
async def _get_load_avg(self) -> Any:
async def _get_load_avg(self) -> list[float]:
"""Fetch load average information from the router."""
return await self._api.async_get_loadavg()
@handle_errors_and_zip((OSError, ValueError), None)
async def _get_temperatures(self) -> Any:
async def _get_temperatures(self) -> dict[str, float]:
"""Fetch temperatures information from the router."""
return await self._api.async_get_temperature()

View File

@@ -175,12 +175,12 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN):
)
async def _async_check_connection(
self, user_input: dict[str, Any]
self, user_input: dict[str, str | int]
) -> tuple[str, str | None]:
"""Attempt to connect the AsusWrt router."""
api: AsusWrtBridge
host: str = user_input[CONF_HOST]
host = user_input[CONF_HOST]
protocol = user_input[CONF_PROTOCOL]
error: str | None = None

View File

@@ -176,7 +176,7 @@ class AsusWrtRouter:
self._on_close: list[Callable] = []
self._options: dict[str, Any] = {
self._options: dict[str, str | bool | int] = {
CONF_DNSMASQ: DEFAULT_DNSMASQ,
CONF_INTERFACE: DEFAULT_INTERFACE,
CONF_REQUIRE_IP: True,
@@ -299,12 +299,10 @@ class AsusWrtRouter:
_LOGGER.warning("Reconnected to ASUS router %s", self.host)
self._connected_devices = len(wrt_devices)
consider_home: int = self._options.get(
CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds()
)
track_unknown: bool = self._options.get(
CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN
consider_home = int(
self._options.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds())
)
track_unknown = self._options.get(CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN)
for device_mac, device in self._devices.items():
dev_info = wrt_devices.pop(device_mac, None)

View File

@@ -5,7 +5,7 @@
"step": {
"init": {
"title": "Set up two-factor authentication using TOTP",
"description": "To activate two-factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six-digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**."
"description": "To activate two-factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator]({google_authenticator_url}) or [Authy]({authy_url}).\n\n{qr_code}\n\nAfter scanning the code, enter the six-digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**."
}
},
"error": {

View File

@@ -109,12 +109,12 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
if self.source == SOURCE_REAUTH:
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
return self.async_update_and_abort(
self._get_reauth_entry(), data_updates=config
)
if self.source == SOURCE_RECONFIGURE:
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
return self.async_update_and_abort(
self._get_reconfigure_entry(), data_updates=config
)
self._abort_if_unique_id_configured()
@@ -248,7 +248,7 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(discovery_info[CONF_MAC])
self._abort_if_unique_id_configured(
updates={CONF_HOST: discovery_info[CONF_HOST]}
updates={CONF_HOST: discovery_info[CONF_HOST]}, reload_on_update=False
)
self.context.update(

View File

@@ -146,7 +146,7 @@
},
"state": {
"title": "Add a Bayesian sensor",
"description": "Add an observation which evaluates to `True` when the value of the sensor exactly matches *'To state'*. When `False`, it will update the prior with probabilities that are the inverse of those set below. This behaviour can be overridden by adding observations for the same entity's other states.",
"description": "Add an observation which evaluates to `True` when the value of the sensor exactly matches *'To state'*. When `False`, it will update the prior with probabilities that are the inverse of those set below. This behavior can be overridden by adding observations for the same entity's other states.",
"data": {
"name": "[%key:common::config_flow::data::name%]",

View File

@@ -113,7 +113,6 @@ __all__ = [
"BluetoothServiceInfo",
"BluetoothServiceInfoBleak",
"HaBluetoothConnector",
"HomeAssistantRemoteScanner",
"async_address_present",
"async_ble_device_from_address",
"async_clear_address_from_match_history",

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["bring_api"],
"quality_scale": "platinum",
"requirements": ["bring-api==1.1.0"]
"requirements": ["bring-api==1.1.1"]
}

View File

@@ -20,5 +20,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bthome",
"iot_class": "local_push",
"requirements": ["bthome-ble==3.14.2"]
"requirements": ["bthome-ble==3.15.0"]
}

View File

@@ -74,7 +74,10 @@ from .const import (
StreamType,
)
from .helper import get_camera_from_entity_id
from .img_util import scale_jpeg_camera_image
from .img_util import (
TurboJPEGSingleton, # noqa: F401
scale_jpeg_camera_image,
)
from .prefs import (
CameraPreferences,
DynamicStreamSettings, # noqa: F401

View File

@@ -816,13 +816,20 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
return MediaPlayerState.PAUSED
if media_status.player_is_idle:
return MediaPlayerState.IDLE
if self.app_id is not None and self.app_id != pychromecast.IDLE_APP_ID:
if self.app_id in APP_IDS_UNRELIABLE_MEDIA_INFO:
# Some apps don't report media status, show the player as playing
return MediaPlayerState.PLAYING
return MediaPlayerState.IDLE
if self._chromecast is not None and self._chromecast.is_idle:
# If library consider us idle, that is our off state
# it takes HDMI status into account for cast devices.
return MediaPlayerState.OFF
if self.app_id in APP_IDS_UNRELIABLE_MEDIA_INFO:
# Some apps don't report media status, show the player as playing
return MediaPlayerState.PLAYING
if self.app_id is not None:
# We have an active app
return MediaPlayerState.IDLE
return None
@property

View File

@@ -14,6 +14,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT
from .const import DEFAULT_PORT, DOMAIN
from .errors import (
ConnectionRefused,
ConnectionReset,
ConnectionTimeout,
ResolveFailed,
ValidationFailure,
@@ -49,6 +50,8 @@ class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN):
self._errors[CONF_HOST] = "connection_timeout"
except ConnectionRefused:
self._errors[CONF_HOST] = "connection_refused"
except ConnectionReset:
self._errors[CONF_HOST] = "connection_reset"
except ValidationFailure:
return True
else:

View File

@@ -25,3 +25,7 @@ class ConnectionTimeout(TemporaryFailure):
class ConnectionRefused(TemporaryFailure):
"""Network connection refused."""
class ConnectionReset(TemporaryFailure):
"""Network connection reset."""

View File

@@ -13,6 +13,7 @@ from homeassistant.util.ssl import get_default_context
from .const import TIMEOUT
from .errors import (
ConnectionRefused,
ConnectionReset,
ConnectionTimeout,
ResolveFailed,
ValidationFailure,
@@ -58,6 +59,8 @@ async def get_cert_expiry_timestamp(
raise ConnectionRefused(
f"Connection refused by server: {hostname}:{port}"
) from err
except ConnectionResetError as err:
raise ConnectionReset(f"Connection reset by server: {hostname}:{port}") from err
except ssl.CertificateError as err:
raise ValidationFailure(err.verify_message) from err
except ssl.SSLError as err:

View File

@@ -14,7 +14,8 @@
"error": {
"resolve_failed": "This host cannot be resolved",
"connection_timeout": "Timeout when connecting to this host",
"connection_refused": "Connection refused when connecting to host"
"connection_refused": "Connection refused when connecting to host",
"connection_reset": "Connection reset when connecting to host"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",

View File

@@ -19,7 +19,7 @@ from homeassistant.components.alexa import (
errors as alexa_errors,
smart_home as alexa_smart_home,
)
from homeassistant.components.camera.webrtc import async_register_ice_servers
from homeassistant.components.camera import async_register_ice_servers
from homeassistant.components.google_assistant import smart_home as ga
from homeassistant.const import __version__ as HA_VERSION
from homeassistant.core import Context, HassJob, HomeAssistant, callback

View File

@@ -12,7 +12,9 @@ from hass_nabucasa.google_report_state import ErrorResponse
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.google_assistant import DOMAIN as GOOGLE_DOMAIN
from homeassistant.components.google_assistant.helpers import AbstractConfig
from homeassistant.components.google_assistant.helpers import ( # pylint: disable=hass-component-root-import
AbstractConfig,
)
from homeassistant.components.homeassistant.exposed_entities import (
async_expose_entity,
async_get_assistant_settings,

View File

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

View File

@@ -11,7 +11,7 @@ from hass_nabucasa.voice import MAP_VOICE, Gender
from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.auth.models import User
from homeassistant.components import webhook
from homeassistant.components.google_assistant.http import (
from homeassistant.components.google_assistant.http import ( # pylint: disable=hass-component-root-import
async_get_users as async_get_google_assistant_users,
)
from homeassistant.core import HomeAssistant, callback

View File

@@ -78,7 +78,10 @@ class CompitConfigFlow(ConfigFlow, domain=DOMAIN):
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
description_placeholders={"compit_url": "https://inext.compit.pl/"},
)
async def async_step_reauth(self, data: Mapping[str, Any]) -> ConfigFlowResult:

View File

@@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"description": "Please enter your https://inext.compit.pl/ credentials.",
"description": "Please enter your {compit_url} credentials.",
"title": "Connect to Compit iNext",
"data": {
"email": "[%key:common::config_flow::data::email%]",

View File

@@ -6,7 +6,9 @@ from typing import Any
import uuid
from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN
from homeassistant.components.automation.config import async_validate_config_item
from homeassistant.components.automation.config import ( # pylint: disable=hass-component-root-import
async_validate_config_item,
)
from homeassistant.config import AUTOMATION_CONFIG_PATH
from homeassistant.const import CONF_ID, SERVICE_RELOAD
from homeassistant.core import HomeAssistant, callback

View File

@@ -5,7 +5,9 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.script import DOMAIN as SCRIPT_DOMAIN
from homeassistant.components.script.config import async_validate_config_item
from homeassistant.components.script.config import ( # pylint: disable=hass-component-root-import
async_validate_config_item,
)
from homeassistant.config import SCRIPT_CONFIG_PATH
from homeassistant.const import SERVICE_RELOAD
from homeassistant.core import HomeAssistant, callback

View File

@@ -34,7 +34,7 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.LIGHT, Platform.MEDIA_PLAYER]
PLATFORMS = [Platform.CLIMATE, Platform.LIGHT, Platform.MEDIA_PLAYER]
@dataclass

View File

@@ -0,0 +1,301 @@
"""Platform for Control4 Climate/Thermostat."""
from __future__ import annotations
from datetime import timedelta
import logging
from typing import Any
from pyControl4.climate import C4Climate
from pyControl4.error_handling import C4Exception
from homeassistant.components.climate import (
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
ClimateEntity,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from . import Control4ConfigEntry, Control4RuntimeData, get_items_of_category
from .const import CONTROL4_ENTITY_TYPE
from .director_utils import update_variables_for_config_entry
from .entity import Control4Entity
_LOGGER = logging.getLogger(__name__)
CONTROL4_CATEGORY = "comfort"
# Control4 variable names
CONTROL4_HVAC_STATE = "HVAC_STATE"
CONTROL4_HVAC_MODE = "HVAC_MODE"
CONTROL4_CURRENT_TEMPERATURE = "TEMPERATURE_F"
CONTROL4_HUMIDITY = "HUMIDITY"
CONTROL4_COOL_SETPOINT = "COOL_SETPOINT_F"
CONTROL4_HEAT_SETPOINT = "HEAT_SETPOINT_F"
VARIABLES_OF_INTEREST = {
CONTROL4_HVAC_STATE,
CONTROL4_HVAC_MODE,
CONTROL4_CURRENT_TEMPERATURE,
CONTROL4_HUMIDITY,
CONTROL4_COOL_SETPOINT,
CONTROL4_HEAT_SETPOINT,
}
# Map Control4 HVAC modes to Home Assistant
C4_TO_HA_HVAC_MODE = {
"Off": HVACMode.OFF,
"Cool": HVACMode.COOL,
"Heat": HVACMode.HEAT,
"Auto": HVACMode.HEAT_COOL,
}
HA_TO_C4_HVAC_MODE = {v: k for k, v in C4_TO_HA_HVAC_MODE.items()}
# Map Control4 HVAC state to Home Assistant HVAC action
C4_TO_HA_HVAC_ACTION = {
"heating": HVACAction.HEATING,
"cooling": HVACAction.COOLING,
"idle": HVACAction.IDLE,
"off": HVACAction.OFF,
}
async def async_setup_entry(
hass: HomeAssistant,
entry: Control4ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Control4 thermostats from a config entry."""
runtime_data = entry.runtime_data
async def async_update_data() -> dict[int, dict[str, Any]]:
"""Fetch data from Control4 director for thermostats."""
try:
return await update_variables_for_config_entry(
hass, entry, VARIABLES_OF_INTEREST
)
except C4Exception as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
coordinator = DataUpdateCoordinator[dict[int, dict[str, Any]]](
hass,
_LOGGER,
name="climate",
update_method=async_update_data,
update_interval=timedelta(seconds=runtime_data.scan_interval),
config_entry=entry,
)
# Fetch initial data so we have data when entities subscribe
await coordinator.async_refresh()
items_of_category = await get_items_of_category(hass, entry, CONTROL4_CATEGORY)
entity_list = []
for item in items_of_category:
try:
if item["type"] == CONTROL4_ENTITY_TYPE:
item_name = item["name"]
item_id = item["id"]
item_parent_id = item["parentId"]
item_manufacturer = None
item_device_name = None
item_model = None
for parent_item in items_of_category:
if parent_item["id"] == item_parent_id:
item_manufacturer = parent_item.get("manufacturer")
item_device_name = parent_item.get("roomName")
item_model = parent_item.get("model")
else:
continue
except KeyError:
_LOGGER.exception(
"Unknown device properties received from Control4: %s",
item,
)
continue
# Skip if we don't have data for this thermostat
if item_id not in coordinator.data:
_LOGGER.warning(
"Couldn't get climate state data for %s (ID: %s), skipping setup",
item_name,
item_id,
)
continue
entity_list.append(
Control4Climate(
runtime_data,
coordinator,
item_name,
item_id,
item_device_name,
item_manufacturer,
item_model,
item_parent_id,
)
)
async_add_entities(entity_list)
class Control4Climate(Control4Entity, ClimateEntity):
"""Control4 climate entity."""
_attr_has_entity_name = True
_attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
| ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TURN_OFF
)
_attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL, HVACMode.HEAT_COOL]
def __init__(
self,
runtime_data: Control4RuntimeData,
coordinator: DataUpdateCoordinator[dict[int, dict[str, Any]]],
name: str,
idx: int,
device_name: str | None,
device_manufacturer: str | None,
device_model: str | None,
device_id: int,
) -> None:
"""Initialize Control4 climate entity."""
super().__init__(
runtime_data,
coordinator,
name,
idx,
device_name,
device_manufacturer,
device_model,
device_id,
)
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self._thermostat_data is not None
def _create_api_object(self) -> C4Climate:
"""Create a pyControl4 device object.
This exists so the director token used is always the latest one, without needing to re-init the entire entity.
"""
return C4Climate(self.runtime_data.director, self._idx)
@property
def _thermostat_data(self) -> dict[str, Any] | None:
"""Return the thermostat data from the coordinator."""
return self.coordinator.data.get(self._idx)
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
data = self._thermostat_data
if data is None:
return None
return data.get(CONTROL4_CURRENT_TEMPERATURE)
@property
def current_humidity(self) -> int | None:
"""Return the current humidity."""
data = self._thermostat_data
if data is None:
return None
humidity = data.get(CONTROL4_HUMIDITY)
return int(humidity) if humidity is not None else None
@property
def hvac_mode(self) -> HVACMode:
"""Return current HVAC mode."""
data = self._thermostat_data
if data is None:
return HVACMode.OFF
c4_mode = data.get(CONTROL4_HVAC_MODE) or ""
return C4_TO_HA_HVAC_MODE.get(c4_mode, HVACMode.OFF)
@property
def hvac_action(self) -> HVACAction | None:
"""Return current HVAC action."""
data = self._thermostat_data
if data is None:
return None
c4_state = data.get(CONTROL4_HVAC_STATE)
if c4_state is None:
return None
# Convert state to lowercase for mapping
return C4_TO_HA_HVAC_ACTION.get(str(c4_state).lower())
@property
def target_temperature(self) -> float | None:
"""Return the target temperature."""
data = self._thermostat_data
if data is None:
return None
hvac_mode = self.hvac_mode
if hvac_mode == HVACMode.COOL:
return data.get(CONTROL4_COOL_SETPOINT)
if hvac_mode == HVACMode.HEAT:
return data.get(CONTROL4_HEAT_SETPOINT)
return None
@property
def target_temperature_high(self) -> float | None:
"""Return the high target temperature for auto mode."""
data = self._thermostat_data
if data is None:
return None
if self.hvac_mode == HVACMode.HEAT_COOL:
return data.get(CONTROL4_COOL_SETPOINT)
return None
@property
def target_temperature_low(self) -> float | None:
"""Return the low target temperature for auto mode."""
data = self._thermostat_data
if data is None:
return None
if self.hvac_mode == HVACMode.HEAT_COOL:
return data.get(CONTROL4_HEAT_SETPOINT)
return None
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target HVAC mode."""
c4_hvac_mode = HA_TO_C4_HVAC_MODE[hvac_mode]
c4_climate = self._create_api_object()
await c4_climate.setHvacMode(c4_hvac_mode)
await self.coordinator.async_request_refresh()
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
c4_climate = self._create_api_object()
low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW)
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
temp = kwargs.get(ATTR_TEMPERATURE)
# Handle temperature range for auto mode
if self.hvac_mode == HVACMode.HEAT_COOL:
if low_temp is not None:
await c4_climate.setHeatSetpointF(low_temp)
if high_temp is not None:
await c4_climate.setCoolSetpointF(high_temp)
# Handle single temperature setpoint
elif temp is not None:
if self.hvac_mode == HVACMode.COOL:
await c4_climate.setCoolSetpointF(temp)
elif self.hvac_mode == HVACMode.HEAT:
await c4_climate.setHeatSetpointF(temp)
await self.coordinator.async_request_refresh()

View File

@@ -87,7 +87,6 @@ __all__ = [
"async_get_chat_log",
"async_get_result_from_chat_log",
"async_set_agent",
"async_setup",
"async_unset_agent",
]

View File

@@ -20,10 +20,13 @@ from homeassistant.util.hass_dict import HassKey
from homeassistant.util.json import JsonObjectType
from . import trace
from .const import ChatLogEventType
from .models import ConversationInput, ConversationResult
DATA_CHAT_LOGS: HassKey[dict[str, ChatLog]] = HassKey("conversation_chat_logs")
SUBSCRIPTIONS: HassKey[list[Callable[[ChatLogEventType, dict[str, Any]], None]]] = (
HassKey("conversation_chat_log_subscriptions")
)
LOGGER = logging.getLogger(__name__)
current_chat_log: ContextVar[ChatLog | None] = ContextVar(
@@ -31,6 +34,37 @@ current_chat_log: ContextVar[ChatLog | None] = ContextVar(
)
@callback
def async_subscribe_chat_logs(
hass: HomeAssistant,
callback_func: Callable[[ChatLogEventType, dict[str, Any]], None],
) -> Callable[[], None]:
"""Subscribe to all chat logs."""
subscriptions = hass.data.get(SUBSCRIPTIONS)
if subscriptions is None:
subscriptions = []
hass.data[SUBSCRIPTIONS] = subscriptions
subscriptions.append(callback_func)
@callback
def unsubscribe() -> None:
"""Unsubscribe from chat logs."""
subscriptions.remove(callback_func)
return unsubscribe
@callback
def _async_notify_subscribers(
hass: HomeAssistant, event_type: ChatLogEventType, data: dict[str, Any]
) -> None:
"""Notify subscribers of a chat log event."""
if subscriptions := hass.data.get(SUBSCRIPTIONS):
for callback_func in subscriptions:
callback_func(event_type, data)
@contextmanager
def async_get_chat_log(
hass: HomeAssistant,
@@ -63,6 +97,8 @@ def async_get_chat_log(
all_chat_logs = {}
hass.data[DATA_CHAT_LOGS] = all_chat_logs
is_new_log = session.conversation_id not in all_chat_logs
if chat_log := all_chat_logs.get(session.conversation_id):
chat_log = replace(chat_log, content=chat_log.content.copy())
else:
@@ -71,6 +107,12 @@ def async_get_chat_log(
if chat_log_delta_listener:
chat_log.delta_listener = chat_log_delta_listener
# Fire CREATED event for new chat logs before any content is added
if is_new_log:
_async_notify_subscribers(
hass, ChatLogEventType.CREATED, {"chat_log": chat_log.as_dict()}
)
if user_input is not None:
chat_log.async_add_user_content(UserContent(content=user_input.text))
@@ -84,14 +126,26 @@ def async_get_chat_log(
LOGGER.debug(
"Chat Log opened but no assistant message was added, ignoring update"
)
# If this was a new log but nothing was added, fire DELETED to clean up
if is_new_log:
_async_notify_subscribers(
hass,
ChatLogEventType.DELETED,
{"conversation_id": session.conversation_id},
)
return
if session.conversation_id not in all_chat_logs:
if is_new_log:
@callback
def do_cleanup() -> None:
"""Handle cleanup."""
all_chat_logs.pop(session.conversation_id)
_async_notify_subscribers(
hass,
ChatLogEventType.DELETED,
{"conversation_id": session.conversation_id},
)
session.async_on_cleanup(do_cleanup)
@@ -100,6 +154,13 @@ def async_get_chat_log(
all_chat_logs[session.conversation_id] = chat_log
# For new logs, CREATED was already fired before content was added
# For existing logs, fire UPDATED
if not is_new_log:
_async_notify_subscribers(
hass, ChatLogEventType.UPDATED, {"chat_log": chat_log.as_dict()}
)
class ConverseError(HomeAssistantError):
"""Error during initialization of conversation.
@@ -130,6 +191,10 @@ class SystemContent:
role: Literal["system"] = field(init=False, default="system")
content: str
def as_dict(self) -> dict[str, Any]:
"""Return a dictionary representation of the content."""
return {"role": self.role, "content": self.content}
@dataclass(frozen=True)
class UserContent:
@@ -139,6 +204,15 @@ class UserContent:
content: str
attachments: list[Attachment] | None = field(default=None)
def as_dict(self) -> dict[str, Any]:
"""Return a dictionary representation of the content."""
result: dict[str, Any] = {"role": self.role, "content": self.content}
if self.attachments:
result["attachments"] = [
attachment.as_dict() for attachment in self.attachments
]
return result
@dataclass(frozen=True)
class Attachment:
@@ -153,6 +227,14 @@ class Attachment:
path: Path
"""Path to the attachment on disk."""
def as_dict(self) -> dict[str, Any]:
"""Return a dictionary representation of the attachment."""
return {
"media_content_id": self.media_content_id,
"mime_type": self.mime_type,
"path": str(self.path),
}
@dataclass(frozen=True)
class AssistantContent:
@@ -165,6 +247,17 @@ class AssistantContent:
tool_calls: list[llm.ToolInput] | None = None
native: Any = None
def as_dict(self) -> dict[str, Any]:
"""Return a dictionary representation of the content."""
result: dict[str, Any] = {"role": self.role, "agent_id": self.agent_id}
if self.content:
result["content"] = self.content
if self.thinking_content:
result["thinking_content"] = self.thinking_content
if self.tool_calls:
result["tool_calls"] = self.tool_calls
return result
@dataclass(frozen=True)
class ToolResultContent:
@@ -176,6 +269,16 @@ class ToolResultContent:
tool_name: str
tool_result: JsonObjectType
def as_dict(self) -> dict[str, Any]:
"""Return a dictionary representation of the content."""
return {
"role": self.role,
"agent_id": self.agent_id,
"tool_call_id": self.tool_call_id,
"tool_name": self.tool_name,
"tool_result": self.tool_result,
}
type Content = SystemContent | UserContent | AssistantContent | ToolResultContent
@@ -211,6 +314,13 @@ class ChatLog:
delta_listener: Callable[[ChatLog, dict], None] | None = None
llm_input_provided_index = 0
def as_dict(self) -> dict[str, Any]:
"""Return a dictionary representation of the chat log."""
return {
"conversation_id": self.conversation_id,
"continue_conversation": self.continue_conversation,
}
@property
def continue_conversation(self) -> bool:
"""Return whether the conversation should continue."""
@@ -241,6 +351,11 @@ class ChatLog:
"""Add user content to the log."""
LOGGER.debug("Adding user content: %s", content)
self.content.append(content)
_async_notify_subscribers(
self.hass,
ChatLogEventType.CONTENT_ADDED,
{"conversation_id": self.conversation_id, "content": content.as_dict()},
)
@callback
def async_add_assistant_content_without_tools(
@@ -259,6 +374,11 @@ class ChatLog:
):
raise ValueError("Non-external tool calls not allowed")
self.content.append(content)
_async_notify_subscribers(
self.hass,
ChatLogEventType.CONTENT_ADDED,
{"conversation_id": self.conversation_id, "content": content.as_dict()},
)
async def async_add_assistant_content(
self,
@@ -317,6 +437,14 @@ class ChatLog:
tool_result=tool_result,
)
self.content.append(response_content)
_async_notify_subscribers(
self.hass,
ChatLogEventType.CONTENT_ADDED,
{
"conversation_id": self.conversation_id,
"content": response_content.as_dict(),
},
)
yield response_content
async def async_add_delta_content_stream(
@@ -569,14 +697,17 @@ class ChatLog:
if llm_api:
prompt_parts.append(llm_api.api_prompt)
prompt_parts.append(
await self._async_expand_prompt_template(
llm_context,
llm.BASE_PROMPT,
llm_context.language,
user_name,
# Append current date and time to the prompt if the corresponding tool is not provided
llm_tools: list[llm.Tool] = llm_api.tools if llm_api else []
if not any(tool.name.endswith("GetDateTime") for tool in llm_tools):
prompt_parts.append(
await self._async_expand_prompt_template(
llm_context,
llm.DATE_TIME_PROMPT,
llm_context.language,
user_name,
)
)
)
if extra_system_prompt := (
# Take new system prompt if one was given
@@ -590,6 +721,11 @@ class ChatLog:
self.llm_api = llm_api
self.extra_system_prompt = extra_system_prompt
self.content[0] = SystemContent(content=prompt)
_async_notify_subscribers(
self.hass,
ChatLogEventType.UPDATED,
{"conversation_id": self.conversation_id, "chat_log": self.as_dict()},
)
LOGGER.debug("Prompt: %s", self.content)
LOGGER.debug("Tools: %s", self.llm_api.tools if self.llm_api else None)

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from enum import IntFlag
from enum import IntFlag, StrEnum
from typing import TYPE_CHECKING
from homeassistant.util.hass_dict import HassKey
@@ -30,3 +30,12 @@ class ConversationEntityFeature(IntFlag):
"""Supported features of the conversation entity."""
CONTROL = 1
class ChatLogEventType(StrEnum):
"""Chat log event type."""
CREATED = "created"
UPDATED = "updated"
DELETED = "deleted"
CONTENT_ADDED = "content_added"

View File

@@ -20,6 +20,7 @@ from .agent_manager import (
async_get_agent,
get_agent_manager,
)
from .chat_log import async_subscribe_chat_logs
from .const import DATA_COMPONENT
from .entity import ConversationEntity
from .models import ConversationInput
@@ -35,6 +36,7 @@ def async_setup(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, websocket_list_sentences)
websocket_api.async_register_command(hass, websocket_hass_agent_debug)
websocket_api.async_register_command(hass, websocket_hass_agent_language_scores)
websocket_api.async_register_command(hass, websocket_subscribe_chat_logs)
@websocket_api.websocket_command(
@@ -265,3 +267,28 @@ class ConversationProcessView(http.HomeAssistantView):
)
return self.json(result.as_dict())
@websocket_api.websocket_command(
{
vol.Required("type"): "conversation/chat_log/subscribe",
}
)
@websocket_api.require_admin
def websocket_subscribe_chat_logs(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Subscribe to all chat logs."""
@callback
def forward_events(event_type: str, data: dict) -> None:
"""Forward chat log events to websocket connection."""
connection.send_message(
{"type": "event", "event_type": event_type, "data": data}
)
unsubscribe = async_subscribe_chat_logs(hass, forward_events)
connection.subscriptions[msg["id"]] = unsubscribe
connection.send_result(msg["id"])

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"quality_scale": "bronze",
"requirements": ["pycync==0.4.1"]
"requirements": ["pycync==0.4.2"]
}

View File

@@ -184,7 +184,8 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
CONF_HOST: self.host,
CONF_PORT: self.port,
CONF_API_KEY: self.api_key,
}
},
reload_on_update=False,
)
except TimeoutError:
@@ -231,7 +232,8 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
updates={
CONF_HOST: self.host,
CONF_PORT: self.port,
}
},
reload_on_update=False,
)
self.context.update(
@@ -265,7 +267,8 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
CONF_HOST: self.host,
CONF_PORT: self.port,
CONF_API_KEY: self.api_key,
}
},
reload_on_update=False,
)
self.context["configuration_url"] = HASSIO_CONFIGURATION_URL

View File

@@ -5,7 +5,9 @@ from __future__ import annotations
import datetime
from homeassistant.components.alarm_control_panel import AlarmControlPanelState
from homeassistant.components.manual.alarm_control_panel import ManualAlarm
from homeassistant.components.manual.alarm_control_panel import ( # pylint: disable=hass-component-root-import
ManualAlarm,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ARMING_TIME, CONF_DELAY_TIME, CONF_TRIGGER_TIME
from homeassistant.core import HomeAssistant

View File

@@ -139,6 +139,7 @@ class DemoCover(CoverEntity):
self.async_write_ha_state()
return
self._is_opening = False
self._is_closing = True
self._listen_cover()
self._requested_closing = True
@@ -162,6 +163,7 @@ class DemoCover(CoverEntity):
return
self._is_opening = True
self._is_closing = False
self._listen_cover()
self._requested_closing = False
self.async_write_ha_state()
@@ -181,10 +183,14 @@ class DemoCover(CoverEntity):
if self._position == position:
return
self._is_closing = position < (self._position or 0)
self._is_opening = not self._is_closing
self._listen_cover()
self._requested_closing = (
self._position is not None and position < self._position
)
self.async_write_ha_state()
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover til to a specific position."""

View File

@@ -3,12 +3,14 @@
from __future__ import annotations
import asyncio
from datetime import datetime
from typing import Any
from homeassistant.components.valve import ValveEntity, ValveEntityFeature, ValveState
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_track_utc_time_change
OPEN_CLOSE_DELAY = 2 # Used to give a realistic open/close experience in frontend
@@ -23,6 +25,8 @@ async def async_setup_entry(
[
DemoValve("Front Garden", ValveState.OPEN),
DemoValve("Orchard", ValveState.CLOSED),
DemoValve("Back Garden", ValveState.CLOSED, position=70),
DemoValve("Trees", ValveState.CLOSED, position=30),
]
)
@@ -37,6 +41,7 @@ class DemoValve(ValveEntity):
name: str,
state: str,
moveable: bool = True,
position: int | None = None,
) -> None:
"""Initialize the valve."""
self._attr_name = name
@@ -46,11 +51,23 @@ class DemoValve(ValveEntity):
)
self._state = state
self._moveable = moveable
self._attr_reports_position = False
self._unsub_listener_valve: CALLBACK_TYPE | None = None
self._set_position: int = 0
self._position: int = 0
if position is None:
return
self._position = self._set_position = position
self._attr_reports_position = True
self._attr_supported_features |= (
ValveEntityFeature.SET_POSITION | ValveEntityFeature.STOP
)
@property
def is_open(self) -> bool:
"""Return true if valve is open."""
return self._state == ValveState.OPEN
def current_valve_position(self) -> int:
"""Return current position of valve."""
return self._position
@property
def is_opening(self) -> bool:
@@ -67,11 +84,6 @@ class DemoValve(ValveEntity):
"""Return true if valve is closed."""
return self._state == ValveState.CLOSED
@property
def reports_position(self) -> bool:
"""Return True if entity reports position, False otherwise."""
return False
async def async_open_valve(self, **kwargs: Any) -> None:
"""Open the valve."""
self._state = ValveState.OPENING
@@ -87,3 +99,45 @@ class DemoValve(ValveEntity):
await asyncio.sleep(OPEN_CLOSE_DELAY)
self._state = ValveState.CLOSED
self.async_write_ha_state()
async def async_stop_valve(self) -> None:
"""Stop the valve."""
self._state = ValveState.OPEN if self._position > 0 else ValveState.CLOSED
if self._unsub_listener_valve is not None:
self._unsub_listener_valve()
self._unsub_listener_valve = None
self.async_write_ha_state()
async def async_set_valve_position(self, position: int) -> None:
"""Move the valve to a specific position."""
if position == self._position:
return
if position > self._position:
self._state = ValveState.OPENING
else:
self._state = ValveState.CLOSING
self._set_position = round(position, -1)
self._listen_valve()
self.async_write_ha_state()
@callback
def _listen_valve(self) -> None:
"""Listen for changes in valve."""
if self._unsub_listener_valve is None:
self._unsub_listener_valve = async_track_utc_time_change(
self.hass, self._time_changed_valve
)
async def _time_changed_valve(self, now: datetime) -> None:
"""Track time changes."""
if self._state == ValveState.OPENING:
self._position += 10
elif self._state == ValveState.CLOSING:
self._position -= 10
if self._position in (100, 0, self._set_position):
await self.async_stop_valve()
return
self.async_write_ha_state()

View File

@@ -2,12 +2,12 @@
from __future__ import annotations
from homeassistant.const import STATE_HOME
from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME # noqa: F401
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from .config_entry import (
from .config_entry import ( # noqa: F401
ScannerEntity,
ScannerEntityDescription,
TrackerEntity,
@@ -15,7 +15,7 @@ from .config_entry import (
async_setup_entry,
async_unload_entry,
)
from .const import (
from .const import ( # noqa: F401
ATTR_ATTRIBUTES,
ATTR_BATTERY,
ATTR_DEV_ID,
@@ -37,7 +37,7 @@ from .const import (
SCAN_INTERVAL,
SourceType,
)
from .legacy import (
from .legacy import ( # noqa: F401
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
SERVICE_SEE,
@@ -61,44 +61,3 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the device tracker."""
async_setup_legacy_integration(hass, config)
return True
__all__ = (
"ATTR_ATTRIBUTES",
"ATTR_BATTERY",
"ATTR_DEV_ID",
"ATTR_GPS",
"ATTR_HOST_NAME",
"ATTR_IP",
"ATTR_LOCATION_NAME",
"ATTR_MAC",
"ATTR_SOURCE_TYPE",
"CONF_CONSIDER_HOME",
"CONF_NEW_DEVICE_DEFAULTS",
"CONF_SCAN_INTERVAL",
"CONF_TRACK_NEW",
"CONNECTED_DEVICE_REGISTERED",
"DEFAULT_CONSIDER_HOME",
"DEFAULT_TRACK_NEW",
"DOMAIN",
"ENTITY_ID_FORMAT",
"PLATFORM_SCHEMA",
"PLATFORM_SCHEMA_BASE",
"SCAN_INTERVAL",
"SERVICE_SEE",
"SERVICE_SEE_PAYLOAD_SCHEMA",
"SOURCE_TYPES",
"AsyncSeeCallback",
"DeviceScanner",
"ScannerEntity",
"ScannerEntityDescription",
"SeeCallback",
"SourceType",
"TrackerEntity",
"TrackerEntityDescription",
"async_setup",
"async_setup_entry",
"async_unload_entry",
"is_on",
"see",
)

View File

@@ -80,8 +80,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
class DevoloScannerEntity(
CoordinatorEntity[DevoloDataUpdateCoordinator[dict[str, ConnectedStationInfo]]],
ScannerEntity,
):

View File

@@ -122,10 +122,12 @@ class WanIpSensor(SensorEntity):
try:
async with asyncio.timeout(10):
response = await self.resolver.query(self.hostname, self.querytype)
except TimeoutError:
except TimeoutError as err:
_LOGGER.debug("Timeout while resolving host: %s", err)
await self.resolver.close()
except DNSError as err:
_LOGGER.warning("Exception while resolving host: %s", err)
await self.resolver.close()
if response:
sorted_ips = sort_ips(

View File

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

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/droplet",
"iot_class": "local_push",
"quality_scale": "bronze",
"requirements": ["pydroplet==2.3.3"],
"requirements": ["pydroplet==2.3.4"],
"zeroconf": ["_droplet._tcp.local."]
}

View File

@@ -4,6 +4,7 @@
"codeowners": ["@mib1185", "@edenhaus", "@Augar"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==15.1.0"]

View File

@@ -8,8 +8,11 @@ from eheimdigital.classic_vario import EheimDigitalClassicVario
from eheimdigital.device import EheimDigitalDevice
from eheimdigital.types import FilterErrorCode
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.components.sensor.const import SensorDeviceClass
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

View File

@@ -21,6 +21,9 @@ DEFAULT_STT_MODEL = "scribe_v1"
DEFAULT_STYLE = 0
DEFAULT_USE_SPEAKER_BOOST = True
MAX_REQUEST_IDS = 3
MODELS_PREVIOUS_INFO_NOT_SUPPORTED = ("eleven_v3",)
STT_LANGUAGES = [
"af-ZA", # Afrikaans
"am-ET", # Amharic

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["elevenlabs"],
"requirements": ["elevenlabs==2.3.0"]
"requirements": ["elevenlabs==2.3.0", "sentence-stream==1.2.0"]
}

View File

@@ -85,4 +85,4 @@ rules:
# Platinum
async-dependency: done
inject-websession: done
strict-typing: done
strict-typing: todo

View File

@@ -2,17 +2,23 @@
from __future__ import annotations
from collections.abc import Mapping
import asyncio
from collections import deque
from collections.abc import AsyncGenerator, Mapping
import contextlib
import logging
from typing import Any
from elevenlabs import AsyncElevenLabs
from elevenlabs.core import ApiError
from elevenlabs.types import Model, Voice as ElevenLabsVoice, VoiceSettings
from sentence_stream import SentenceBoundaryDetector
from homeassistant.components.tts import (
ATTR_VOICE,
TextToSpeechEntity,
TTSAudioRequest,
TTSAudioResponse,
TtsAudioType,
Voice,
)
@@ -35,10 +41,12 @@ from .const import (
DEFAULT_STYLE,
DEFAULT_USE_SPEAKER_BOOST,
DOMAIN,
MAX_REQUEST_IDS,
MODELS_PREVIOUS_INFO_NOT_SUPPORTED,
)
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
PARALLEL_UPDATES = 6
def to_voice_settings(options: Mapping[str, Any]) -> VoiceSettings:
@@ -122,7 +130,12 @@ class ElevenLabsTTSEntity(TextToSpeechEntity):
self._attr_supported_languages = [
lang.language_id for lang in self._model.languages or []
]
self._attr_default_language = self.supported_languages[0]
# Use the first supported language as the default if available
self._attr_default_language = (
self._attr_supported_languages[0]
if self._attr_supported_languages
else "en"
)
def async_get_supported_voices(self, language: str) -> list[Voice]:
"""Return a list of supported voices for a language."""
@@ -151,3 +164,151 @@ class ElevenLabsTTSEntity(TextToSpeechEntity):
)
raise HomeAssistantError(exc) from exc
return "mp3", bytes_combined
async def async_stream_tts_audio(
self, request: TTSAudioRequest
) -> TTSAudioResponse:
"""Generate speech from an incoming message."""
_LOGGER.debug(
"Getting TTS audio for language %s and options: %s",
request.language,
request.options,
)
return TTSAudioResponse("mp3", self._process_tts_stream(request))
async def _process_tts_stream(
self, request: TTSAudioRequest
) -> AsyncGenerator[bytes]:
"""Generate speech from an incoming message."""
text_stream = request.message_gen
boundary_detector = SentenceBoundaryDetector()
sentences: list[str] = []
sentences_ready = asyncio.Event()
sentences_complete = False
language_code: str | None = request.language
voice_id = request.options.get(ATTR_VOICE, self._default_voice_id)
model = request.options.get(ATTR_MODEL, self._model.model_id)
use_request_ids = model not in MODELS_PREVIOUS_INFO_NOT_SUPPORTED
previous_request_ids: deque[str] = deque(maxlen=MAX_REQUEST_IDS)
base_stream_params = {
"voice_id": voice_id,
"model_id": model,
"output_format": "mp3_44100_128",
"voice_settings": self._voice_settings,
}
if language_code:
base_stream_params["language_code"] = language_code
_LOGGER.debug("Starting TTS Stream with options: %s", base_stream_params)
async def _add_sentences() -> None:
nonlocal sentences_complete
try:
# Text chunks may not be on word or sentence boundaries
async for text_chunk in text_stream:
for sentence in boundary_detector.add_chunk(text_chunk):
if not sentence.strip():
continue
sentences.append(sentence)
if not sentences:
continue
sentences_ready.set()
# Final sentence
if text := boundary_detector.finish():
sentences.append(text)
finally:
sentences_complete = True
sentences_ready.set()
_add_sentences_task = self.hass.async_create_background_task(
_add_sentences(), name="elevenlabs_tts_add_sentences"
)
# Process new sentences as they're available, but synthesize the first
# one immediately. While that's playing, synthesize (up to) the next 3
# sentences. After that, synthesize all completed sentences as they're
# available.
sentence_schedule = [1, 3]
while True:
await sentences_ready.wait()
# Don't wait again if no more sentences are coming
if not sentences_complete:
sentences_ready.clear()
if not sentences:
if sentences_complete:
# Exit TTS loop
_LOGGER.debug("No more sentences to process")
break
# More sentences may be coming
continue
new_sentences = sentences[:]
sentences.clear()
while new_sentences:
if sentence_schedule:
max_sentences = sentence_schedule.pop(0)
sentences_to_process = new_sentences[:max_sentences]
new_sentences = new_sentences[len(sentences_to_process) :]
else:
# Process all available sentences together
sentences_to_process = new_sentences[:]
new_sentences.clear()
# Combine all new sentences completed to this point
text = " ".join(sentences_to_process).strip()
if not text:
continue
# Build kwargs common to both modes
kwargs = base_stream_params | {
"text": text,
}
# Provide previous_request_ids if supported.
if previous_request_ids:
# Send previous request ids.
kwargs["previous_request_ids"] = list(previous_request_ids)
# Synthesize audio while text chunks are still being accumulated
_LOGGER.debug("Synthesizing TTS for text: %s", text)
try:
async with self._client.text_to_speech.with_raw_response.stream(
**kwargs
) as stream:
async for chunk_bytes in stream.data:
yield chunk_bytes
if use_request_ids:
if (rid := stream.headers.get("request-id")) is not None:
previous_request_ids.append(rid)
else:
_LOGGER.debug(
"No request-id returned from server; clearing previous requests"
)
previous_request_ids.clear()
except ApiError as exc:
_LOGGER.warning(
"Error during processing of TTS request %s", exc, exc_info=True
)
_add_sentences_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await _add_sentences_task
raise HomeAssistantError(exc) from exc
# Capture and store server request-id for next calls (only when supported)
_LOGGER.debug("Completed TTS stream for text: %s", text)
_LOGGER.debug("Completed TTS stream")

View File

@@ -16,7 +16,9 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorStateClass,
)
from homeassistant.components.sensor.recorder import reset_detected
from homeassistant.components.sensor.recorder import ( # pylint: disable=hass-component-root-import
reset_detected,
)
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfEnergy, UnitOfVolume
from homeassistant.core import (
HomeAssistant,

View File

@@ -10,8 +10,8 @@ from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.components.sensor.const import SensorStateClass
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

View File

@@ -9,6 +9,7 @@ from typing import Any, cast
from aioesphomeapi import (
ClimateAction,
ClimateFanMode,
ClimateFeature,
ClimateInfo,
ClimateMode,
ClimatePreset,
@@ -134,12 +135,16 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = "climate"
_feature_flags = ClimateFeature(0)
@callback
def _on_static_info_update(self, static_info: EntityInfo) -> None:
"""Set attrs from static info."""
super()._on_static_info_update(static_info)
static_info = self._static_info
self._feature_flags = ClimateFeature(
static_info.supported_feature_flags_compat(self._api_version)
)
self._attr_precision = self._get_precision()
self._attr_hvac_modes = [
_CLIMATE_MODES.from_esphome(mode) for mode in static_info.supported_modes
@@ -163,11 +168,18 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
self._attr_max_temp = static_info.visual_max_temperature
self._attr_min_humidity = round(static_info.visual_min_humidity)
self._attr_max_humidity = round(static_info.visual_max_humidity)
features = ClimateEntityFeature.TARGET_TEMPERATURE
if static_info.supports_two_point_target_temperature:
features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
if static_info.supports_target_humidity:
features = ClimateEntityFeature(0)
if self._feature_flags & ClimateFeature.SUPPORTS_TARGET_HUMIDITY:
features |= ClimateEntityFeature.TARGET_HUMIDITY
if self._feature_flags & ClimateFeature.REQUIRES_TWO_POINT_TARGET_TEMPERATURE:
features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
else:
features |= ClimateEntityFeature.TARGET_TEMPERATURE
if (
self._feature_flags
& ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE
):
features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
if self.preset_modes:
features |= ClimateEntityFeature.PRESET_MODE
if self.fan_modes:
@@ -203,7 +215,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
def hvac_action(self) -> HVACAction | None:
"""Return current action."""
# HA has no support feature field for hvac_action
if not self._static_info.supports_action:
if not self._feature_flags & ClimateFeature.SUPPORTS_ACTION:
return None
return _CLIMATE_ACTIONS.from_esphome(self._state.action)
@@ -233,7 +245,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
@esphome_float_state_property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
if not self._static_info.supports_current_temperature:
if not self._feature_flags & ClimateFeature.SUPPORTS_CURRENT_TEMPERATURE:
return None
return self._state.current_temperature
@@ -242,7 +254,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
def current_humidity(self) -> int | None:
"""Return the current humidity."""
if (
not self._static_info.supports_current_humidity
(not self._feature_flags & ClimateFeature.SUPPORTS_CURRENT_HUMIDITY)
or (val := self._state.current_humidity) is None
or not isfinite(val)
):
@@ -254,7 +266,11 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
if (
not self._static_info.supports_two_point_target_temperature
not self._feature_flags
& (
ClimateFeature.REQUIRES_TWO_POINT_TARGET_TEMPERATURE
| ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE
)
and self.hvac_mode != HVACMode.AUTO
):
return self._state.target_temperature
@@ -295,7 +311,10 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
cast(HVACMode, kwargs[ATTR_HVAC_MODE])
)
if ATTR_TEMPERATURE in kwargs:
if not self._static_info.supports_two_point_target_temperature:
if not self._feature_flags & (
ClimateFeature.REQUIRES_TWO_POINT_TARGET_TEMPERATURE
| ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE
):
data["target_temperature"] = kwargs[ATTR_TEMPERATURE]
else:
hvac_mode = kwargs.get(ATTR_HVAC_MODE) or self.hvac_mode

View File

@@ -542,7 +542,16 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
# Check if Z-Wave capabilities are present and start discovery flow
next_flow_id: str | None = None
if self._device_info.zwave_proxy_feature_flags:
# If the zwave_home_id is not set, we don't know if it's a fresh
# adapter, or the cable is just unplugged. So only start
# the zwave_js config flow automatically if there is a
# zwave_home_id present. If it's a fresh adapter, the manager
# will handle starting the flow once it gets the home id changed
# request from the ESPHome device.
if (
self._device_info.zwave_proxy_feature_flags
and self._device_info.zwave_home_id
):
assert self._connected_address is not None
assert self._port is not None
@@ -559,7 +568,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
},
data=ESPHomeServiceInfo(
name=self._device_info.name,
zwave_home_id=self._device_info.zwave_home_id or None,
zwave_home_id=self._device_info.zwave_home_id,
ip_address=self._connected_address,
port=self._port,
noise_psk=self._noise_psk,

View File

@@ -491,13 +491,30 @@ class RuntimeEntryData:
assert self.client.connected_address
# If the device does not have a zwave_home_id, it means
# either the Z-Wave controller has never been connected
# to the ESPHome device, or the Z-Wave controller has
# never been provisioned with a home ID (brand new).
# Since we cannot tell the difference, and it could
# just be the cable is unplugged we only
# automatically start the flow if we have a home ID.
if not device_info.zwave_home_id:
return
self.async_create_zwave_js_flow(hass, device_info, device_info.zwave_home_id)
def async_create_zwave_js_flow(
self, hass: HomeAssistant, device_info: DeviceInfo, zwave_home_id: int
) -> None:
"""Create a zwave_js config flow for a Z-Wave JS Proxy device."""
assert self.client.connected_address is not None
discovery_flow.async_create_flow(
hass,
"zwave_js",
{"source": config_entries.SOURCE_ESPHOME},
ESPHomeServiceInfo(
name=device_info.name,
zwave_home_id=device_info.zwave_home_id or None,
zwave_home_id=zwave_home_id,
ip_address=self.client.connected_address,
port=self.client.port,
noise_psk=self.client.noise_psk,

View File

@@ -6,6 +6,7 @@ import base64
from functools import partial
import logging
import secrets
import struct
from typing import TYPE_CHECKING, Any, NamedTuple
from aioesphomeapi import (
@@ -22,6 +23,8 @@ from aioesphomeapi import (
RequiresEncryptionAPIError,
UserService,
UserServiceArgType,
ZWaveProxyRequest,
ZWaveProxyRequestType,
parse_log_message,
)
from awesomeversion import AwesomeVersion
@@ -44,12 +47,18 @@ from homeassistant.core import (
State,
callback,
)
from homeassistant.exceptions import HomeAssistantError, TemplateError
from homeassistant.exceptions import (
HomeAssistantError,
ServiceNotFound,
ServiceValidationError,
TemplateError,
)
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
issue_registry as ir,
json,
template,
)
from homeassistant.helpers.device_registry import format_mac
@@ -84,6 +93,8 @@ from .encryption_key_storage import async_get_encryption_key_storage
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
DEVICE_CONFLICT_ISSUE_FORMAT = "device_conflict-{}"
UNPACK_UINT32_BE = struct.Struct(">I").unpack_from
if TYPE_CHECKING:
from aioesphomeapi.api_pb2 import SubscribeLogsResponse # type: ignore[attr-defined] # noqa: I001
@@ -268,11 +279,32 @@ class ESPHomeManager:
elif self.entry.options.get(
CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS
):
hass.async_create_task(
hass.services.async_call(
domain, service_name, service_data, blocking=True
call_id = service.call_id
if call_id and service.wants_response:
# Service call with response expected
self.entry.async_create_task(
hass,
self._handle_service_call_with_response(
domain,
service_name,
service_data,
call_id,
service.response_template,
),
)
elif call_id:
# Service call without response but needs success/failure notification
self.entry.async_create_task(
hass,
self._handle_service_call_with_notification(
domain, service_name, service_data, call_id
),
)
else:
# Fire and forget service call
self.entry.async_create_task(
hass, hass.services.async_call(domain, service_name, service_data)
)
)
else:
device_info = self.entry_data.device_info
assert device_info is not None
@@ -298,6 +330,98 @@ class ESPHomeManager:
service_data,
)
async def _handle_service_call_with_response(
self,
domain: str,
service_name: str,
service_data: dict,
call_id: int,
response_template: str | None = None,
) -> None:
"""Handle service call that expects a response and send response back to ESPHome."""
try:
# Call the service with response capture enabled
action_response = await self.hass.services.async_call(
domain=domain,
service=service_name,
service_data=service_data,
blocking=True,
return_response=True,
)
if response_template:
try:
# Render response template
tmpl = Template(response_template, self.hass)
response = tmpl.async_render(
variables={"response": action_response},
strict=True,
)
response_dict = {"response": response}
except TemplateError as ex:
raise HomeAssistantError(
f"Error rendering response template: {ex}"
) from ex
else:
response_dict = {"response": action_response}
# JSON encode response data for ESPHome
response_data = json.json_bytes(response_dict)
except (
ServiceNotFound,
ServiceValidationError,
vol.Invalid,
HomeAssistantError,
) as ex:
self._send_service_call_response(
call_id, success=False, error_message=str(ex), response_data=b""
)
else:
# Send success response back to ESPHome
self._send_service_call_response(
call_id=call_id,
success=True,
error_message="",
response_data=response_data,
)
async def _handle_service_call_with_notification(
self, domain: str, service_name: str, service_data: dict, call_id: int
) -> None:
"""Handle service call that needs success/failure notification."""
try:
await self.hass.services.async_call(
domain, service_name, service_data, blocking=True
)
except (ServiceNotFound, ServiceValidationError, vol.Invalid) as ex:
self._send_service_call_response(call_id, False, str(ex), b"")
else:
self._send_service_call_response(call_id, True, "", b"")
def _send_service_call_response(
self,
call_id: int,
success: bool,
error_message: str,
response_data: bytes,
) -> None:
"""Send service call response back to ESPHome device."""
_LOGGER.debug(
"Service call response for call_id %s: success=%s, error=%s",
call_id,
success,
error_message,
)
self.cli.send_homeassistant_action_response(
call_id,
success,
error_message,
response_data,
)
@callback
def _send_home_assistant_state(
self, entity_id: str, attribute: str | None, state: State | None
@@ -557,6 +681,11 @@ class ESPHomeManager:
)
entry_data.loaded_platforms.add(Platform.ASSIST_SATELLITE)
if device_info.zwave_proxy_feature_flags:
entry_data.disconnect_callbacks.add(
cli.subscribe_zwave_proxy_request(self._async_zwave_proxy_request)
)
cli.subscribe_home_assistant_states_and_services(
on_state=entry_data.async_update_state,
on_service_call=self.async_on_service_call,
@@ -568,6 +697,25 @@ class ESPHomeManager:
_async_check_firmware_version(hass, device_info, api_version)
_async_check_using_api_password(hass, device_info, bool(self.password))
def _async_zwave_proxy_request(self, request: ZWaveProxyRequest) -> None:
"""Handle a request to create a zwave_js config flow."""
if request.type != ZWaveProxyRequestType.HOME_ID_CHANGE:
return
# ESPHome will send a home id change on every connection
# if the Z-Wave controller is connected to the ESPHome device
# so we know for sure that the Z-Wave controller is connected
# when we get the message. This makes it safe to start
# the zwave_js config flow automatically even if the zwave_home_id
# is 0 (not yet provisioned) as we know for sure the controller
# is connected to the ESPHome device and do not have to guess
# if it's a broken connection or Z-Wave controller or a not
# yet provisioned controller.
zwave_home_id: int = UNPACK_UINT32_BE(request.data[0:4])[0]
assert self.entry_data.device_info is not None
self.entry_data.async_create_zwave_js_flow(
self.hass, self.entry_data.device_info, zwave_home_id
)
async def on_disconnect(self, expected_disconnect: bool) -> None:
"""Run disconnect callbacks on API disconnect."""
entry_data = self.entry_data

View File

@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==42.0.0",
"aioesphomeapi==42.2.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.4.0"
],

View File

@@ -6,7 +6,7 @@ from dataclasses import replace
from aioesphomeapi import EntityInfo, SelectInfo, SelectState
from homeassistant.components.assist_pipeline.select import (
from homeassistant.components.assist_pipeline import (
AssistPipelineSelect,
VadSensitivitySelect,
)

View File

@@ -4,6 +4,7 @@
"codeowners": ["@mib1185"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/feedreader",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["feedparser", "sgmllib3k"],
"requirements": ["feedparser==6.0.12"]

View File

@@ -19,7 +19,9 @@ from homeassistant.components.ffmpeg import (
FFmpegManager,
get_ffmpeg_manager,
)
from homeassistant.components.ffmpeg_motion.binary_sensor import FFmpegBinarySensor
from homeassistant.components.ffmpeg_motion.binary_sensor import ( # pylint: disable=hass-component-root-import
FFmpegBinarySensor,
)
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv

View File

@@ -0,0 +1,42 @@
"""The Fing integration."""
from __future__ import annotations
import logging
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from .coordinator import FingConfigEntry, FingDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.DEVICE_TRACKER]
async def async_setup_entry(hass: HomeAssistant, config_entry: FingConfigEntry) -> bool:
"""Set up the Fing component."""
coordinator = FingDataUpdateCoordinator(hass, config_entry)
await coordinator.async_config_entry_first_refresh()
if coordinator.data.network_id is None:
_LOGGER.warning(
"Skip setting up Fing integration; Received an empty NetworkId from the request - Check if the API version is the latest"
)
raise ConfigEntryError(
"The Agent's API version is outdated. Please update the agent to the latest version."
)
config_entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True
async def async_unload_entry(
hass: HomeAssistant, config_entry: FingConfigEntry
) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)

View File

@@ -0,0 +1,114 @@
"""Config flow file."""
from contextlib import suppress
import logging
from typing import Any
from fing_agent_api import FingAgent
import httpx
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_IP_ADDRESS, CONF_PORT
from .const import DOMAIN, UPNP_AVAILABLE
_LOGGER = logging.getLogger(__name__)
class FingConfigFlow(ConfigFlow, domain=DOMAIN):
"""Fing config flow."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Set up user step."""
errors: dict[str, str] = {}
description_placeholders: dict[str, str] = {}
if user_input is not None:
devices_response = None
agent_info_response = None
self._async_abort_entries_match(
{CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS]}
)
fing_api = FingAgent(
ip=user_input[CONF_IP_ADDRESS],
port=int(user_input[CONF_PORT]),
key=user_input[CONF_API_KEY],
)
try:
devices_response = await fing_api.get_devices()
with suppress(httpx.ConnectError):
# The suppression is needed because the get_agent_info method isn't available for desktop agents
agent_info_response = await fing_api.get_agent_info()
except httpx.NetworkError as _:
errors["base"] = "cannot_connect"
except httpx.TimeoutException as _:
errors["base"] = "timeout_connect"
except httpx.HTTPStatusError as exception:
description_placeholders["message"] = (
f"{exception.response.status_code} - {exception.response.reason_phrase}"
)
if exception.response.status_code == 401:
errors["base"] = "invalid_api_key"
else:
errors["base"] = "http_status_error"
except httpx.InvalidURL as _:
errors["base"] = "url_error"
except (
httpx.HTTPError,
httpx.CookieConflict,
httpx.StreamError,
) as ex:
_LOGGER.error("Unexpected exception: %s", ex)
errors["base"] = "unknown"
else:
if (
devices_response.network_id is not None
and len(devices_response.network_id) > 0
):
agent_name = user_input.get(CONF_IP_ADDRESS)
upnp_available = False
if agent_info_response is not None:
upnp_available = True
agent_name = agent_info_response.agent_id
await self.async_set_unique_id(agent_info_response.agent_id)
self._abort_if_unique_id_configured()
data = {
CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS],
CONF_PORT: user_input[CONF_PORT],
CONF_API_KEY: user_input[CONF_API_KEY],
UPNP_AVAILABLE: upnp_available,
}
return self.async_create_entry(
title=f"Fing Agent {agent_name}",
data=data,
)
return self.async_abort(reason="api_version_error")
return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(
vol.Schema(
{
vol.Required(CONF_IP_ADDRESS): str,
vol.Required(CONF_PORT, default="49090"): str,
vol.Required(CONF_API_KEY): str,
}
),
user_input,
),
errors=errors,
description_placeholders=description_placeholders,
)

View File

@@ -0,0 +1,4 @@
"""Const for the Fing integration."""
DOMAIN = "fing"
UPNP_AVAILABLE = "upnp_available"

View File

@@ -0,0 +1,85 @@
"""DataUpdateCoordinator for Fing integration."""
from dataclasses import dataclass, field
from datetime import timedelta
import logging
from fing_agent_api import FingAgent
from fing_agent_api.models import AgentInfoResponse, Device
import httpx
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_IP_ADDRESS, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, UPNP_AVAILABLE
_LOGGER = logging.getLogger(__name__)
type FingConfigEntry = ConfigEntry[FingDataUpdateCoordinator]
@dataclass
class FingDataObject:
"""Fing Data Object."""
network_id: str | None = None
agent_info: AgentInfoResponse | None = None
devices: dict[str, Device] = field(default_factory=dict)
class FingDataUpdateCoordinator(DataUpdateCoordinator[FingDataObject]):
"""Class to manage fetching data from Fing Agent."""
def __init__(self, hass: HomeAssistant, config_entry: FingConfigEntry) -> None:
"""Initialize global Fing updater."""
self._fing = FingAgent(
ip=config_entry.data[CONF_IP_ADDRESS],
port=int(config_entry.data[CONF_PORT]),
key=config_entry.data[CONF_API_KEY],
)
self._upnp_available = config_entry.data[UPNP_AVAILABLE]
update_interval = timedelta(seconds=30)
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=update_interval,
config_entry=config_entry,
)
async def _async_update_data(self) -> FingDataObject:
"""Fetch data from Fing Agent."""
device_response = None
agent_info_response = None
try:
device_response = await self._fing.get_devices()
if self._upnp_available:
agent_info_response = await self._fing.get_agent_info()
except httpx.NetworkError as err:
raise UpdateFailed("Failed to connect") from err
except httpx.TimeoutException as err:
raise UpdateFailed("Timeout establishing connection") from err
except httpx.HTTPStatusError as err:
if err.response.status_code == 401:
raise UpdateFailed("Invalid API key") from err
raise UpdateFailed(
f"Http request failed -> {err.response.status_code} - {err.response.reason_phrase}"
) from err
except httpx.InvalidURL as err:
raise UpdateFailed("Invalid hostname or IP address") from err
except (
httpx.HTTPError,
httpx.CookieConflict,
httpx.StreamError,
) as err:
raise UpdateFailed("Unexpected error from HTTP request") from err
else:
return FingDataObject(
device_response.network_id,
agent_info_response,
{device.mac: device for device in device_response.devices},
)

View File

@@ -0,0 +1,127 @@
"""Platform for Device tracker integration."""
from fing_agent_api.models import Device
from homeassistant.components.device_tracker import ScannerEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import FingConfigEntry
from .coordinator import FingDataUpdateCoordinator
from .utils import get_icon_from_type
async def async_setup_entry(
hass: HomeAssistant,
config_entry: FingConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add sensors for passed config_entry in HA."""
coordinator = config_entry.runtime_data
entity_registry = er.async_get(hass)
tracked_devices: set[str] = set()
@callback
def add_entities() -> None:
latest_devices = set(coordinator.data.devices.keys())
devices_to_remove = tracked_devices - set(latest_devices)
devices_to_add = set(latest_devices) - tracked_devices
entities_to_remove = []
for entity_entry in entity_registry.entities.values():
if entity_entry.config_entry_id != config_entry.entry_id:
continue
try:
_, mac = entity_entry.unique_id.rsplit("-", 1)
if mac in devices_to_remove:
entities_to_remove.append(entity_entry.entity_id)
except ValueError:
continue
for entity_id in entities_to_remove:
entity_registry.async_remove(entity_id)
entities_to_add = []
for mac_addr in devices_to_add:
device = coordinator.data.devices[mac_addr]
entities_to_add.append(FingTrackedDevice(coordinator, device))
tracked_devices.clear()
tracked_devices.update(latest_devices)
async_add_entities(entities_to_add)
add_entities()
config_entry.async_on_unload(coordinator.async_add_listener(add_entities))
class FingTrackedDevice(CoordinatorEntity[FingDataUpdateCoordinator], ScannerEntity):
"""Represent a tracked device."""
_attr_has_entity_name = True
def __init__(self, coordinator: FingDataUpdateCoordinator, device: Device) -> None:
"""Set up FingDevice entity."""
super().__init__(coordinator)
self._device = device
agent_id = coordinator.data.network_id
if coordinator.data.agent_info is not None:
agent_id = coordinator.data.agent_info.agent_id
self._attr_mac_address = self._device.mac
self._attr_unique_id = f"{agent_id}-{self._attr_mac_address}"
self._attr_name = self._device.name
self._attr_icon = get_icon_from_type(self._device.type)
@property
def is_connected(self) -> bool:
"""Return true if the device is connected to the network."""
return self._device.active
@property
def ip_address(self) -> str | None:
"""Return the primary ip address of the device."""
return self._device.ip[0] if self._device.ip else None
@property
def entity_registry_enabled_default(self) -> bool:
"""Enable entity by default."""
return True
@property
def unique_id(self) -> str | None:
"""Return the unique ID of the entity."""
return self._attr_unique_id
def check_for_updates(self, new_device: Device) -> bool:
"""Return true if the device has updates."""
new_device_ip = new_device.ip[0] if new_device.ip else None
current_device_ip = self._device.ip[0] if self._device.ip else None
return (
current_device_ip != new_device_ip
or self._device.active != new_device.active
or self._device.type != new_device.type
or self._attr_name != new_device.name
or self._attr_icon != get_icon_from_type(new_device.type)
)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
updated_device_data = self.coordinator.data.devices.get(self._device.mac)
if updated_device_data is not None and self.check_for_updates(
updated_device_data
):
self._device = updated_device_data
self._attr_name = updated_device_data.name
self._attr_icon = get_icon_from_type(updated_device_data.type)
er.async_get(self.hass).async_update_entity(
entity_id=self.entity_id,
original_name=self._attr_name,
original_icon=self._attr_icon,
)
self.async_write_ha_state()

View File

@@ -0,0 +1,10 @@
{
"domain": "fing",
"name": "Fing",
"codeowners": ["@Lorenzo-Gasparini"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/fing",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["fing_agent_api==1.0.3"]
}

View File

@@ -0,0 +1,72 @@
rules:
# Bronze
action-setup:
status: exempt
comment: The integration has no actions.
appropriate-polling: done
brands: done
common-modules: done
config-flow: done
config-flow-test-coverage: done
dependency-transparency: done
docs-actions:
status: exempt
comment: There are no actions in Fing integration.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: Fing integration entities do not use events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: The integration has no actions.
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: The integration has no options flow.
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: todo
reauthentication-flow: todo
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery: todo
discovery-update-info: todo
docs-data-update: done
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: todo
dynamic-devices: done
entity-category: todo
entity-device-class:
status: exempt
comment: The integration creates only device tracker entities
entity-disabled-by-default: todo
entity-translations: todo
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices: done
# Platinum
async-dependency: todo
inject-websession: todo
strict-typing: todo

View File

@@ -0,0 +1,31 @@
{
"config": {
"step": {
"user": {
"title": "Set up Fing agent",
"data": {
"ip_address": "[%key:common::config_flow::data::ip%]",
"port": "[%key:common::config_flow::data::port%]",
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"ip_address": "IP address of the Fing agent.",
"port": "Port number of the Fing API.",
"api_key": "API key used to authenticate with the Fing API."
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]",
"url_error": "[%key:common::config_flow::error::invalid_host%]",
"http_status_error": "HTTP request failed: {message}"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"api_version_error": "Your agent is using an outdated API version. The required 'network_id' parameter is missing. Please update to the latest API version."
}
}
}

View File

@@ -0,0 +1,85 @@
"""Utils functions."""
from enum import Enum
class DeviceType(Enum):
"""Device types enum."""
GENERIC = "mdi:lan-connect"
MOBILE = PHONE = "mdi:cellphone"
TABLET = IPOD = EREADER = "mdi:tablet"
WATCH = WEARABLE = "mdi:watch"
CAR = AUTOMOTIVE = "mdi:car-back"
MEDIA_PLAYER = "mdi:volume-high"
TELEVISION = "mdi:television"
GAME_CONSOLE = "mdi:nintendo-game-boy"
STREAMING_DONGLE = "mdi:cast"
LOUDSPEAKER = SOUND_SYSTEM = STB = SATELLITE = MUSIC = "mdi:speaker"
DISC_PLAYER = "mdi:disk-player"
REMOTE_CONTROL = "mdi:remote-tv"
RADIO = "mdi:radio"
PHOTO_CAMERA = PHOTOS = "mdi:camera"
MICROPHONE = VOICE_CONTROL = "mdi:microphone"
PROJECTOR = "mdi:projector"
COMPUTER = DESKTOP = "mdi:desktop-tower"
LAPTOP = "mdi:laptop"
PRINTER = "mdi:printer"
SCANNER = "mdi:scanner"
POS = "mdi:printer-pos"
CLOCK = "mdi:clock"
BARCODE = "mdi:barcode"
SURVEILLANCE_CAMERA = BABY_MONITOR = PET_MONITOR = "mdi:cctv"
POE_PLUG = HEALTH_MONITOR = SMART_HOME = SMART_METER = APPLIANCE = SLEEP = (
"mdi:home-automation"
)
SMART_PLUG = "mdi:power-plug"
LIGHT = "mdi:lightbulb"
THERMOSTAT = HEATING = "mdi:home-thermometer"
POWER_SYSTEM = ENERGY = "mdi:lightning-bolt"
SOLAR_PANEL = "mdi:solar-power"
WASHER = "mdi:washing-machine"
FRIDGE = "mdi:fridge"
CLEANER = "mdi:vacuum"
GARAGE = "mdi:garage"
SPRINKLER = "mdi:sprinkler"
BELL = "mdi:doorbell"
KEY_LOCK = "mdi:lock-smart"
CONTROL_PANEL = SMART_CONTROLLER = "mdi:alarm-panel"
SCALE = "mdi:scale-bathroom"
TOY = "mdi:teddy-bear"
ROBOT = "mdi:robot"
WEATHER = "mdi:weather-cloudy"
ALARM = "mdi:alarm-light"
MOTION_DETECTOR = "mdi:motion-sensor"
SMOKE = HUMIDITY = SENSOR = DOMOTZ_BOX = FINGBOX = "mdi:smoke-detector"
ROUTER = MODEM = GATEWAY = FIREWALL = VPN = SMALL_CELL = "mdi:router-network"
WIFI = WIFI_EXTENDER = "mdi:wifi"
NAS_STORAGE = "mdi:nas"
SWITCH = "mdi:switch"
USB = "mdi:usb"
CLOUD = "mdi:cloud"
BATTERY = "mdi:battery"
NETWORK_APPLIANCE = "mdi:network"
VIRTUAL_MACHINE = MAIL_SERVER = FILE_SERVER = PROXY_SERVER = WEB_SERVER = (
DOMAIN_SERVER
) = COMMUNICATION = "mdi:monitor"
SERVER = "mdi:server"
TERMINAL = "mdi:console"
DATABASE = "mdi:database"
RASPBERRY = ARDUINO = "mdi:raspberry-pi"
PROCESSOR = CIRCUIT_CARD = RFID = "mdi:chip"
INDUSTRIAL = "mdi:factory"
MEDICAL = "mdi:medical-bag"
VOIP = CONFERENCING = "mdi:phone-voip"
FITNESS = "mdi:dumbbell"
POOL = "mdi:pool"
SECURITY_SYSTEM = "mdi:security"
def get_icon_from_type(type: str) -> str:
"""Return the right icon based on the type."""
try:
return DeviceType[type].value
except (ValueError, KeyError):
return "mdi:lan-connect"

View File

@@ -0,0 +1,26 @@
"""Diagnostics for the Firefly III integration."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_API_KEY, CONF_URL
from homeassistant.core import HomeAssistant
from . import FireflyConfigEntry
from .coordinator import FireflyDataUpdateCoordinator
TO_REDACT = [CONF_API_KEY, CONF_URL]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: FireflyConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: FireflyDataUpdateCoordinator = entry.runtime_data
return {
"config_entry": async_redact_data(entry.as_dict(), TO_REDACT),
"data": {"primary_currency": coordinator.data.primary_currency.to_dict()},
}

View File

@@ -4,8 +4,12 @@ from __future__ import annotations
from pyfirefly.models import Account, Category
from homeassistant.components.sensor import SensorEntity, SensorStateClass, StateType
from homeassistant.components.sensor.const import SensorDeviceClass
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
StateType,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

View File

@@ -111,7 +111,12 @@ class FlumeConfigFlow(ConfigFlow, domain=DOMAIN):
errors[CONF_PASSWORD] = "invalid_auth"
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
step_id="user",
data_schema=DATA_SCHEMA,
errors=errors,
description_placeholders={
"api_url": "https://portal.flumetech.com/settings#token"
},
)
async def async_step_reauth(

View File

@@ -7,7 +7,7 @@
},
"step": {
"user": {
"description": "In order to access the Flume Personal API, you will need to request a 'Client ID' and 'Client Secret' at https://portal.flumetech.com/settings#token",
"description": "In order to access the Flume Personal API, you will need to request a 'Client ID' and 'Client Secret' at {api_url}",
"title": "Connect to your Flume account",
"data": {
"username": "[%key:common::config_flow::data::username%]",

View File

@@ -6,9 +6,8 @@ import logging
from typing import Any
from homeassistant.components.camera import CameraEntityFeature
from homeassistant.components.ffmpeg.camera import (
CONF_EXTRA_ARGUMENTS,
CONF_INPUT,
from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, CONF_INPUT
from homeassistant.components.ffmpeg.camera import ( # pylint: disable=hass-component-root-import
DEFAULT_ARGUMENTS,
FFmpegCamera,
)

View File

@@ -14,6 +14,7 @@ from homeassistant.helpers import aiohttp_client
from .const import DOMAIN
STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str})
API_KEY_URL = "https://freedompro.eu/"
class Hub:
@@ -53,7 +54,11 @@ class FreedomProConfigFlow(ConfigFlow, domain=DOMAIN):
"""Show the setup form to the user."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
description_placeholders={
"api_key_url": API_KEY_URL,
},
)
errors = {}
@@ -68,7 +73,12 @@ class FreedomProConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_create_entry(title="Freedompro", data=user_input)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
description_placeholders={
"api_key_url": API_KEY_URL,
},
)

View File

@@ -5,7 +5,7 @@
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"description": "Please enter the API key obtained from https://home.freedompro.eu",
"description": "Please enter the API key obtained from {api_key_url}",
"title": "Freedompro API key"
}
},

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable, Mapping
from dataclasses import dataclass, field
from datetime import datetime, timedelta
@@ -16,6 +17,7 @@ from fritzconnection.core.exceptions import (
FritzConnectionException,
FritzSecurityError,
)
from fritzconnection.lib.fritzcall import FritzCall
from fritzconnection.lib.fritzhosts import FritzHosts
from fritzconnection.lib.fritzstatus import FritzStatus
from fritzconnection.lib.fritzwlan import FritzGuestWLAN
@@ -120,6 +122,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
self.fritz_guest_wifi: FritzGuestWLAN = None
self.fritz_hosts: FritzHosts = None
self.fritz_status: FritzStatus = None
self.fritz_call: FritzCall = None
self.host = host
self.mesh_role = MeshRoles.NONE
self.mesh_wifi_uplink = False
@@ -183,6 +186,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
self.fritz_hosts = FritzHosts(fc=self.connection)
self.fritz_guest_wifi = FritzGuestWLAN(fc=self.connection)
self.fritz_status = FritzStatus(fc=self.connection)
self.fritz_call = FritzCall(fc=self.connection)
info = self.fritz_status.get_device_info()
_LOGGER.debug(
@@ -617,6 +621,14 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
self.fritz_guest_wifi.set_password, password, length
)
async def async_trigger_dial(self, number: str, max_ring_seconds: int) -> None:
"""Trigger service to dial a number."""
try:
await self.hass.async_add_executor_job(self.fritz_call.dial, number)
await asyncio.sleep(max_ring_seconds)
finally:
await self.hass.async_add_executor_job(self.fritz_call.hangup)
async def async_trigger_cleanup(self) -> None:
"""Trigger device trackers cleanup."""
_LOGGER.debug("Device tracker cleanup triggered")

View File

@@ -62,6 +62,9 @@
},
"set_guest_wifi_password": {
"service": "mdi:form-textbox-password"
},
"dial": {
"service": "mdi:phone-dial"
}
}
}

View File

@@ -5,6 +5,7 @@
"config_flow": true,
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/fritz",
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["fritzconnection"],
"requirements": ["fritzconnection[qr]==1.15.0", "xmltodict==0.13.0"],

View File

@@ -4,6 +4,7 @@ import logging
from fritzconnection.core.exceptions import (
FritzActionError,
FritzActionFailedError,
FritzConnectionException,
FritzServiceError,
)
@@ -27,6 +28,14 @@ SERVICE_SCHEMA_SET_GUEST_WIFI_PW = vol.Schema(
vol.Optional("length"): vol.Range(min=8, max=63),
}
)
SERVICE_DIAL = "dial"
SERVICE_SCHEMA_DIAL = vol.Schema(
{
vol.Required("device_id"): str,
vol.Required("number"): str,
vol.Required("max_ring_seconds"): vol.Range(min=1, max=300),
}
)
async def _async_set_guest_wifi_password(service_call: ServiceCall) -> None:
@@ -65,6 +74,46 @@ async def _async_set_guest_wifi_password(service_call: ServiceCall) -> None:
) from ex
async def _async_dial(service_call: ServiceCall) -> None:
"""Call Fritz dial service."""
target_entry_ids = await async_extract_config_entry_ids(service_call)
target_entries: list[FritzConfigEntry] = [
loaded_entry
for loaded_entry in service_call.hass.config_entries.async_loaded_entries(
DOMAIN
)
if loaded_entry.entry_id in target_entry_ids
]
if not target_entries:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="config_entry_not_found",
translation_placeholders={"service": service_call.service},
)
for target_entry in target_entries:
_LOGGER.debug("Executing service %s", service_call.service)
avm_wrapper = target_entry.runtime_data
try:
await avm_wrapper.async_trigger_dial(
service_call.data["number"],
max_ring_seconds=service_call.data["max_ring_seconds"],
)
except (FritzServiceError, FritzActionError) as ex:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="service_parameter_unknown"
) from ex
except FritzActionFailedError as ex:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="service_dial_failed"
) from ex
except FritzConnectionException as ex:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="service_not_supported"
) from ex
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services for Fritz integration."""
@@ -75,3 +124,4 @@ def async_setup_services(hass: HomeAssistant) -> None:
_async_set_guest_wifi_password,
SERVICE_SCHEMA_SET_GUEST_WIFI_PW,
)
hass.services.async_register(DOMAIN, SERVICE_DIAL, _async_dial, SERVICE_SCHEMA_DIAL)

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