Compare commits

..

216 Commits

Author SHA1 Message Date
epenet
1bfac54e56 Drop ignore-missing-annotations from pylint 2025-08-10 17:28:33 +02:00
Marc Mueller
0eaea13e8d Update pylint to 3.3.8 + astroid to 3.3.11 (#150327) 2025-08-10 16:41:59 +02:00
Norbert Rittel
b1e4513f7d Capitalize "Ice Plus" as feature name in lg_thinq (#150370) 2025-08-10 15:14:40 +02:00
Steven Looman
6d7f8bb7d7 Remove unused string scan_interval in upnp component (#150372) 2025-08-10 15:14:14 +02:00
Norbert Rittel
b481aaba77 Fix wrong translation of unlock_inside_the_door in xiaomi_ble (#150371)
thanks
2025-08-10 11:45:24 +02:00
Alexandre CUER
d539f37aa4 Remove CONF_EXCLUDE_FEEDID constant from the emoncms integration (#150333) 2025-08-10 09:52:17 +02:00
J. Nick Koston
865b3a6646 Add raw advertisement data to Bluetooth WebSocket API (#150358) 2025-08-10 09:44:15 +02:00
Denis Shulyaka
1c603f968f Bump openai to 1.99.5 (#150342) 2025-08-10 09:41:55 +02:00
J. Nick Koston
d821d27730 Bump habluetooth to 5.0.1 (#150320) 2025-08-10 09:41:25 +02:00
tronikos
dfa060a7e1 Remove Mercury NZ Limited virtual integration (#150316) 2025-08-10 09:38:48 +02:00
Alexandre CUER
5262cca8e6 Use "device_id" instead of "slave" in modbus integration (#150200) 2025-08-10 09:31:26 +02:00
Yuxin Wang
2c36a74da5 Also test unique ID in config flow test for APCUPSD (#150362) 2025-08-10 07:49:25 +02:00
G Johansson
084cde6ecf Add base entity to workday (#150329) 2025-08-09 21:52:39 +02:00
Denis Shulyaka
3e34aa5fb7 Add thinking and native content to chatlog (#149699) 2025-08-09 15:26:19 +02:00
Pete Sage
268f0d9e03 Add Tests for Sonos Alarms (#150014) 2025-08-09 13:47:16 +02:00
Thomas D
f8d3bc1b89 Volvo: Skip unsupported API fields (#150285) 2025-08-09 12:24:53 +02:00
Philipp Waller
fb64ff1d17 Update knx-frontend to 2025.8.9.63154 (#150323) 2025-08-09 12:14:31 +02:00
steinmn
ff72faf83a Set suggested display precision on Volvo energy/fuel consumption sensors (#150296) 2025-08-09 07:48:49 +02:00
Tom
acb58c41eb Bump airOS to 0.2.7 supporting firmware 8.7.11 (#150298) 2025-08-09 07:48:05 +02:00
epenet
586b197fc3 Speedup Tuya snapshot tests (#150198) 2025-08-09 07:46:48 +02:00
Manu
5c1d16d582 Abort config flow if user has no friends in PlayStation Network (#150301) 2025-08-09 07:44:35 +02:00
Tom
73be4625ae Add sensor uom suggestions to airOS (#150303) 2025-08-09 07:43:51 +02:00
Andrew Jackson
775701133d Remove deprecated notify platform from Mastodon (#149735) 2025-08-09 01:17:48 +02:00
MB901
1af0282091 Add hardware version to FreeboxRouter device info (#150004) 2025-08-09 00:54:53 +02:00
Joakim Plate
c876bed33f Add ToGrill integration (#150075)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-08-09 00:24:54 +02:00
G Johansson
e9d39a826e Remove deprecated horizontal vane select from Sensibo (#150108) 2025-08-09 00:24:38 +02:00
Thomas55555
f9e1c07c04 Add event platform to Husqvarna Automower (#148212)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2025-08-09 00:07:47 +02:00
Renat Sibgatulin
c0bef51563 Refactor airq tests to mock the API class in a fixture (#149712) 2025-08-09 00:01:39 +02:00
Willem-Jan van Rootselaar
b41a9575af Add protected call for data retrieval (#150035) 2025-08-08 23:58:19 +02:00
Norbert Rittel
e585b3abd1 Fix missing sentence-casing of "AC failure" in bosch_alarm (#150279) 2025-08-08 23:33:55 +02:00
mbo18
5d2877f454 Add absolute humidity sensor to Awair integration (#150110) 2025-08-08 22:55:24 +02:00
Artur Pragacz
2d89c60ac5 Improve service schemas in unifiprotect (#150236) 2025-08-08 22:51:24 +02:00
Marco Gasparini
860a7b7d91 Fix Progettihwsw config flow (#150149) 2025-08-08 22:29:50 +02:00
Ludovic BOUÉ
5585376b40 Switchbot Hub Light level (#150147) 2025-08-08 22:13:23 +02:00
Thomas55555
c4cb70fc06 Handle HusqvarnaWSClientError (#150145) 2025-08-08 22:12:18 +02:00
Pete Sage
981ae39182 Fix dialog enhancement switch for Sonos Arc Ultra (#150116) 2025-08-08 22:11:32 +02:00
Alexandre CUER
dff4f79925 Remove useless strings from emoncms (#150182) 2025-08-08 22:00:48 +02:00
Manu
bf64e11960 Migrate unique_id only if monitor_id is present in Uptime Kuma (#150197) 2025-08-08 21:38:27 +02:00
Thomas D
823d20c67f Volvo: fix distance to empty battery (#150278) 2025-08-08 21:28:29 +02:00
Norbert Rittel
1a654cd35d Use common strings "Low"/"High" for more states in tuya (#150283) 2025-08-08 20:52:03 +02:00
Denis Shulyaka
13e592edaf Bump anthropic to 0.62.0 (#150284) 2025-08-08 20:51:49 +02:00
Norbert Rittel
94191239c6 Remove misleading "the" from Launch Library configuration (#150288) 2025-08-08 20:50:14 +02:00
Denis Shulyaka
91a1ca09f7 Add GPT-5 support (#150281) 2025-08-08 20:49:09 +02:00
Tom
9f1fe8a067 Add binary_sensor to UISP airOS (#149803)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-08-08 20:34:40 +02:00
Yuxin Wang
f2c9cdb09e Add quality scale for APCUPSD integration (#146999) 2025-08-08 20:31:34 +02:00
Tom
712115cdb8 Bump airOS to 0.2.6 improving device class matching more devices (#150134) 2025-08-08 19:33:16 +02:00
dependabot[bot]
eb6ae9d2d6 Bump actions/cache from 4.2.3 to 4.2.4 (#150253)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-08 15:28:51 +02:00
puddly
b126f3fa66 Bump ZHA to 0.0.68 (#150208) 2025-08-08 15:27:17 +02:00
puddly
2d720f0d32 Fix JSON serialization for ZHA diagnostics download (#150210) 2025-08-08 15:27:00 +02:00
Raphael Hehl
c0155f5e80 Handle Unifi Protect BadRequest exception during API key creation (#150223) 2025-08-08 15:26:02 +02:00
Thomas D
23a2d69984 Volvo: fix missing charging power options (#150272) 2025-08-08 15:25:19 +02:00
peteS-UK
a8779d5f52 Fix error on startup when no Apps or Radio plugins are installed for Squeezebox (#150267) 2025-08-08 15:24:41 +02:00
Robert Resch
01c197e830 Constraint num2words to 0.5.14 (#150276) 2025-08-08 15:06:31 +02:00
peteS-UK
ef4f476844 Fix handing for zero volume error in Squeezebox (#150265) 2025-08-08 14:26:04 +02:00
dependabot[bot]
8aee05b8b0 Bump github/codeql-action from 3.29.5 to 3.29.7 (#150254)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-08 13:57:20 +02:00
Denis Shulyaka
0f3f8d5707 Bump openai to 1.99.3 (#150232) 2025-08-08 13:57:12 +02:00
epenet
2948b1c58e Cleanup Tuya fixture files (#150190) 2025-08-08 13:56:44 +02:00
Joris Pelgröm
4cb2af4d08 Add select platform to LetPot integration (#150212) 2025-08-08 13:47:13 +02:00
G Johansson
8e12d2028d Remove previously deprecated linear_garage_door (#150109)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-08-08 13:09:01 +02:00
G Johansson
5b046def8e Move holiday object to runtime data in workday (#149122) 2025-08-08 12:02:07 +02:00
Åke Strandberg
6a81bf6f5e Improve interface between Miele integration and pymiele library (#150214) 2025-08-08 11:40:04 +02:00
epenet
102d6a37c0 Use generated device id in tuya tests (#150196) 2025-08-08 09:15:42 +02:00
Yuxin Wang
fd6aba3022 Add missing strings for APCUPSD (#150242) 2025-08-08 08:41:03 +02:00
tronikos
a88eadf863 Update Opower strings (#150247) 2025-08-08 08:40:28 +02:00
Tom
52f0d04c38 Improve Roborock test teardown (#150144) 2025-08-07 20:32:05 -07:00
Denis Shulyaka
3ab80c6ff2 Bump google-genai to 1.29.0 (#150225) 2025-08-07 16:26:02 -07:00
Vincent Wolsink
71485871c8 Bump Huum requirement to 0.8.1 (#150220) 2025-08-07 21:59:58 +01:00
Martin Hjelmare
ba0da4c2a3 Remove switchbot vacuum battery attribute (#150227) 2025-08-07 22:39:45 +02:00
Martin Hjelmare
cbaadebac3 Fix Tibber coordinator ContextVar warning (#150229) 2025-08-07 22:39:24 +02:00
Åke Strandberg
fd0ae32058 Bump pymiele to 0.5.3 (#150216) 2025-08-07 20:48:25 +02:00
Martin Hjelmare
382bf78ee0 Ignore MQTT vacuum battery warning (#150211) 2025-08-07 20:11:39 +02:00
Martin Hjelmare
6aa077a48d Silence vacuum battery deprecation for built in integrations (#150204) 2025-08-07 19:43:36 +02:00
Joakim Sørensen
b638fcbaad Bump hass-nabucasa from 0.111.1 to 0.111.2 (#150209) 2025-08-07 19:42:22 +02:00
G Johansson
704edac9fd Remove deprecated state from backup schedule (#150114)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-08-07 17:42:53 +01:00
yufeng
ff9e2a8f1e Update tuya translation for reverse energy sensor (#149317) 2025-08-07 17:08:57 +02:00
Stefan H.
d778afe61a Fix Enigma2 startup hang (#149756) 2025-08-07 15:33:24 +01:00
Norbert Rittel
448084e2b5 Fix description of button.press action (#150181) 2025-08-07 15:22:36 +02:00
jan iversen
d99379ffdf modbus: use only 1 logger instance. (#150130) 2025-08-07 15:11:00 +02:00
Maciej Bieniek
b835b7f266 Bump imgw_pib to version 1.5.3 (#150178) 2025-08-07 13:31:55 +02:00
epenet
e96e97edca Add Tuya snapshots tests for sj category (rain sensor) (#150173) 2025-08-07 13:24:33 +02:00
epenet
df7c657d7e Add Tuya snapshots tests for wk category (thermostat) (#150175) 2025-08-07 12:53:19 +02:00
epenet
4f5502ab47 Add Tuya snapshots tests for ldcg category (luminance sensor) (#150169) 2025-08-07 12:50:46 +02:00
epenet
c30ee776e9 Add Tuya snapshots tests for zwjcy category (soil sensor) (#150168) 2025-08-07 10:44:51 +02:00
epenet
efebdc0181 Add Tuya snapshots tests for cl category (curtains) (#150167) 2025-08-07 10:42:36 +02:00
jan iversen
da7fc88f1f Bump pymodbus to v3.11.0. (#150129) 2025-08-07 08:13:11 +02:00
Joris Pelgröm
566aeb5e9a Bump letpot to 0.6.1 (#150137) 2025-08-07 08:08:47 +02:00
J. Nick Koston
d17f0ef55a Bump inkbird-ble to 1.1.0 to add support for IAM-T2 (#150158) 2025-08-07 08:07:31 +02:00
Abílio Costa
35025c4b59 Fix roborock config flow tests (#150135) 2025-08-07 00:05:31 +01:00
Abílio Costa
e5d512d5e5 Add entity filter to target state change tracker (#150064)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-06 20:03:09 +02:00
puddly
2b5028bfb7 Bump ZHA to 0.0.67 (#150132) 2025-08-06 19:56:44 +02:00
Paul Bottein
757fee9f73 Use state selector for climate set hvac mode service (#148963) 2025-08-06 17:48:55 +01:00
Artur Pragacz
06130219b4 Use relative condition keys (#150021) 2025-08-06 17:20:30 +01:00
AlCalzone
4e2fe63182 Check for Z-Wave firmware updates of sleeping devices (#150123) 2025-08-06 18:08:51 +02:00
Luca Angemi
d0cc9990dd Deprecate Roborock battery feature (#150126) 2025-08-06 17:32:23 +02:00
epenet
76ca9ce3a4 Add comment to Tuya code for unsupported devices (#150125)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-06 17:12:32 +02:00
epenet
124e7cf4c8 Add support for tuya ywcgq category (liquid level) (#150096)
Thanks @joostlek / @frenck
2025-08-06 15:38:50 +02:00
G Johansson
260ea9a3be Remove previously deprecated raw value attribute from onewire (#150112) 2025-08-06 15:24:22 +02:00
Bram Kragten
e1f6820cb6 Update frontend to 20250806.0 (#150106)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-08-06 15:22:46 +02:00
David Poll
2215777cfb Fix zero-argument functions with as_function (#150062) 2025-08-06 15:20:03 +02:00
G Johansson
fa3ce62ae8 Bump holidays to 0.78 (#150103) 2025-08-06 14:55:00 +02:00
Joakim Sørensen
33421bddf3 Remove myself as codeowner from traccar_server (#150107) 2025-08-06 14:51:43 +02:00
markhannon
1efe2b437d Improve dependency transparency for Zimi integration (#145879) 2025-08-06 14:50:06 +02:00
Joost Lekkerkerker
a54f0adf74 Enable disabled Ollama config entries after entry migration (#150105) 2025-08-06 14:27:36 +02:00
epenet
afe574f74e Simplify DPCode lookup in Tuya (#150052) 2025-08-06 14:24:01 +02:00
epenet
25aae8944d Add Tuya snapshots tests for mzj category (sous-vide) (#150102) 2025-08-06 14:17:30 +02:00
Martin Hjelmare
f26e6ad211 Fix update coordinator ContextVar log for custom integrations (#150100) 2025-08-06 14:14:42 +02:00
Joost Lekkerkerker
e9444a2e4d Enable disabled Anthropic config entries after entry migration (#150098) 2025-08-06 13:24:49 +02:00
Joost Lekkerkerker
60988534a9 Enable disabled OpenAI config entries after entry migration (#150099) 2025-08-06 13:24:37 +02:00
Michael
932bf81ac8 Add common constant ATTR_CONFIG_ENTRY_ID (#150067) 2025-08-06 12:42:51 +02:00
Jan Bouwhuis
1302b6744e Deprecate MQTT vacuum battery feature and remove it as default feature (#149877)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-08-06 11:51:31 +02:00
tronikos
0aeff366bd Fix PG&E and Duquesne Light Company in Opower (#149658)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-08-06 11:32:42 +02:00
epenet
0db23b0da6 Add Tuya debug logging for new devices (#150091)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-06 11:23:34 +02:00
epenet
863e2074b6 Add more switches to Tuya tdq category (#150090) 2025-08-06 11:03:26 +02:00
epenet
13828f6713 Remove tuya vacuum battery level attribute (#150086) 2025-08-06 11:02:04 +02:00
starkillerOG
fdb38ec8ec Reduce Reolink fimware polling from 12h to 24h (#150095) 2025-08-06 10:58:52 +02:00
Robert Resch
55abb6e594 Fix hassio tests by only mocking supervisor id (#150093) 2025-08-06 10:53:55 +02:00
Stefan Agner
a83e4f5c63 Add missing translations for unhealthy Supervisor issues (#150036) 2025-08-06 10:07:36 +02:00
J. Nick Koston
cba15ee439 Bump habluetooth to 4.0.2 (#150078)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-08-06 09:51:44 +02:00
dependabot[bot]
400620399a Bump actions/download-artifact from 4.3.0 to 5.0.0 (#150084)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-06 09:48:10 +02:00
dependabot[bot]
28e19215ad Bump actions/ai-inference from 1.2.7 to 1.2.8 (#150083)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-06 09:46:02 +02:00
Philipp Waller
119d0a0170 Update knx-frontend to 2025.8.6.52906 (#150085) 2025-08-06 09:28:44 +02:00
Joakim Sørensen
69faf38e86 Bump hass-nabucasa from 0.111.0 to 0.111.1 (#150082) 2025-08-06 09:24:09 +02:00
puddly
d0ef1a1a8b Bump ZHA to 0.0.66 (#150081) 2025-08-06 09:22:07 +02:00
Retha Runolfsson
8f328810bf Bump pyswitchbot to 0.68.3 (#150080) 2025-08-05 19:20:37 -10:00
Pete Sage
4f1b75e3b4 Bump soco to 0.30.11 (#150072) 2025-08-05 22:56:27 +01:00
J. Nick Koston
445a7fc749 Bump yalexs to 8.11.1 (#150073) 2025-08-05 22:55:01 +01:00
Robert Svensson
977c0797aa Bump axis to v65 (#150065) 2025-08-05 11:36:48 -10:00
Ludovic BOUÉ
a24f027923 Add icon for esa_state in Matter integration (#149075) 2025-08-05 23:18:48 +02:00
Martin Hjelmare
7b45798e30 Remove matter vacuum battery level attribute (#150061) 2025-08-05 22:40:42 +02:00
Artur Pragacz
2b0cda0ad1 Adjust condition and trigger method names (#150060) 2025-08-05 19:46:03 +01:00
starkillerOG
12dca4b1bf Bump reolink-aio to 0.14.6 (#150055) 2025-08-05 18:58:22 +02:00
karwosts
8c509b11b2 Fix template sensor uom string (#150057) 2025-08-05 18:56:34 +02:00
Joost Lekkerkerker
991c9008bd Change AI task strings (#150051) 2025-08-05 16:35:41 +02:00
Martin Hjelmare
fe95f6e1c5 Improve downloader service (#150046)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-08-05 16:12:55 +02:00
Bram Kragten
37510aa316 Update frontend to 20250805.0 (#150049) 2025-08-05 16:01:47 +02:00
Marc Mueller
4e40e9bf74 Update mypy-dev to 1.18.0a4 (#150005) 2025-08-05 15:56:03 +02:00
Bouwe Westerdijk
70c9b1f095 Implement snapshot testing for Plugwise button platform (#149984) 2025-08-05 15:31:02 +02:00
dependabot[bot]
f714388130 Bump docker/login-action from 3.4.0 to 3.5.0 (#150034)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 15:25:58 +02:00
Joost Lekkerkerker
ffb2a693f4 Ignore vacuum entities that properly deprecate battery (#150043) 2025-08-05 15:22:21 +02:00
Andrew Jackson
9d8e253ad3 Default to zero quantity on new todo items in Mealie (#150047) 2025-08-05 15:15:08 +02:00
dependabot[bot]
31631cc882 Bump actions/ai-inference from 1.2.4 to 1.2.7 (#150038)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 14:40:01 +02:00
epenet
3a64357201 Fix Tuya fan speeds with numeric values (#149971) 2025-08-05 13:22:45 +02:00
Thomas55555
20fdec9e9c Reduce polling in Husqvarna Automower (#149255)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-08-05 12:56:27 +02:00
Nippey
064a63fe1f Add support for Tuya "Bresser 7-in-1 Weatherstation" (#149498) 2025-08-05 12:54:40 +02:00
epenet
803654223a Revert "Do not create Tuya fan entities without control" (#150032) 2025-08-05 12:23:06 +02:00
epenet
a6148b50cf Add Tuya snapshots tests for button and vacuum platform (#149968) 2025-08-05 11:21:05 +02:00
Ludovic BOUÉ
02a3c5be14 Matter pump setpoint CurrentLevel limit (#149689) 2025-08-05 11:19:03 +02:00
Paulus Schoutsen
08ea640629 Do not allow overriding users when uuid is duplicate (#149408) 2025-08-05 11:13:32 +02:00
Grzegorz M
7dd761c9c3 Bump icalendar from 6.1.0 to 6.3.1 for CalDav (#149990) 2025-08-05 11:09:03 +02:00
epenet
6b827dfc33 Do not create Tuya fan entities without control (#149976)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-05 09:52:29 +02:00
Robert Resch
67c19087dd Bump deebot-client to 13.6.0 (#149983) 2025-08-05 09:08:33 +02:00
epenet
55c7c2f730 Redact terminal_id in Tuya fixture files (#149957) 2025-08-05 09:06:15 +02:00
Matthias Alphart
afee936c3d Update knx-frontend to 2025.8.4.154919 (#149991) 2025-08-05 09:03:23 +02:00
Marc Mueller
ed2ced6c36 Fix zimi test RuntimeWarnings (#150017) 2025-08-05 08:55:54 +02:00
Martin Hjelmare
4c5cf028d7 Fix Z-Wave duplicate provisioned device (#150008) 2025-08-05 08:50:42 +02:00
Thomas55555
68faa897ad Bump aioautomower to 2.1.2 (#150003) 2025-08-05 08:48:47 +02:00
Artur Pragacz
53c9c42148 Use relative trigger keys (#149846) 2025-08-04 23:01:40 +01:00
Michael Hansen
d48cc03be7 Bump wyoming to 1.7.2 (#150007) 2025-08-04 23:36:24 +02:00
starkillerOG
28236aa023 Reolink disable entities by default (#149986) 2025-08-04 23:03:38 +02:00
Tom
bfae07135a Bump python-airos to 0.2.4 (#149885) 2025-08-04 22:35:47 +02:00
Thomas55555
99d580e371 Add reset cutting blade usage time to Husqvarna Automower (#149628) 2025-08-04 22:28:34 +02:00
Petro31
4d53450cbf Create battery_level deprecation repair for template vacuum platform (#149987)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-08-04 21:54:50 +02:00
epenet
1fbce01e26 Add initial support for Tuya wg2 category (#149676) 2025-08-04 21:30:43 +02:00
markhannon
a9621ac811 Add tests for Zimi entitites (#144292) 2025-08-04 20:41:05 +02:00
Marc Mueller
94f2118b19 Fix flaky history_stats test case (#149974) 2025-08-04 20:34:07 +02:00
Mike Degatano
73ca6b4900 Add translation strings for unsupported OS version (#149837) 2025-08-04 17:40:11 +02:00
Joakim Sørensen
31e647b5b0 Bump hass-nabucasa from 0.110.1 to 0.111.0 (#149977) 2025-08-04 16:59:07 +02:00
epenet
fac5b2c09c Add Tuya snapshots tests for camera platform (#149959) 2025-08-04 16:58:46 +02:00
Martin Hjelmare
ae48179e95 Bump zwave-js-server-python to 0.67.1 (#149972) 2025-08-04 15:58:57 +02:00
Willem-Jan van Rootselaar
88c9d5dbe3 Fix bsblan reauthentication (#149926) 2025-08-04 15:35:41 +02:00
hanwg
b76f47cd9f Add bot details to Telegram bot events (#148638) 2025-08-04 14:32:48 +02:00
hanwg
822e1ffc8d Minor UI improvements for Telegram bot actions (#149889) 2025-08-04 14:27:15 +02:00
Martin Hjelmare
1632e0aef6 Direct migrations with Z-Wave JS UI to docs (#149966) 2025-08-04 13:36:12 +02:00
Petro31
e2bc73f153 Fix optimistic covers (#149962) 2025-08-04 13:35:13 +02:00
Joakim Sørensen
46cfdddc80 Move to the new handler for migrate_paypal_agreement (#149934) 2025-08-04 13:29:11 +02:00
Joost Lekkerkerker
0bdf6757c4 Pass config entry to Remote Calendar coordinator (#149958) 2025-08-04 13:28:59 +02:00
Joost Lekkerkerker
312e590360 Pass config entry to Broadlink coordinator (#149949) 2025-08-04 13:27:51 +02:00
Joost Lekkerkerker
7a6aaf667b Pass config entry to hue coordinator (#149941) 2025-08-04 13:27:10 +02:00
Joost Lekkerkerker
33eaca24d6 Pass config entry to Simplisafe coordinator (#149943) 2025-08-04 13:21:29 +02:00
Joost Lekkerkerker
3d27d501b1 Pass config entry to Mill coordinator (#149942) 2025-08-04 13:20:30 +02:00
Joost Lekkerkerker
39b651e075 Pass config entry to Kraken coordinator (#149944) 2025-08-04 13:17:27 +02:00
Joost Lekkerkerker
a962777a2e Pass config entry to Meteo France coordinator (#149945) 2025-08-04 13:14:50 +02:00
Joost Lekkerkerker
594ce8f266 Pass config entry to Smarttub coordinator (#149946) 2025-08-04 12:58:46 +02:00
Joost Lekkerkerker
9f867f268c Pass config entry to Snoo coordinator (#149947) 2025-08-04 12:58:19 +02:00
Joost Lekkerkerker
9edd242734 Pass config entry to SMS coordinator (#149955) 2025-08-04 12:49:26 +02:00
Bouwe Westerdijk
93e11aa8bc Refresh plugwise test-fixtures (#149875) 2025-08-04 12:35:24 +02:00
Joakim Sørensen
c2b298283e Bump hass-nabucasa from 0.110.0 to 0.110.1 (#149956) 2025-08-04 12:32:01 +02:00
Joost Lekkerkerker
106c086e8b Pass config entry to Unifi coordinator (#149952) 2025-08-04 12:29:27 +02:00
Markus Adrario
cbf4130bff Add zeroconf flow to Homee (#149820)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-08-04 12:26:22 +02:00
Erik Montnemery
afffe0b08b Fix DeviceEntry.suggested_area deprecation warning (#149951) 2025-08-04 12:20:30 +02:00
Joost Lekkerkerker
c1ccfee7cc Pass config entry to AsusWRT coordinator (#149953) 2025-08-04 12:08:03 +02:00
epenet
8d8383e1c1 Add extra Tuya snapshots for dc and dj category (lights) (#149940) 2025-08-04 12:07:25 +02:00
Marc Mueller
f350a1a1fa Add hassfest check to help with future dependency updates (#149624) 2025-08-04 12:03:39 +02:00
epenet
fe2bd8d09e Add Tuya snapshots for ywcgq category (#149948) 2025-08-04 12:02:34 +02:00
Joost Lekkerkerker
cf14226b02 Pass config entry to Fronius coordinator (#149954) 2025-08-04 12:02:21 +02:00
Brett Adams
bd3fe1d4ad Fix credit sensor when there are no vehicles in Teslemetry (#149925) 2025-08-04 11:26:14 +02:00
Christopher Fenner
377ca04be8 Update sensor icons in Volvo integration (#149811) 2025-08-04 11:24:51 +02:00
epenet
5837f55205 Add extra Tuya snapshots for cz category (#149938) 2025-08-04 11:23:58 +02:00
andreimoraru
0766edb9c4 Bump yt-dlp to 2025.07.21 (#149916)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-08-04 11:15:38 +02:00
epenet
e62e3778f3 Add Tuya snapshots for hps category (#149936) 2025-08-04 11:14:11 +02:00
epenet
aa8e4c1c15 Add Tuya snapshots for sgbj, sp, wfcon and ywbj category (#149933) 2025-08-04 11:11:06 +02:00
Erik Montnemery
46ed8a73fc Bump automower-ble to 0.2.7 (#149928) 2025-08-04 11:09:18 +02:00
dependabot[bot]
83f22497ae Bump actions/ai-inference from 1.2.3 to 1.2.4 (#149929)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-04 11:09:02 +02:00
epenet
3dda1685dc Add Tuya snapshots for pc and pir category (#149931) 2025-08-04 11:08:43 +02:00
Ståle Storø Hauknes
6fa9d42401 Airthings ContextVar warning (#149930) 2025-08-04 11:05:32 +02:00
jvmahon
1a54d566f8 Apple vendor name update (#149845) 2025-08-04 10:26:11 +02:00
puddly
1a9cae0f89 Bump ZHA to 0.0.65 (#149922) 2025-08-04 10:17:25 +02:00
epenet
551dcaa169 Rename Tuya fixture files (#149927) 2025-08-04 10:08:03 +02:00
epenet
5467db065b Make Tuya complex type handling explicit (#149677) 2025-08-04 07:59:47 +02:00
J. Nick Koston
6a8d752e56 Bump aiodiscover to 2.7.1 (#149920) 2025-08-03 16:42:38 -10:00
J. Nick Koston
179a56628d Bump dbus-fast to 2.44.3 (#149921) 2025-08-03 16:42:11 -10:00
J. Nick Koston
b3f830773a Bump yalexs-ble to 3.1.2 (#149917) 2025-08-03 15:02:30 -10:00
Joost Lekkerkerker
084e06ec7d Bump python-open-router to 0.3.1 (#149873) 2025-08-03 21:46:40 +02:00
Maciej Bieniek
e0190afd3c Bump imgw_pib to version 1.5.2 (#149892) 2025-08-03 20:07:01 +02:00
Jan-Philipp Benecke
b9e16d54c4 Add jitter sensor to Ping integration (#149899) 2025-08-03 20:06:14 +02:00
Thomas55555
627785edc1 Fix options for error sensor in Husqvarna Automower (#149901) 2025-08-03 20:05:23 +02:00
619 changed files with 40260 additions and 8152 deletions

View File

@@ -175,7 +175,7 @@ jobs:
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
- name: Download translations
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: translations
@@ -190,7 +190,7 @@ jobs:
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.4.0
uses: docker/login-action@v3.5.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -256,7 +256,7 @@ jobs:
fi
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.4.0
uses: docker/login-action@v3.5.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -330,14 +330,14 @@ jobs:
- name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
uses: docker/login-action@v3.4.0
uses: docker/login-action@v3.5.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: matrix.registry == 'ghcr.io/home-assistant'
uses: docker/login-action@v3.4.0
uses: docker/login-action@v3.5.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -462,7 +462,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download translations
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: translations
@@ -502,7 +502,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}

View File

@@ -255,7 +255,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache@v4.2.3
uses: actions/cache@v4.2.4
with:
path: venv
key: >-
@@ -271,7 +271,7 @@ jobs:
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache@v4.2.3
uses: actions/cache@v4.2.4
with:
path: ${{ env.PRE_COMMIT_CACHE }}
lookup-only: true
@@ -301,7 +301,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.4
with:
path: venv
fail-on-cache-miss: true
@@ -310,7 +310,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.4
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -341,7 +341,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.4
with:
path: venv
fail-on-cache-miss: true
@@ -350,7 +350,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.4
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -381,7 +381,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.4
with:
path: venv
fail-on-cache-miss: true
@@ -390,7 +390,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.4
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -497,7 +497,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache@v4.2.3
uses: actions/cache@v4.2.4
with:
path: venv
key: >-
@@ -505,7 +505,7 @@ jobs:
needs.info.outputs.python_cache_key }}
- name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache@v4.2.3
uses: actions/cache@v4.2.4
with:
path: ${{ env.UV_CACHE_DIR }}
key: >-
@@ -593,7 +593,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.4
with:
path: venv
fail-on-cache-miss: true
@@ -626,7 +626,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.4
with:
path: venv
fail-on-cache-miss: true
@@ -683,7 +683,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.4
with:
path: venv
fail-on-cache-miss: true
@@ -726,7 +726,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.4
with:
path: venv
fail-on-cache-miss: true
@@ -741,14 +741,14 @@ jobs:
run: |
. venv/bin/activate
python --version
pylint --ignore-missing-annotations=y homeassistant
pylint homeassistant
- name: Run pylint (partially)
if: needs.info.outputs.test_full_suite == 'false'
shell: bash
run: |
. venv/bin/activate
python --version
pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.info.outputs.integrations_glob }}
pylint homeassistant/components/${{ needs.info.outputs.integrations_glob }}
pylint-tests:
name: Check pylint on tests
@@ -773,7 +773,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.4
with:
path: venv
fail-on-cache-miss: true
@@ -825,7 +825,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.4
with:
path: venv
fail-on-cache-miss: true
@@ -833,7 +833,7 @@ jobs:
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Restore mypy cache
uses: actions/cache@v4.2.3
uses: actions/cache@v4.2.4
with:
path: .mypy_cache
key: >-
@@ -895,7 +895,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.4
with:
path: venv
fail-on-cache-miss: true
@@ -956,7 +956,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.4
with:
path: venv
fail-on-cache-miss: true
@@ -970,7 +970,7 @@ jobs:
run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Download pytest_buckets
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: pytest_buckets
- name: Compile English translations
@@ -1089,7 +1089,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.4
with:
path: venv
fail-on-cache-miss: true
@@ -1231,7 +1231,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.4
with:
path: venv
fail-on-cache-miss: true
@@ -1336,7 +1336,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
pattern: coverage-*
- name: Upload coverage to Codecov
@@ -1390,7 +1390,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.4
with:
path: venv
fail-on-cache-miss: true
@@ -1486,7 +1486,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
pattern: coverage-*
- name: Upload coverage to Codecov
@@ -1511,7 +1511,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
pattern: test-results-*
- name: Upload test results to Codecov

View File

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

View File

@@ -231,7 +231,7 @@ jobs:
- name: Detect duplicates using AI
id: ai_detection
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
uses: actions/ai-inference@v1.2.3
uses: actions/ai-inference@v1.2.8
with:
model: openai/gpt-4o
system-prompt: |

View File

@@ -57,7 +57,7 @@ jobs:
- name: Detect language using AI
id: ai_language_detection
if: steps.detect_language.outputs.should_continue == 'true'
uses: actions/ai-inference@v1.2.3
uses: actions/ai-inference@v1.2.8
with:
model: openai/gpt-4o-mini
system-prompt: |

View File

@@ -138,17 +138,17 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Download env_file
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: env_file
- name: Download build_constraints
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: build_constraints
- name: Download requirements_diff
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: requirements_diff
@@ -187,22 +187,22 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Download env_file
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: env_file
- name: Download build_constraints
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: build_constraints
- name: Download requirements_diff
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: requirements_diff
- name: Download requirements_all_wheels
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: requirements_all_wheels

View File

@@ -310,7 +310,6 @@ homeassistant.components.letpot.*
homeassistant.components.lidarr.*
homeassistant.components.lifx.*
homeassistant.components.light.*
homeassistant.components.linear_garage_door.*
homeassistant.components.linkplay.*
homeassistant.components.litejet.*
homeassistant.components.litterrobot.*

6
CODEOWNERS generated
View File

@@ -862,8 +862,6 @@ build.json @home-assistant/supervisor
/tests/components/lifx/ @Djelibeybi
/homeassistant/components/light/ @home-assistant/core
/tests/components/light/ @home-assistant/core
/homeassistant/components/linear_garage_door/ @IceBotYT
/tests/components/linear_garage_door/ @IceBotYT
/homeassistant/components/linkplay/ @Velleman
/tests/components/linkplay/ @Velleman
/homeassistant/components/linux_battery/ @fabaff
@@ -1599,6 +1597,8 @@ build.json @home-assistant/supervisor
/tests/components/todo/ @home-assistant/core
/homeassistant/components/todoist/ @boralyl
/tests/components/todoist/ @boralyl
/homeassistant/components/togrill/ @elupus
/tests/components/togrill/ @elupus
/homeassistant/components/tolo/ @MatthiasLohr
/tests/components/tolo/ @MatthiasLohr
/homeassistant/components/tomorrowio/ @raman325 @lymanepp
@@ -1613,8 +1613,6 @@ build.json @home-assistant/supervisor
/tests/components/tplink_omada/ @MarkGodwin
/homeassistant/components/traccar/ @ludeeus
/tests/components/traccar/ @ludeeus
/homeassistant/components/traccar_server/ @ludeeus
/tests/components/traccar_server/ @ludeeus
/homeassistant/components/trace/ @home-assistant/core
/tests/components/trace/ @home-assistant/core
/homeassistant/components/tractive/ @Danielhiversen @zhulik @bieniu

View File

@@ -120,6 +120,9 @@ class AuthStore:
new_user = models.User(**kwargs)
while new_user.id in self._users:
new_user = models.User(**kwargs)
self._users[new_user.id] = new_user
if credentials is None:

View File

@@ -10,7 +10,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
_PLATFORMS: list[Platform] = [Platform.SENSOR]
_PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.SENSOR,
]
async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:

View File

@@ -0,0 +1,106 @@
"""AirOS Binary Sensor component for Home Assistant."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AirOSConfigEntry, AirOSData, AirOSDataUpdateCoordinator
from .entity import AirOSEntity
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class AirOSBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describe an AirOS binary sensor."""
value_fn: Callable[[AirOSData], bool]
BINARY_SENSORS: tuple[AirOSBinarySensorEntityDescription, ...] = (
AirOSBinarySensorEntityDescription(
key="portfw",
translation_key="port_forwarding",
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.portfw,
),
AirOSBinarySensorEntityDescription(
key="dhcp_client",
translation_key="dhcp_client",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.services.dhcpc,
),
AirOSBinarySensorEntityDescription(
key="dhcp_server",
translation_key="dhcp_server",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.services.dhcpd,
entity_registry_enabled_default=False,
),
AirOSBinarySensorEntityDescription(
key="dhcp6_server",
translation_key="dhcp6_server",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.services.dhcp6d_stateful,
entity_registry_enabled_default=False,
),
AirOSBinarySensorEntityDescription(
key="pppoe",
translation_key="pppoe",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.services.pppoe,
entity_registry_enabled_default=False,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AirOSConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the AirOS binary sensors from a config entry."""
coordinator = config_entry.runtime_data
async_add_entities(
AirOSBinarySensor(coordinator, description) for description in BINARY_SENSORS
)
class AirOSBinarySensor(AirOSEntity, BinarySensorEntity):
"""Representation of a binary sensor."""
entity_description: AirOSBinarySensorEntityDescription
def __init__(
self,
coordinator: AirOSDataUpdateCoordinator,
description: AirOSBinarySensorEntityDescription,
) -> None:
"""Initialize the binary sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.data.host.device_id}_{description.key}"
@property
def is_on(self) -> bool:
"""Return the state of the binary sensor."""
return self.entity_description.value_fn(self.coordinator.data)

View File

@@ -6,11 +6,11 @@ import logging
from typing import Any
from airos.exceptions import (
ConnectionAuthenticationError,
ConnectionSetupError,
DataMissingError,
DeviceConnectionError,
KeyDataMissingError,
AirOSConnectionAuthenticationError,
AirOSConnectionSetupError,
AirOSDataMissingError,
AirOSDeviceConnectionError,
AirOSKeyDataMissingError,
)
import voluptuous as vol
@@ -59,13 +59,13 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
airos_data = await airos_device.status()
except (
ConnectionSetupError,
DeviceConnectionError,
AirOSConnectionSetupError,
AirOSDeviceConnectionError,
):
errors["base"] = "cannot_connect"
except (ConnectionAuthenticationError, DataMissingError):
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
errors["base"] = "invalid_auth"
except KeyDataMissingError:
except AirOSKeyDataMissingError:
errors["base"] = "key_data_missing"
except Exception:
_LOGGER.exception("Unexpected exception")

View File

@@ -6,10 +6,10 @@ import logging
from airos.airos8 import AirOS, AirOSData
from airos.exceptions import (
ConnectionAuthenticationError,
ConnectionSetupError,
DataMissingError,
DeviceConnectionError,
AirOSConnectionAuthenticationError,
AirOSConnectionSetupError,
AirOSDataMissingError,
AirOSDeviceConnectionError,
)
from homeassistant.config_entries import ConfigEntry
@@ -47,18 +47,22 @@ class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSData]):
try:
await self.airos_device.login()
return await self.airos_device.status()
except (ConnectionAuthenticationError,) as err:
except (AirOSConnectionAuthenticationError,) as err:
_LOGGER.exception("Error authenticating with airOS device")
raise ConfigEntryError(
translation_domain=DOMAIN, translation_key="invalid_auth"
) from err
except (ConnectionSetupError, DeviceConnectionError, TimeoutError) as err:
except (
AirOSConnectionSetupError,
AirOSDeviceConnectionError,
TimeoutError,
) as err:
_LOGGER.error("Error connecting to airOS device: %s", err)
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err
except (DataMissingError,) as err:
except (AirOSDataMissingError,) as err:
_LOGGER.error("Expected data not returned by airOS device: %s", err)
raise UpdateFailed(
translation_domain=DOMAIN,

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airos",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["airos==0.2.1"]
"requirements": ["airos==0.2.7"]
}

View File

@@ -54,9 +54,7 @@ rules:
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default:
status: todo
comment: prepared binary_sensors will provide this
entity-disabled-by-default: done
entity-translations: done
exception-translations: done
icon-translations:

View File

@@ -46,6 +46,7 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
translation_key="host_cpuload",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
value_fn=lambda data: data.host.cpuload,
entity_registry_enabled_default=False,
),
@@ -69,13 +70,6 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
translation_key="wireless_essid",
value_fn=lambda data: data.wireless.essid,
),
AirOSSensorEntityDescription(
key="wireless_mode",
translation_key="wireless_mode",
device_class=SensorDeviceClass.ENUM,
value_fn=lambda data: data.wireless.mode.value.replace("-", "_").lower(),
options=WIRELESS_MODE_OPTIONS,
),
AirOSSensorEntityDescription(
key="wireless_antenna_gain",
translation_key="wireless_antenna_gain",
@@ -90,6 +84,8 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
value_fn=lambda data: data.wireless.throughput.tx,
),
AirOSSensorEntityDescription(
@@ -98,6 +94,8 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
value_fn=lambda data: data.wireless.throughput.rx,
),
AirOSSensorEntityDescription(
@@ -106,6 +104,8 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
value_fn=lambda data: data.wireless.polling.dl_capacity,
),
AirOSSensorEntityDescription(
@@ -114,6 +114,8 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
value_fn=lambda data: data.wireless.polling.ul_capacity,
),
)

View File

@@ -26,6 +26,23 @@
}
},
"entity": {
"binary_sensor": {
"port_forwarding": {
"name": "Port forwarding"
},
"dhcp_client": {
"name": "DHCP client"
},
"dhcp_server": {
"name": "DHCP server"
},
"dhcp6_server": {
"name": "DHCPv6 server"
},
"pppoe": {
"name": "PPPoE link"
}
},
"sensor": {
"host_cpuload": {
"name": "CPU load"
@@ -43,13 +60,6 @@
"wireless_essid": {
"name": "Wireless SSID"
},
"wireless_mode": {
"name": "Wireless mode",
"state": {
"ap_ptp": "Access point",
"sta_ptp": "Station"
}
},
"wireless_antenna_gain": {
"name": "Antenna gain"
},

View File

@@ -7,21 +7,18 @@ import logging
from airthings import Airthings
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_SECRET
from .coordinator import AirthingsDataUpdateCoordinator
from .coordinator import AirthingsConfigEntry, AirthingsDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR]
SCAN_INTERVAL = timedelta(minutes=6)
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
"""Set up Airthings from a config entry."""
@@ -31,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
async_get_clientsession(hass),
)
coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
coordinator = AirthingsDataUpdateCoordinator(hass, airthings, entry)
await coordinator.async_config_entry_first_refresh()

View File

@@ -5,6 +5,7 @@ import logging
from airthings import Airthings, AirthingsDevice, AirthingsError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -13,15 +14,23 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=6)
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]):
"""Coordinator for Airthings data updates."""
def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None:
def __init__(
self,
hass: HomeAssistant,
airthings: Airthings,
config_entry: AirthingsConfigEntry,
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_method=self._update_method,
update_interval=SCAN_INTERVAL,

View File

@@ -9,7 +9,6 @@ DOMAIN: Final = "amberelectric"
CONF_SITE_NAME = "site_name"
CONF_SITE_ID = "site_id"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
ATTR_CHANNEL_TYPE = "channel_type"
ATTRIBUTION = "Data provided by Amber Electric"

View File

@@ -4,6 +4,7 @@ from amberelectric.models.channel import ChannelType
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
from homeassistant.core import (
HomeAssistant,
ServiceCall,
@@ -16,7 +17,6 @@ from homeassistant.util.json import JsonValueType
from .const import (
ATTR_CHANNEL_TYPE,
ATTR_CONFIG_ENTRY_ID,
CONTROLLED_LOAD_CHANNEL,
DOMAIN,
FEED_IN_CHANNEL,

View File

@@ -81,11 +81,15 @@ async def async_update_options(
async def async_migrate_integration(hass: HomeAssistant) -> None:
"""Migrate integration entry structure."""
entries = hass.config_entries.async_entries(DOMAIN)
# Make sure we get enabled config entries first
entries = sorted(
hass.config_entries.async_entries(DOMAIN),
key=lambda e: e.disabled_by is not None,
)
if not any(entry.version == 1 for entry in entries):
return
api_keys_entries: dict[str, ConfigEntry] = {}
api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {}
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
@@ -99,30 +103,61 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
)
if entry.data[CONF_API_KEY] not in api_keys_entries:
use_existing = True
api_keys_entries[entry.data[CONF_API_KEY]] = entry
all_disabled = all(
e.disabled_by is not None
for e in entries
if e.data[CONF_API_KEY] == entry.data[CONF_API_KEY]
)
api_keys_entries[entry.data[CONF_API_KEY]] = (entry, all_disabled)
parent_entry = api_keys_entries[entry.data[CONF_API_KEY]]
parent_entry, all_disabled = api_keys_entries[entry.data[CONF_API_KEY]]
hass.config_entries.async_add_subentry(parent_entry, subentry)
conversation_entity = entity_registry.async_get_entity_id(
conversation_entity_id = entity_registry.async_get_entity_id(
"conversation",
DOMAIN,
entry.entry_id,
)
if conversation_entity is not None:
entity_registry.async_update_entity(
conversation_entity,
config_entry_id=parent_entry.entry_id,
config_subentry_id=subentry.subentry_id,
new_unique_id=subentry.subentry_id,
)
device = device_registry.async_get_device(
identifiers={(DOMAIN, entry.entry_id)}
)
if conversation_entity_id is not None:
conversation_entity_entry = entity_registry.entities[conversation_entity_id]
entity_disabled_by = conversation_entity_entry.disabled_by
if (
entity_disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY
and not all_disabled
):
# Device and entity registries don't update the disabled_by flag
# when moving a device or entity from one config entry to another,
# so we need to do it manually.
entity_disabled_by = (
er.RegistryEntryDisabler.DEVICE
if device
else er.RegistryEntryDisabler.USER
)
entity_registry.async_update_entity(
conversation_entity_id,
config_entry_id=parent_entry.entry_id,
config_subentry_id=subentry.subentry_id,
disabled_by=entity_disabled_by,
new_unique_id=subentry.subentry_id,
)
if device is not None:
# Device and entity registries don't update the disabled_by flag when
# moving a device or entity from one config entry to another, so we
# need to do it manually.
device_disabled_by = device.disabled_by
if (
device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY
and not all_disabled
):
device_disabled_by = dr.DeviceEntryDisabler.USER
device_registry.async_update_device(
device.id,
disabled_by=device_disabled_by,
new_identifiers={(DOMAIN, subentry.subentry_id)},
add_config_subentry_id=subentry.subentry_id,
add_config_entry_id=parent_entry.entry_id,
@@ -147,7 +182,7 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
title=DEFAULT_CONVERSATION_NAME,
options={},
version=2,
minor_version=2,
minor_version=3,
)
@@ -173,6 +208,38 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AnthropicConfigEntry)
hass.config_entries.async_update_entry(entry, minor_version=2)
if entry.version == 2 and entry.minor_version == 2:
# Fix migration where the disabled_by flag was not set correctly.
# We can currently only correct this for enabled config entries,
# because migration does not run for disabled config entries. This
# is asserted in tests, and if that behavior is changed, we should
# correct also disabled config entries.
device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
entity_entries = er.async_entries_for_config_entry(
entity_registry, entry.entry_id
)
if entry.disabled_by is None:
# If the config entry is not disabled, we need to set the disabled_by
# flag on devices to USER, and on entities to DEVICE, if they are set
# to CONFIG_ENTRY.
for device in devices:
if device.disabled_by is not dr.DeviceEntryDisabler.CONFIG_ENTRY:
continue
device_registry.async_update_device(
device.id,
disabled_by=dr.DeviceEntryDisabler.USER,
)
for entity in entity_entries:
if entity.disabled_by is not er.RegistryEntryDisabler.CONFIG_ENTRY:
continue
entity_registry.async_update_entity(
entity.entity_id,
disabled_by=er.RegistryEntryDisabler.DEVICE,
)
hass.config_entries.async_update_entry(entry, minor_version=3)
LOGGER.debug(
"Migration to version %s:%s successful", entry.version, entry.minor_version
)

View File

@@ -75,7 +75,7 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Anthropic."""
VERSION = 2
MINOR_VERSION = 2
MINOR_VERSION = 3
async def async_step_user(
self, user_input: dict[str, Any] | None = None

View File

@@ -20,10 +20,8 @@ RECOMMENDED_THINKING_BUDGET = 0
MIN_THINKING_BUDGET = 1024
THINKING_MODELS = [
"claude-3-7-sonnet-20250219",
"claude-3-7-sonnet-latest",
"claude-opus-4-20250514",
"claude-opus-4-0",
"claude-sonnet-4-20250514",
"claude-3-7-sonnet",
"claude-sonnet-4-0",
"claude-opus-4-0",
"claude-opus-4-1",
]

View File

@@ -361,7 +361,10 @@ class AnthropicBaseLLMEntity(Entity):
"system": system.content,
"stream": True,
}
if model in THINKING_MODELS and thinking_budget >= MIN_THINKING_BUDGET:
if (
model.startswith(tuple(THINKING_MODELS))
and thinking_budget >= MIN_THINKING_BUDGET
):
model_args["thinking"] = ThinkingConfigEnabledParam(
type="enabled", budget_tokens=thinking_budget
)

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/anthropic",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["anthropic==0.52.0"]
"requirements": ["anthropic==0.62.0"]
}

View File

@@ -6,5 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/apcupsd",
"iot_class": "local_polling",
"loggers": ["apcaccess"],
"quality_scale": "bronze",
"requirements": ["aioapcaccess==0.4.2"]
}

View File

@@ -0,0 +1,98 @@
rules:
# Bronze
action-setup: done
appropriate-polling: done
brands: done
common-modules:
status: done
comment: |
Consider deriving a base entity.
config-flow-test-coverage:
status: done
comment: |
Consider looking into making a `mock_setup_entry` fixture that just automatically do this.
`test_config_flow_cannot_connect`: Needs to end in CREATE_ENTRY to test that its able to recover.
`test_config_flow_duplicate`: this test should be split in 2, one for testing duplicate host/port and one for duplicate serial number.
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
The integration does not provide any actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
Entities of this integration does not explicitly subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: |
The integration does not provide any actions.
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: |
The integration does not provide any additional options.
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow:
status: exempt
comment: |
The integration does not require authentication.
test-coverage:
status: todo
comment: |
Patch `aioapcaccess.request_status` where we use it.
# Gold
devices: done
diagnostics: done
discovery-update-info:
status: exempt
comment: |
This integration cannot be discovered.
discovery:
status: exempt
comment: |
This integration cannot be discovered.
docs-data-update: done
docs-examples: done
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices:
status: exempt
comment: |
The integration connects to a single service per configuration entry.
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow: done
repair-issues: done
stale-devices:
status: exempt
comment: |
This integration connect to a single service per configuration entry.
# Platinum
async-dependency: done
inject-websession:
status: exempt
comment: |
The integration does not connect via HTTP.
strict-typing: done

View File

@@ -14,7 +14,22 @@
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of the APC UPS Daemon",
"port": "The port the APC UPS Daemon is listening on"
},
"description": "Enter the host and port on which the apcupsd NIS is being served."
},
"reconfigure": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "[%key:component::apcupsd::config::step::user::data_description::host%]",
"port": "[%key:component::apcupsd::config::step::user::data_description::port%]"
},
"description": "[%key:component::apcupsd::config::step::user::description%]"
}
}
},

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import Callable, Mapping
from datetime import datetime, timedelta
import logging
from typing import Any
from typing import TYPE_CHECKING, Any
from pyasuswrt import AsusWrtError
@@ -40,6 +40,9 @@ from .const import (
SENSORS_CONNECTED_DEVICE,
)
if TYPE_CHECKING:
from . import AsusWrtConfigEntry
CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]
SCAN_INTERVAL = timedelta(seconds=30)
@@ -52,10 +55,13 @@ _LOGGER = logging.getLogger(__name__)
class AsusWrtSensorDataHandler:
"""Data handler for AsusWrt sensor."""
def __init__(self, hass: HomeAssistant, api: AsusWrtBridge) -> None:
def __init__(
self, hass: HomeAssistant, api: AsusWrtBridge, entry: AsusWrtConfigEntry
) -> None:
"""Initialize a AsusWrt sensor data handler."""
self._hass = hass
self._api = api
self._entry = entry
self._connected_devices = 0
async def _get_connected_devices(self) -> dict[str, int]:
@@ -91,6 +97,7 @@ class AsusWrtSensorDataHandler:
update_method=method,
# Polling interval. Will only be polled if there are subscribers.
update_interval=SCAN_INTERVAL if should_poll else None,
config_entry=self._entry,
)
await coordinator.async_refresh()
@@ -321,7 +328,9 @@ class AsusWrtRouter:
if self._sensors_data_handler:
return
self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api)
self._sensors_data_handler = AsusWrtSensorDataHandler(
self.hass, self._api, self._entry
)
self._sensors_data_handler.update_device_count(self._connected_devices)
sensors_types = await self._api.async_get_available_sensors()

View File

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

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
from datetime import timedelta
import logging
API_ABS_HUMID = "abs_humid"
API_CO2 = "carbon_dioxide"
API_DEW_POINT = "dew_point"
API_DUST = "dust"

View File

@@ -18,6 +18,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_CONNECTIONS,
ATTR_SW_VERSION,
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
@@ -33,6 +34,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
API_ABS_HUMID,
API_CO2,
API_DEW_POINT,
API_DUST,
@@ -120,6 +122,14 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
AwairSensorEntityDescription(
key=API_ABS_HUMID,
device_class=SensorDeviceClass.ABSOLUTE_HUMIDITY,
native_unit_of_measurement=CONCENTRATION_GRAMS_PER_CUBIC_METER,
unique_id_tag="absolute_humidity",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
)
SENSOR_TYPES_DUST: tuple[AwairSensorEntityDescription, ...] = (

View File

@@ -29,7 +29,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["axis"],
"requirements": ["axis==64"],
"requirements": ["axis==65"],
"ssdp": [
{
"manufacturer": "AXIS"

View File

@@ -127,7 +127,6 @@ class BackupConfigData:
schedule=BackupSchedule(
days=days,
recurrence=ScheduleRecurrence(data["schedule"]["recurrence"]),
state=ScheduleState(data["schedule"].get("state", ScheduleState.NEVER)),
time=time,
),
)
@@ -453,7 +452,6 @@ class StoredBackupSchedule(TypedDict):
days: list[Day]
recurrence: ScheduleRecurrence
state: ScheduleState
time: str | None
@@ -462,7 +460,6 @@ class ScheduleParametersDict(TypedDict, total=False):
days: list[Day]
recurrence: ScheduleRecurrence
state: ScheduleState
time: dt.time | None
@@ -486,32 +483,12 @@ class ScheduleRecurrence(StrEnum):
CUSTOM_DAYS = "custom_days"
class ScheduleState(StrEnum):
"""Represent the schedule recurrence.
This is deprecated and can be remove in HA Core 2025.8.
"""
NEVER = "never"
DAILY = "daily"
MONDAY = "mon"
TUESDAY = "tue"
WEDNESDAY = "wed"
THURSDAY = "thu"
FRIDAY = "fri"
SATURDAY = "sat"
SUNDAY = "sun"
@dataclass(kw_only=True)
class BackupSchedule:
"""Represent the backup schedule."""
days: list[Day] = field(default_factory=list)
recurrence: ScheduleRecurrence = ScheduleRecurrence.NEVER
# Although no longer used, state is kept for backwards compatibility.
# It can be removed in HA Core 2025.8.
state: ScheduleState = ScheduleState.NEVER
time: dt.time | None = None
cron_event: CronSim | None = field(init=False, default=None)
next_automatic_backup: datetime | None = field(init=False, default=None)
@@ -610,7 +587,6 @@ class BackupSchedule:
return StoredBackupSchedule(
days=self.days,
recurrence=self.recurrence,
state=self.state,
time=self.time.isoformat() if self.time else None,
)

View File

@@ -331,9 +331,6 @@ async def handle_config_info(
"""Send the stored backup config."""
manager = hass.data[DATA_MANAGER]
config = manager.config.data.to_dict()
# Remove state from schedule, it's not needed in the frontend
# mypy doesn't like deleting from TypedDict, ignore it
del config["schedule"]["state"] # type: ignore[misc]
connection.send_result(
msg["id"],
{

View File

@@ -25,7 +25,6 @@ SERVICE_TRIGGER = "trigger_camera"
SERVICE_SAVE_VIDEO = "save_video"
SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips"
SERVICE_SEND_PIN = "send_pin"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
PLATFORMS = [
Platform.ALARM_CONTROL_PANEL,

View File

@@ -5,12 +5,12 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_PIN
from homeassistant.const import ATTR_CONFIG_ENTRY_ID, CONF_PIN
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN, SERVICE_SEND_PIN
from .const import DOMAIN, SERVICE_SEND_PIN
from .coordinator import BlinkConfigEntry
SERVICE_SEND_PIN_SCHEMA = vol.Schema(

View File

@@ -388,12 +388,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE
scanner = HaScanner(mode, adapter, address)
scanner.async_setup()
try:
await scanner.async_start()
except (RuntimeError, ScannerStartError) as err:
raise ConfigEntryNotReady(
f"{adapter_human_name(adapter, address)}: {err}"
) from err
adapters = await manager.async_get_bluetooth_adapters()
details = adapters[adapter]
if entry.title == address:
@@ -401,8 +395,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry, title=adapter_title(adapter, details)
)
slots: int = details.get(ADAPTER_CONNECTION_SLOTS) or DEFAULT_CONNECTION_SLOTS
# Register the scanner before starting so
# any raw advertisement data can be processed
entry.async_on_unload(async_register_scanner(hass, scanner, connection_slots=slots))
await async_update_device(hass, entry, adapter, details)
try:
await scanner.async_start()
except (RuntimeError, ScannerStartError) as err:
raise ConfigEntryNotReady(
f"{adapter_human_name(adapter, address)}: {err}"
) from err
entry.async_on_unload(entry.add_update_listener(async_update_listener))
entry.async_on_unload(scanner.async_stop)
return True

View File

@@ -235,10 +235,9 @@ class HomeAssistantBluetoothManager(BluetoothManager):
def _async_save_scanner_history(self, scanner: BaseHaScanner) -> None:
"""Save the scanner history."""
if isinstance(scanner, BaseHaRemoteScanner):
self.storage.async_set_advertisement_history(
scanner.source, scanner.serialize_discovered_devices()
)
self.storage.async_set_advertisement_history(
scanner.source, scanner.serialize_discovered_devices()
)
def _async_unregister_scanner(
self, scanner: BaseHaScanner, unregister: CALLBACK_TYPE
@@ -285,9 +284,8 @@ class HomeAssistantBluetoothManager(BluetoothManager):
connection_slots: int | None = None,
) -> CALLBACK_TYPE:
"""Register a scanner."""
if isinstance(scanner, BaseHaRemoteScanner):
if history := self.storage.async_get_advertisement_history(scanner.source):
scanner.restore_discovered_devices(history)
if history := self.storage.async_get_advertisement_history(scanner.source):
scanner.restore_discovered_devices(history)
unregister = super().async_register_scanner(scanner, connection_slots)
return partial(self._async_unregister_scanner, scanner, unregister)

View File

@@ -20,7 +20,7 @@
"bluetooth-adapters==2.0.0",
"bluetooth-auto-recovery==1.5.2",
"bluetooth-data-tools==1.28.2",
"dbus-fast==2.44.2",
"habluetooth==4.0.1"
"dbus-fast==2.44.3",
"habluetooth==5.0.1"
]
}

View File

@@ -39,7 +39,13 @@ def async_setup(hass: HomeAssistant) -> None:
def serialize_service_info(
service_info: BluetoothServiceInfoBleak, time_diff: float
) -> dict[str, Any]:
"""Serialize a BluetoothServiceInfoBleak object."""
"""Serialize a BluetoothServiceInfoBleak object.
The raw field is included for:
1. Debugging - to see the actual advertisement packet
2. Data freshness - manufacturer_data and service_data are aggregated
across multiple advertisements, raw shows the latest packet only
"""
return {
"name": service_info.name,
"address": service_info.address,
@@ -57,6 +63,7 @@ def serialize_service_info(
"connectable": service_info.connectable,
"time": service_info.time + time_diff,
"tx_power": service_info.tx_power,
"raw": service_info.raw.hex() if service_info.raw else None,
}

View File

@@ -6,4 +6,3 @@ CONF_INSTALLER_CODE = "installer_code"
CONF_USER_CODE = "user_code"
ATTR_DATETIME = "datetime"
SERVICE_SET_DATE_TIME = "set_date_time"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"

View File

@@ -9,12 +9,13 @@ from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.util import dt as dt_util
from .const import ATTR_CONFIG_ENTRY_ID, ATTR_DATETIME, DOMAIN, SERVICE_SET_DATE_TIME
from .const import ATTR_DATETIME, DOMAIN, SERVICE_SET_DATE_TIME
from .types import BoschAlarmConfigEntry

View File

@@ -95,7 +95,7 @@
"name": "Battery missing"
},
"panel_fault_ac_fail": {
"name": "AC Failure"
"name": "AC failure"
},
"panel_fault_parameter_crc_fail_in_pif": {
"name": "CRC failure in panel configuration"

View File

@@ -64,6 +64,7 @@ class BroadlinkUpdateManager(ABC, Generic[_ApiT]):
device.hass,
_LOGGER,
name=f"{device.name} ({device.api.model} at {device.api.host[0]})",
config_entry=device.config,
update_method=self.async_update,
update_interval=self.SCAN_INTERVAL,
)

View File

@@ -2,7 +2,16 @@
import dataclasses
from bsblan import BSBLAN, BSBLANConfig, Device, Info, StaticState
from bsblan import (
BSBLAN,
BSBLANAuthError,
BSBLANConfig,
BSBLANConnectionError,
BSBLANError,
Device,
Info,
StaticState,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -13,9 +22,14 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryError,
ConfigEntryNotReady,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_PASSKEY
from .const import CONF_PASSKEY, DOMAIN
from .coordinator import BSBLanUpdateCoordinator
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER]
@@ -54,10 +68,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
coordinator = BSBLanUpdateCoordinator(hass, entry, bsblan)
await coordinator.async_config_entry_first_refresh()
# Fetch all required data concurrently
device = await bsblan.device()
info = await bsblan.info()
static = await bsblan.static_values()
try:
# Fetch all required data sequentially
device = await bsblan.device()
info = await bsblan.info()
static = await bsblan.static_values()
except BSBLANConnectionError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="setup_connection_error",
translation_placeholders={"host": entry.data[CONF_HOST]},
) from err
except BSBLANAuthError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="setup_auth_error",
) from err
except BSBLANError as err:
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="setup_general_error",
) from err
entry.runtime_data = BSBLanData(
client=bsblan,

View File

@@ -211,16 +211,16 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
),
)
# Use existing host and port, update auth credentials
self.host = existing_entry.data[CONF_HOST]
self.port = existing_entry.data[CONF_PORT]
self.passkey = user_input.get(CONF_PASSKEY) or existing_entry.data.get(
CONF_PASSKEY
)
self.username = user_input.get(CONF_USERNAME) or existing_entry.data.get(
CONF_USERNAME
)
self.password = user_input.get(CONF_PASSWORD)
# Combine existing data with the user's new input for validation.
# This correctly handles adding, changing, and clearing credentials.
config_data = existing_entry.data.copy()
config_data.update(user_input)
self.host = config_data[CONF_HOST]
self.port = config_data[CONF_PORT]
self.passkey = config_data.get(CONF_PASSKEY)
self.username = config_data.get(CONF_USERNAME)
self.password = config_data.get(CONF_PASSWORD)
try:
await self._get_bsblan_info(raise_on_progress=False, is_reauth=True)
@@ -267,17 +267,9 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
errors={"base": "cannot_connect"},
)
# Update the config entry with new auth data
data_updates = {}
if self.passkey is not None:
data_updates[CONF_PASSKEY] = self.passkey
if self.username is not None:
data_updates[CONF_USERNAME] = self.username
if self.password is not None:
data_updates[CONF_PASSWORD] = self.password
# Update only the fields that were provided by the user
return self.async_update_reload_and_abort(
existing_entry, data_updates=data_updates, reason="reauth_successful"
existing_entry, data_updates=user_input, reason="reauth_successful"
)
@callback

View File

@@ -41,6 +41,11 @@
"passkey": "[%key:component::bsblan::config::step::user::data::passkey%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"passkey": "[%key:component::bsblan::config::step::user::data_description::passkey%]",
"username": "[%key:component::bsblan::config::step::user::data_description::username%]",
"password": "[%key:component::bsblan::config::step::user::data_description::password%]"
}
}
},
@@ -66,6 +71,15 @@
},
"set_operation_mode_error": {
"message": "An error occurred while setting the operation mode"
},
"setup_connection_error": {
"message": "Failed to retrieve static device data from BSB-Lan device at {host}"
},
"setup_auth_error": {
"message": "Authentication failed while retrieving static device data"
},
"setup_general_error": {
"message": "An unknown error occurred while retrieving static device data"
}
},
"entity": {

View File

@@ -25,7 +25,7 @@
"services": {
"press": {
"name": "Press",
"description": "Press the button entity."
"description": "Presses a button entity."
}
}
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/caldav",
"iot_class": "cloud_polling",
"loggers": ["caldav", "vobject"],
"requirements": ["caldav==1.6.0", "icalendar==6.1.0"]
"requirements": ["caldav==1.6.0", "icalendar==6.3.1"]
}

View File

@@ -100,16 +100,10 @@ set_hvac_mode:
fields:
hvac_mode:
selector:
select:
options:
- "off"
- "auto"
- "cool"
- "dry"
- "fan_only"
- "heat_cool"
- "heat"
translation_key: hvac_mode
state:
hide_states:
- unavailable
- unknown
set_swing_mode:
target:
entity:

View File

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

View File

@@ -4,11 +4,13 @@ from __future__ import annotations
import asyncio
import logging
from typing import Any
from aiohttp.client_exceptions import ClientError
from hass_nabucasa import Cloud, cloud_api
from hass_nabucasa.payments_api import PaymentsApiError, SubscriptionInfo
from hass_nabucasa import (
Cloud,
MigratePaypalAgreementInfo,
PaymentsApiError,
SubscriptionInfo,
)
from .client import CloudClient
from .const import REQUEST_TIMEOUT
@@ -29,17 +31,17 @@ async def async_subscription_info(cloud: Cloud[CloudClient]) -> SubscriptionInfo
async def async_migrate_paypal_agreement(
cloud: Cloud[CloudClient],
) -> dict[str, Any] | None:
) -> MigratePaypalAgreementInfo | None:
"""Migrate a paypal agreement from legacy."""
try:
async with asyncio.timeout(REQUEST_TIMEOUT):
return await cloud_api.async_migrate_paypal_agreement(cloud)
return await cloud.payments.migrate_paypal_agreement()
except TimeoutError:
_LOGGER.error(
"A timeout of %s was reached while trying to start agreement migration",
REQUEST_TIMEOUT,
)
except ClientError as exception:
except PaymentsApiError as exception:
_LOGGER.error("Failed to start agreement migration - %s", exception)
return None

View File

@@ -161,7 +161,9 @@ class AssistantContent:
role: Literal["assistant"] = field(init=False, default="assistant")
agent_id: str
content: str | None = None
thinking_content: str | None = None
tool_calls: list[llm.ToolInput] | None = None
native: Any = None
@dataclass(frozen=True)
@@ -183,7 +185,9 @@ class AssistantContentDeltaDict(TypedDict, total=False):
role: Literal["assistant"]
content: str | None
thinking_content: str | None
tool_calls: list[llm.ToolInput] | None
native: Any
@dataclass
@@ -306,6 +310,8 @@ class ChatLog:
The keys content and tool_calls will be concatenated if they appear multiple times.
"""
current_content = ""
current_thinking_content = ""
current_native: Any = None
current_tool_calls: list[llm.ToolInput] = []
tool_call_tasks: dict[str, asyncio.Task] = {}
@@ -316,6 +322,14 @@ class ChatLog:
if "role" not in delta:
if delta_content := delta.get("content"):
current_content += delta_content
if delta_thinking_content := delta.get("thinking_content"):
current_thinking_content += delta_thinking_content
if delta_native := delta.get("native"):
if current_native is not None:
raise RuntimeError(
"Native content already set, cannot overwrite"
)
current_native = delta_native
if delta_tool_calls := delta.get("tool_calls"):
if self.llm_api is None:
raise ValueError("No LLM API configured")
@@ -337,11 +351,18 @@ class ChatLog:
raise ValueError(f"Only assistant role expected. Got {delta['role']}")
# Yield the previous message if it has content
if current_content or current_tool_calls:
if (
current_content
or current_thinking_content
or current_tool_calls
or current_native
):
content = AssistantContent(
agent_id=agent_id,
content=current_content or None,
thinking_content=current_thinking_content or None,
tool_calls=current_tool_calls or None,
native=current_native,
)
yield content
async for tool_result in self.async_add_assistant_content(
@@ -352,16 +373,25 @@ class ChatLog:
self.delta_listener(self, asdict(tool_result))
current_content = delta.get("content") or ""
current_thinking_content = delta.get("thinking_content") or ""
current_tool_calls = delta.get("tool_calls") or []
current_native = delta.get("native")
if self.delta_listener:
self.delta_listener(self, delta) # type: ignore[arg-type]
if current_content or current_tool_calls:
if (
current_content
or current_thinking_content
or current_tool_calls
or current_native
):
content = AssistantContent(
agent_id=agent_id,
content=current_content or None,
thinking_content=current_thinking_content or None,
tool_calls=current_tool_calls or None,
native=current_native,
)
yield content
async for tool_result in self.async_add_assistant_content(

View File

@@ -61,7 +61,7 @@ class DeviceCondition(Condition):
self._hass = hass
@classmethod
async def async_validate_condition_config(
async def async_validate_config(
cls, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate device condition config."""
@@ -69,7 +69,7 @@ class DeviceCondition(Condition):
hass, config, cv.DEVICE_CONDITION_SCHEMA, DeviceAutomationType.CONDITION
)
async def async_condition_from_config(self) -> condition.ConditionCheckerType:
async def async_get_checker(self) -> condition.ConditionCheckerType:
"""Test a device condition."""
platform = await async_get_device_automation_platform(
self._hass, self._config[CONF_DOMAIN], DeviceAutomationType.CONDITION
@@ -80,7 +80,7 @@ class DeviceCondition(Condition):
CONDITIONS: dict[str, type[Condition]] = {
"device": DeviceCondition,
"_device": DeviceCondition,
}

View File

@@ -16,7 +16,7 @@
"quality_scale": "internal",
"requirements": [
"aiodhcpwatcher==1.2.0",
"aiodiscover==2.7.0",
"aiodiscover==2.7.1",
"cached-ipaddress==0.10.0"
]
}

View File

@@ -18,6 +18,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# If path is relative, we assume relative to Home Assistant config dir
if not os.path.isabs(download_path):
download_path = hass.config.path(download_path)
hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_DOWNLOAD_DIR: download_path}
)
if not await hass.async_add_executor_job(os.path.isdir, download_path):
_LOGGER.error(

View File

@@ -11,6 +11,7 @@ import requests
import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
@@ -34,24 +35,33 @@ def download_file(service: ServiceCall) -> None:
entry = service.hass.config_entries.async_loaded_entries(DOMAIN)[0]
download_path = entry.data[CONF_DOWNLOAD_DIR]
url: str = service.data[ATTR_URL]
subdir: str | None = service.data.get(ATTR_SUBDIR)
target_filename: str | None = service.data.get(ATTR_FILENAME)
overwrite: bool = service.data[ATTR_OVERWRITE]
if subdir:
# Check the path
try:
raise_if_invalid_path(subdir)
except ValueError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="subdir_invalid",
translation_placeholders={"subdir": subdir},
) from err
if os.path.isabs(subdir):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="subdir_not_relative",
translation_placeholders={"subdir": subdir},
)
def do_download() -> None:
"""Download the file."""
final_path = None
filename = target_filename
try:
url = service.data[ATTR_URL]
subdir = service.data.get(ATTR_SUBDIR)
filename = service.data.get(ATTR_FILENAME)
overwrite = service.data.get(ATTR_OVERWRITE)
if subdir:
# Check the path
raise_if_invalid_path(subdir)
final_path = None
req = requests.get(url, stream=True, timeout=10)
if req.status_code != HTTPStatus.OK:

View File

@@ -12,6 +12,14 @@
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
},
"exceptions": {
"subdir_invalid": {
"message": "Invalid subdirectory, got: {subdir}"
},
"subdir_not_relative": {
"message": "Subdirectory must be relative, got: {subdir}"
}
},
"services": {
"download_file": {
"name": "Download file",

View File

@@ -20,7 +20,6 @@ from homeassistant.const import Platform
_LOGGER = logging.getLogger(__package__)
DOMAIN = "ecobee"
ATTR_CONFIG_ENTRY_ID = "entry_id"
ATTR_AVAILABLE_SENSORS = "available_sensors"
ATTR_ACTIVE_SENSORS = "active_sensors"

View File

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

View File

@@ -2,7 +2,6 @@
import logging
CONF_EXCLUDE_FEEDID = "exclude_feed_id"
CONF_ONLY_INCLUDE_FEEDID = "include_only_feed_id"
CONF_MESSAGE = "message"
CONF_SUCCESS = "success"

View File

@@ -34,13 +34,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .config_flow import sensor_name
from .const import (
CONF_EXCLUDE_FEEDID,
CONF_ONLY_INCLUDE_FEEDID,
FEED_ID,
FEED_NAME,
FEED_TAG,
)
from .const import CONF_ONLY_INCLUDE_FEEDID, FEED_ID, FEED_NAME, FEED_TAG
from .coordinator import EmonCMSConfigEntry, EmoncmsCoordinator
SENSORS: dict[str | None, SensorEntityDescription] = {
@@ -200,12 +194,11 @@ async def async_setup_entry(
) -> None:
"""Set up the emoncms sensors."""
name = sensor_name(entry.data[CONF_URL])
exclude_feeds = entry.data.get(CONF_EXCLUDE_FEEDID)
include_only_feeds = entry.options.get(
CONF_ONLY_INCLUDE_FEEDID, entry.data.get(CONF_ONLY_INCLUDE_FEEDID)
)
if exclude_feeds is None and include_only_feeds is None:
if include_only_feeds is None:
return
coordinator = entry.runtime_data

View File

@@ -111,14 +111,6 @@
}
},
"issues": {
"remove_value_template": {
"title": "The {domain} integration cannot start",
"description": "Configuring {domain} using YAML is being removed and the `{parameter}` parameter cannot be imported.\n\nPlease remove `{parameter}` from your `{domain}` yaml configuration and restart Home Assistant\n\nAlternatively, you may entirely remove the `{domain}` configuration from your configuration.yaml, restart Home Assistant, and add the {domain} integration manually."
},
"missing_include_only_feed_id": {
"title": "No feed synchronized with the {domain} sensor",
"description": "Configuring {domain} using YAML is being removed.\n\nPlease add manually the feeds you want to synchronize with the `configure` button of the integration."
},
"migrate_database": {
"title": "Upgrade your emoncms version",
"description": "Your [emoncms]({url}) does not ship a unique identifier.\n\nPlease upgrade to at least version 11.5.7 and migrate your emoncms database.\n\nMore info in the [emoncms documentation]({doc_url})"

View File

@@ -1,5 +1,6 @@
"""Data update coordinator for the Enigma2 integration."""
import asyncio
import logging
from openwebif.api import OpenWebIfDevice, OpenWebIfStatus
@@ -30,6 +31,8 @@ from .const import CONF_SOURCE_BOUQUET, DOMAIN
LOGGER = logging.getLogger(__package__)
SETUP_TIMEOUT = 10
type Enigma2ConfigEntry = ConfigEntry[Enigma2UpdateCoordinator]
@@ -79,7 +82,7 @@ class Enigma2UpdateCoordinator(DataUpdateCoordinator[OpenWebIfStatus]):
async def _async_setup(self) -> None:
"""Provide needed data to the device info."""
about = await self.device.get_about()
about = await asyncio.wait_for(self.device.get_about(), timeout=SETUP_TIMEOUT)
self.device.mac_address = about["info"]["ifaces"][0]["mac"]
self.device_info["model"] = about["info"]["model"]
self.device_info["manufacturer"] = about["info"]["brand"]

View File

@@ -117,6 +117,7 @@ class FreeboxRouter:
self.name: str = freebox_config["model_info"]["pretty_name"]
self.mac: str = freebox_config["mac"]
self._sw_v: str = freebox_config["firmware_version"]
self._hw_v: str | None = freebox_config.get("board_name")
self._attrs: dict[str, Any] = {}
self.supports_hosts = True
@@ -282,7 +283,9 @@ class FreeboxRouter:
identifiers={(DOMAIN, self.mac)},
manufacturer="Freebox SAS",
name=self.name,
model=self.name,
sw_version=self._sw_v,
hw_version=self._hw_v,
)
@property

View File

@@ -106,6 +106,7 @@ class FroniusSolarNet:
solar_net=self,
logger=_LOGGER,
name=f"{DOMAIN}_logger_{self.host}",
config_entry=self.config_entry,
)
await self.logger_coordinator.async_config_entry_first_refresh()
@@ -120,6 +121,7 @@ class FroniusSolarNet:
solar_net=self,
logger=_LOGGER,
name=f"{DOMAIN}_meters_{self.host}",
config_entry=self.config_entry,
)
)
@@ -129,6 +131,7 @@ class FroniusSolarNet:
solar_net=self,
logger=_LOGGER,
name=f"{DOMAIN}_ohmpilot_{self.host}",
config_entry=self.config_entry,
)
)
@@ -138,6 +141,7 @@ class FroniusSolarNet:
solar_net=self,
logger=_LOGGER,
name=f"{DOMAIN}_power_flow_{self.host}",
config_entry=self.config_entry,
)
)
@@ -147,6 +151,7 @@ class FroniusSolarNet:
solar_net=self,
logger=_LOGGER,
name=f"{DOMAIN}_storages_{self.host}",
config_entry=self.config_entry,
)
)
@@ -206,6 +211,7 @@ class FroniusSolarNet:
logger=_LOGGER,
name=_inverter_name,
inverter_info=_inverter_info,
config_entry=self.config_entry,
)
if self.config_entry.state == ConfigEntryState.LOADED:
await _coordinator.async_refresh()

View File

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

View File

@@ -124,7 +124,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
f"Error generating content due to content violations, reason: {response.prompt_feedback.block_reason_message}"
)
if not response.candidates[0].content.parts:
if (
not response.candidates
or not response.candidates[0].content
or not response.candidates[0].content.parts
):
raise HomeAssistantError("Unknown error generating content")
return {"text": response.text}

View File

@@ -377,7 +377,7 @@ async def google_generative_ai_config_option_schema(
value=api_model.name,
)
for api_model in sorted(
api_models, key=lambda x: x.name.lstrip("models/") or ""
api_models, key=lambda x: (x.name or "").lstrip("models/")
)
if (
api_model.name

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
import codecs
from collections.abc import AsyncGenerator, Callable
from collections.abc import AsyncGenerator, AsyncIterator, Callable
from dataclasses import replace
import mimetypes
from pathlib import Path
@@ -15,6 +15,7 @@ from google.genai.errors import APIError, ClientError
from google.genai.types import (
AutomaticFunctionCallingConfig,
Content,
ContentDict,
File,
FileState,
FunctionDeclaration,
@@ -23,9 +24,11 @@ from google.genai.types import (
GoogleSearch,
HarmCategory,
Part,
PartUnionDict,
SafetySetting,
Schema,
Tool,
ToolListUnion,
)
import voluptuous as vol
from voluptuous_openapi import convert
@@ -237,7 +240,7 @@ def _convert_content(
async def _transform_stream(
result: AsyncGenerator[GenerateContentResponse],
result: AsyncIterator[GenerateContentResponse],
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
new_message = True
try:
@@ -342,7 +345,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
"""Generate an answer for the chat log."""
options = self.subentry.data
tools: list[Tool | Callable[..., Any]] | None = None
tools: ToolListUnion | None = None
if chat_log.llm_api:
tools = [
_format_tool(tool, chat_log.llm_api.custom_serializer)
@@ -373,7 +376,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
else:
raise HomeAssistantError("Invalid prompt content")
messages: list[Content] = []
messages: list[Content | ContentDict] = []
# Google groups tool results, we do not. Group them before sending.
tool_results: list[conversation.ToolResultContent] = []
@@ -400,7 +403,10 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
# The SDK requires the first message to be a user message
# This is not the case if user used `start_conversation`
# Workaround from https://github.com/googleapis/python-genai/issues/529#issuecomment-2740964537
if messages and messages[0].role != "user":
if messages and (
(isinstance(messages[0], Content) and messages[0].role != "user")
or (isinstance(messages[0], dict) and messages[0]["role"] != "user")
):
messages.insert(
0,
Content(role="user", parts=[Part.from_text(text=" ")]),
@@ -440,14 +446,14 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
)
user_message = chat_log.content[-1]
assert isinstance(user_message, conversation.UserContent)
chat_request: str | list[Part] = user_message.content
chat_request: list[PartUnionDict] = [user_message.content]
if user_message.attachments:
files = await async_prepare_files_for_prompt(
self.hass,
self._genai_client,
[a.path for a in user_message.attachments],
)
chat_request = [chat_request, *files]
chat_request = [*chat_request, *files]
# To prevent infinite loops, we limit the number of iterations
for _iteration in range(MAX_TOOL_ITERATIONS):
@@ -464,15 +470,17 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
error = ERROR_GETTING_RESPONSE
raise HomeAssistantError(error) from err
chat_request = _create_google_tool_response_parts(
[
content
async for content in chat_log.async_add_delta_content_stream(
self.entity_id,
_transform_stream(chat_response_generator),
)
if isinstance(content, conversation.ToolResultContent)
]
chat_request = list(
_create_google_tool_response_parts(
[
content
async for content in chat_log.async_add_delta_content_stream(
self.entity_id,
_transform_stream(chat_response_generator),
)
if isinstance(content, conversation.ToolResultContent)
]
)
)
if not chat_log.unresponded_tool_results:
@@ -559,13 +567,13 @@ async def async_prepare_files_for_prompt(
await asyncio.sleep(FILE_POLLING_INTERVAL_SECONDS)
uploaded_file = await client.aio.files.get(
name=uploaded_file.name,
name=uploaded_file.name or "",
config={"http_options": {"timeout": TIMEOUT_MILLIS}},
)
if uploaded_file.state == FileState.FAILED:
raise HomeAssistantError(
f"File `{uploaded_file.name}` processing failed, reason: {uploaded_file.error.message}"
f"File `{uploaded_file.name}` processing failed, reason: {uploaded_file.error.message if uploaded_file.error else 'unknown'}"
)
prompt_parts = await hass.async_add_executor_job(upload_files)

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/google_generative_ai_conversation",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["google-genai==1.7.0"]
"requirements": ["google-genai==1.29.0"]
}

View File

@@ -123,10 +123,10 @@
},
"ai_task_data": {
"initiate_flow": {
"user": "Add Generate data with AI service",
"reconfigure": "Reconfigure Generate data with AI service"
"user": "Add AI task",
"reconfigure": "Reconfigure AI task"
},
"entry_type": "Generate data with AI service",
"entry_type": "AI task",
"step": {
"set_options": {
"data": {

View File

@@ -146,15 +146,41 @@ class GoogleGenerativeAITextToSpeechEntity(
)
)
)
def _extract_audio_parts(
response: types.GenerateContentResponse,
) -> tuple[bytes, str]:
if (
not response.candidates
or not response.candidates[0].content
or not response.candidates[0].content.parts
or not response.candidates[0].content.parts[0].inline_data
):
raise ValueError("No content returned from TTS generation")
data = response.candidates[0].content.parts[0].inline_data.data
mime_type = response.candidates[0].content.parts[0].inline_data.mime_type
if not isinstance(data, bytes):
raise TypeError(
f"Expected bytes for audio data, got {type(data).__name__}"
)
if not isinstance(mime_type, str):
raise TypeError(
f"Expected str for mime_type, got {type(mime_type).__name__}"
)
return data, mime_type
try:
response = await self._genai_client.aio.models.generate_content(
model=self.subentry.data.get(CONF_CHAT_MODEL, RECOMMENDED_TTS_MODEL),
contents=message,
config=config,
)
data = response.candidates[0].content.parts[0].inline_data.data
mime_type = response.candidates[0].content.parts[0].inline_data.mime_type
except (APIError, ClientError, ValueError) as exc:
data, mime_type = _extract_audio_parts(response)
except (APIError, ClientError, ValueError, TypeError) as exc:
LOGGER.error("Error during TTS: %s", exc, exc_info=True)
raise HomeAssistantError(exc) from exc
return "wav", convert_to_wav(data, mime_type)

View File

@@ -86,9 +86,11 @@ UNSUPPORTED_REASONS = {
UNSUPPORTED_SKIP_REPAIR = {"privileged"}
UNHEALTHY_REASONS = {
"docker",
"supervisor",
"setup",
"duplicate_os_installation",
"oserror_bad_message",
"privileged",
"setup",
"supervisor",
"untrusted",
}

View File

@@ -117,35 +117,43 @@
},
"unhealthy": {
"title": "Unhealthy system - {reason}",
"description": "System is currently unhealthy due to {reason}. Use the link to learn more and how to fix this."
"description": "System is currently unhealthy due to {reason}. For troubleshooting information, select Learn more."
},
"unhealthy_docker": {
"title": "Unhealthy system - Docker misconfigured",
"description": "System is currently unhealthy because Docker is configured incorrectly. Use the link to learn more and how to fix this."
"description": "System is currently unhealthy because Docker is configured incorrectly. For troubleshooting information, select Learn more."
},
"unhealthy_supervisor": {
"title": "Unhealthy system - Supervisor update failed",
"description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. Use the link to learn more and how to fix this."
"unhealthy_duplicate_os_installation": {
"description": "System is currently unhealthy because it has detected multiple Home Assistant OS installations. For troubleshooting information, select Learn more.",
"title": "Unhealthy system - Duplicate Home Assistant OS installation"
},
"unhealthy_setup": {
"title": "Unhealthy system - Setup failed",
"description": "System is currently unhealthy because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this."
"unhealthy_oserror_bad_message": {
"description": "System is currently unhealthy because the operating system has reported an OS error: Bad message. For troubleshooting information, select Learn more.",
"title": "Unhealthy system - Operating System error: Bad message"
},
"unhealthy_privileged": {
"title": "Unhealthy system - Not privileged",
"description": "System is currently unhealthy because it does not have privileged access to the docker runtime. Use the link to learn more and how to fix this."
"description": "System is currently unhealthy because it does not have privileged access to the docker runtime. For troubleshooting information, select Learn more."
},
"unhealthy_setup": {
"title": "Unhealthy system - Setup failed",
"description": "System is currently unhealthy because setup failed to complete. There are a number of reasons this can occur, For troubleshooting information, select Learn more."
},
"unhealthy_supervisor": {
"title": "Unhealthy system - Supervisor update failed",
"description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. For troubleshooting information, select Learn more."
},
"unhealthy_untrusted": {
"title": "Unhealthy system - Untrusted code",
"description": "System is currently unhealthy because it has detected untrusted code or images in use. Use the link to learn more and how to fix this."
"description": "System is currently unhealthy because it has detected untrusted code or images in use. For troubleshooting information, select Learn more."
},
"unsupported": {
"title": "Unsupported system - {reason}",
"description": "System is unsupported due to {reason}. Use the link to learn more and how to fix this."
"description": "System is unsupported due to {reason}. For troubleshooting information, select Learn more."
},
"unsupported_apparmor": {
"title": "Unsupported system - AppArmor issues",
"description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. Use the link to learn more and how to fix this."
"description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. For troubleshooting information, select Learn more."
},
"unsupported_cgroup_version": {
"title": "Unsupported system - CGroup version",
@@ -153,23 +161,23 @@
},
"unsupported_connectivity_check": {
"title": "Unsupported system - Connectivity check disabled",
"description": "System is unsupported because Home Assistant cannot determine when an Internet connection is available. Use the link to learn more and how to fix this."
"description": "System is unsupported because Home Assistant cannot determine when an Internet connection is available. For troubleshooting information, select Learn more."
},
"unsupported_content_trust": {
"title": "Unsupported system - Content-trust check disabled",
"description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. Use the link to learn more and how to fix this."
"description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. For troubleshooting information, select Learn more."
},
"unsupported_dbus": {
"title": "Unsupported system - D-Bus issues",
"description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. Use the link to learn more and how to fix this."
"description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. For troubleshooting information, select Learn more."
},
"unsupported_dns_server": {
"title": "Unsupported system - DNS server issues",
"description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. Use the link to learn more and how to fix this."
"description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. For troubleshooting information, select Learn more."
},
"unsupported_docker_configuration": {
"title": "Unsupported system - Docker misconfigured",
"description": "System is unsupported because the Docker daemon is running in an unexpected way. Use the link to learn more and how to fix this."
"description": "System is unsupported because the Docker daemon is running in an unexpected way. For troubleshooting information, select Learn more."
},
"unsupported_docker_version": {
"title": "Unsupported system - Docker version",
@@ -177,15 +185,15 @@
},
"unsupported_job_conditions": {
"title": "Unsupported system - Protections disabled",
"description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. Use the link to learn more and how to fix this."
"description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. For troubleshooting information, select Learn more."
},
"unsupported_lxc": {
"title": "Unsupported system - LXC detected",
"description": "System is unsupported because it is being run in an LXC virtual machine. Use the link to learn more and how to fix this."
"description": "System is unsupported because it is being run in an LXC virtual machine. For troubleshooting information, select Learn more."
},
"unsupported_network_manager": {
"title": "Unsupported system - Network Manager issues",
"description": "System is unsupported because Network Manager is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
"description": "System is unsupported because Network Manager is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
},
"unsupported_os": {
"title": "Unsupported system - Operating System",
@@ -193,39 +201,43 @@
},
"unsupported_os_agent": {
"title": "Unsupported system - OS-Agent issues",
"description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
"description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
},
"unsupported_restart_policy": {
"title": "Unsupported system - Container restart policy",
"description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. Use the link to learn more and how to fix this."
"description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. For troubleshooting information, select Learn more."
},
"unsupported_software": {
"title": "Unsupported system - Unsupported software",
"description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. Use the link to learn more and how to fix this."
"description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. For troubleshooting information, select Learn more."
},
"unsupported_source_mods": {
"title": "Unsupported system - Supervisor source modifications",
"description": "System is unsupported because Supervisor source code has been modified. Use the link to learn more and how to fix this."
"description": "System is unsupported because Supervisor source code has been modified. For troubleshooting information, select Learn more."
},
"unsupported_supervisor_version": {
"title": "Unsupported system - Supervisor version",
"description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. Use the link to learn more and how to fix this."
"description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. For troubleshooting information, select Learn more."
},
"unsupported_systemd": {
"title": "Unsupported system - Systemd issues",
"description": "System is unsupported because Systemd is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
"description": "System is unsupported because Systemd is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
},
"unsupported_systemd_journal": {
"title": "Unsupported system - Systemd Journal issues",
"description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
"description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
},
"unsupported_systemd_resolved": {
"title": "Unsupported system - Systemd-Resolved issues",
"description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
"description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
},
"unsupported_virtualization_image": {
"title": "Unsupported system - Incorrect OS image for virtualization",
"description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. Use the link to learn more and how to fix this."
"description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. For troubleshooting information, select Learn more."
},
"unsupported_os_version": {
"title": "Unsupported system - Home Assistant OS version",
"description": "System is unsupported because the Home Assistant OS version in use is not supported. For troubleshooting information, select Learn more."
}
},
"entity": {

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.77", "babel==2.15.0"]
"requirements": ["holidays==0.78", "babel==2.15.0"]
}

View File

@@ -11,10 +11,16 @@ from pyHomee import (
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import DOMAIN
from .const import (
DOMAIN,
RESULT_CANNOT_CONNECT,
RESULT_INVALID_AUTH,
RESULT_UNKNOWN_ERROR,
)
_LOGGER = logging.getLogger(__name__)
@@ -33,60 +39,137 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
homee: Homee
_host: str
_name: str
_reauth_host: str
_reauth_username: str
async def _connect_homee(self) -> dict[str, str]:
errors: dict[str, str] = {}
try:
await self.homee.get_access_token()
except HomeeConnectionFailedException:
errors["base"] = RESULT_CANNOT_CONNECT
except HomeeAuthenticationFailedException:
errors["base"] = RESULT_INVALID_AUTH
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = RESULT_UNKNOWN_ERROR
else:
_LOGGER.info("Got access token for homee")
self.hass.loop.create_task(self.homee.run())
_LOGGER.debug("Homee task created")
await self.homee.wait_until_connected()
_LOGGER.info("Homee connected")
self.homee.disconnect()
_LOGGER.debug("Homee disconnecting")
await self.homee.wait_until_disconnected()
_LOGGER.info("Homee config successfully tested")
await self.async_set_unique_id(
self.homee.settings.uid, raise_on_progress=self.source != SOURCE_USER
)
self._abort_if_unique_id_configured()
_LOGGER.info("Created new homee entry with ID %s", self.homee.settings.uid)
return errors
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial user step."""
errors: dict[str, str] = {}
errors = {}
if user_input is not None:
self.homee = Homee(
user_input[CONF_HOST],
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
)
errors = await self._connect_homee()
try:
await self.homee.get_access_token()
except HomeeConnectionFailedException:
errors["base"] = "cannot_connect"
except HomeeAuthenticationFailedException:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
_LOGGER.info("Got access token for homee")
self.hass.loop.create_task(self.homee.run())
_LOGGER.debug("Homee task created")
await self.homee.wait_until_connected()
_LOGGER.info("Homee connected")
self.homee.disconnect()
_LOGGER.debug("Homee disconnecting")
await self.homee.wait_until_disconnected()
_LOGGER.info("Homee config successfully tested")
await self.async_set_unique_id(self.homee.settings.uid)
self._abort_if_unique_id_configured()
_LOGGER.info(
"Created new homee entry with ID %s", self.homee.settings.uid
)
if not errors:
return self.async_create_entry(
title=f"{self.homee.settings.homee_name} ({self.homee.host})",
data=user_input,
)
return self.async_show_form(
step_id="user",
data_schema=AUTH_SCHEMA,
errors=errors,
)
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle zeroconf discovery."""
# Ensure that an IPv4 address is received
self._host = discovery_info.host
self._name = discovery_info.hostname[6:18]
if discovery_info.ip_address.version == 6:
return self.async_abort(reason="ipv6_address")
await self.async_set_unique_id(self._name)
self._abort_if_unique_id_configured(updates={CONF_HOST: self._host})
# Cause an auth-error to see if homee is reachable.
self.homee = Homee(
self._host,
"dummy_username",
"dummy_password",
)
errors = await self._connect_homee()
if errors["base"] != RESULT_INVALID_AUTH:
return self.async_abort(reason=RESULT_CANNOT_CONNECT)
self.context["title_placeholders"] = {"name": self._name, "host": self._host}
return await self.async_step_zeroconf_confirm()
async def async_step_zeroconf_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm the configuration of the device."""
errors: dict[str, str] = {}
if user_input is not None:
self.homee = Homee(
self._host,
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
)
errors = await self._connect_homee()
if not errors:
return self.async_create_entry(
title=f"{self.homee.settings.homee_name} ({self.homee.host})",
data={
CONF_HOST: self._host,
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
},
)
return self.async_show_form(
step_id="zeroconf_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
),
errors=errors,
description_placeholders={
CONF_HOST: self._name,
},
last_step=True,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
@@ -108,12 +191,12 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
try:
await self.homee.get_access_token()
except HomeeConnectionFailedException:
errors["base"] = "cannot_connect"
errors["base"] = RESULT_CANNOT_CONNECT
except HomeeAuthenticationFailedException:
errors["base"] = "invalid_auth"
errors["base"] = RESULT_INVALID_AUTH
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
errors["base"] = RESULT_UNKNOWN_ERROR
else:
self.hass.loop.create_task(self.homee.run())
await self.homee.wait_until_connected()
@@ -161,12 +244,12 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
try:
await self.homee.get_access_token()
except HomeeConnectionFailedException:
errors["base"] = "cannot_connect"
errors["base"] = RESULT_CANNOT_CONNECT
except HomeeAuthenticationFailedException:
errors["base"] = "invalid_auth"
errors["base"] = RESULT_INVALID_AUTH
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
errors["base"] = RESULT_UNKNOWN_ERROR
else:
self.hass.loop.create_task(self.homee.run())
await self.homee.wait_until_connected()

View File

@@ -20,6 +20,11 @@ from homeassistant.const import (
# General
DOMAIN = "homee"
# Error strings
RESULT_CANNOT_CONNECT = "cannot_connect"
RESULT_INVALID_AUTH = "invalid_auth"
RESULT_UNKNOWN_ERROR = "unknown"
# Sensor mappings
HOMEE_UNIT_TO_HA_UNIT = {
"": None,

View File

@@ -8,5 +8,11 @@
"iot_class": "local_push",
"loggers": ["homee"],
"quality_scale": "silver",
"requirements": ["pyHomee==1.2.10"]
"requirements": ["pyHomee==1.2.10"],
"zeroconf": [
{
"type": "_ssh._tcp.local.",
"name": "homee-*"
}
]
}

View File

@@ -46,6 +46,17 @@
"data_description": {
"host": "[%key:component::homee::config::step::user::data_description::host%]"
}
},
"zeroconf_confirm": {
"title": "Configure discovered homee {host}",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"username": "[%key:component::homee::config::step::user::data_description::username%]",
"password": "[%key:component::homee::config::step::user::data_description::password%]"
}
}
}
},

View File

@@ -24,6 +24,7 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_CONFIG_ENTRY_ID,
ATTR_HW_VERSION,
ATTR_MODEL,
ATTR_SW_VERSION,
@@ -54,7 +55,6 @@ from homeassistant.helpers.typing import ConfigType
from .const import (
ADMIN_SERVICES,
ALL_KEYS,
ATTR_CONFIG_ENTRY_ID,
CONF_MANUFACTURER,
CONF_UNAUTHENTICATED_MODE,
CONF_UPNP_UDN,

View File

@@ -2,8 +2,6 @@
DOMAIN = "huawei_lte"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
CONF_MANUFACTURER = "manufacturer"
CONF_TRACK_WIRED_CLIENTS = "track_wired_clients"
CONF_UNAUTHENTICATED_MODE = "unauthenticated_mode"

View File

@@ -8,12 +8,12 @@ from typing import Any
from huawei_lte_api.exceptions import ResponseErrorException
from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService
from homeassistant.const import CONF_RECIPIENT
from homeassistant.const import ATTR_CONFIG_ENTRY_ID, CONF_RECIPIENT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import Router
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)

View File

@@ -163,6 +163,7 @@ async def async_setup_entry(
name="light",
update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update),
update_interval=SCAN_INTERVAL,
config_entry=config_entry,
request_refresh_debouncer=Debouncer(
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
),
@@ -197,6 +198,7 @@ async def async_setup_entry(
name="group",
update_method=partial(async_safe_fetch, bridge, bridge.api.groups.update),
update_interval=SCAN_INTERVAL,
config_entry=config_entry,
request_refresh_debouncer=Debouncer(
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
),

View File

@@ -53,6 +53,7 @@ class SensorManager:
LOGGER,
name="sensor",
update_method=self.async_update_data,
config_entry=bridge.config_entry,
update_interval=self.SCAN_INTERVAL,
request_refresh_debouncer=debounce.Debouncer(
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True

View File

@@ -21,6 +21,7 @@ PLATFORMS: list[Platform] = [
Platform.BUTTON,
Platform.CALENDAR,
Platform.DEVICE_TRACKER,
Platform.EVENT,
Platform.LAWN_MOWER,
Platform.NUMBER,
Platform.SELECT,

View File

@@ -21,6 +21,20 @@ _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
async def async_reset_cutting_blade_usage_time(
session: AutomowerSession,
mower_id: str,
) -> None:
"""Reset cutting blade usage time."""
await session.commands.reset_cutting_blade_usage_time(mower_id)
def reset_cutting_blade_usage_time_availability(data: MowerAttributes) -> bool:
"""Return True if blade usage time is greater than 0."""
value = data.statistics.cutting_blade_usage_time
return value is not None and value > 0
@dataclass(frozen=True, kw_only=True)
class AutomowerButtonEntityDescription(ButtonEntityDescription):
"""Describes Automower button entities."""
@@ -28,6 +42,7 @@ class AutomowerButtonEntityDescription(ButtonEntityDescription):
available_fn: Callable[[MowerAttributes], bool] = lambda _: True
exists_fn: Callable[[MowerAttributes], bool] = lambda _: True
press_fn: Callable[[AutomowerSession, str], Awaitable[Any]]
poll_after_sending: bool = False
MOWER_BUTTON_TYPES: tuple[AutomowerButtonEntityDescription, ...] = (
@@ -43,6 +58,14 @@ MOWER_BUTTON_TYPES: tuple[AutomowerButtonEntityDescription, ...] = (
translation_key="sync_clock",
press_fn=lambda session, mower_id: session.commands.set_datetime(mower_id),
),
AutomowerButtonEntityDescription(
key="reset_cutting_blade_usage_time",
translation_key="reset_cutting_blade_usage_time",
available_fn=reset_cutting_blade_usage_time_availability,
exists_fn=lambda data: data.statistics.cutting_blade_usage_time is not None,
press_fn=async_reset_cutting_blade_usage_time,
poll_after_sending=True,
),
)
@@ -93,3 +116,5 @@ class AutomowerButtonEntity(AutomowerControlEntity, ButtonEntity):
async def async_press(self) -> None:
"""Send a command to the mower."""
await self.entity_description.press_fn(self.coordinator.api, self.mower_id)
if self.entity_description.poll_after_sending:
await self.coordinator.async_request_refresh()

View File

@@ -17,3 +17,128 @@ ERROR_STATES = [
MowerStates.WAIT_POWER_UP,
MowerStates.WAIT_UPDATING,
]
ERROR_KEYS = [
"alarm_mower_in_motion",
"alarm_mower_lifted",
"alarm_mower_stopped",
"alarm_mower_switched_off",
"alarm_mower_tilted",
"alarm_outside_geofence",
"angular_sensor_problem",
"battery_problem",
"battery_restriction_due_to_ambient_temperature",
"can_error",
"charging_current_too_high",
"charging_station_blocked",
"charging_system_problem",
"collision_sensor_defect",
"collision_sensor_error",
"collision_sensor_problem_front",
"collision_sensor_problem_rear",
"com_board_not_available",
"communication_circuit_board_sw_must_be_updated",
"complex_working_area",
"connection_changed",
"connection_not_changed",
"connectivity_problem",
"connectivity_settings_restored",
"cutting_drive_motor_1_defect",
"cutting_drive_motor_2_defect",
"cutting_drive_motor_3_defect",
"cutting_height_blocked",
"cutting_height_problem",
"cutting_height_problem_curr",
"cutting_height_problem_dir",
"cutting_height_problem_drive",
"cutting_motor_problem",
"cutting_stopped_slope_too_steep",
"cutting_system_blocked",
"cutting_system_imbalance_warning",
"cutting_system_major_imbalance",
"destination_not_reachable",
"difficult_finding_home",
"docking_sensor_defect",
"electronic_problem",
"empty_battery",
"folding_cutting_deck_sensor_defect",
"folding_sensor_activated",
"geofence_problem",
"gps_navigation_problem",
"guide_1_not_found",
"guide_2_not_found",
"guide_3_not_found",
"guide_calibration_accomplished",
"guide_calibration_failed",
"high_charging_power_loss",
"high_internal_power_loss",
"high_internal_temperature",
"internal_voltage_error",
"invalid_battery_combination_invalid_combination_of_different_battery_types",
"invalid_sub_device_combination",
"invalid_system_configuration",
"left_brush_motor_overloaded",
"lift_sensor_defect",
"lifted",
"limited_cutting_height_range",
"loop_sensor_defect",
"loop_sensor_problem_front",
"loop_sensor_problem_left",
"loop_sensor_problem_rear",
"loop_sensor_problem_right",
"low_battery",
"memory_circuit_problem",
"mower_lifted",
"mower_tilted",
"no_accurate_position_from_satellites",
"no_confirmed_position",
"no_drive",
"no_loop_signal",
"no_power_in_charging_station",
"no_response_from_charger",
"outside_working_area",
"poor_signal_quality",
"reference_station_communication_problem",
"right_brush_motor_overloaded",
"safety_function_faulty",
"settings_restored",
"sim_card_locked",
"sim_card_not_found",
"sim_card_requires_pin",
"slipped_mower_has_slipped_situation_not_solved_with_moving_pattern",
"slope_too_steep",
"sms_could_not_be_sent",
"stop_button_problem",
"stuck_in_charging_station",
"switch_cord_problem",
"temporary_battery_problem",
"tilt_sensor_problem",
"too_high_discharge_current",
"too_high_internal_current",
"trapped",
"ultrasonic_problem",
"ultrasonic_sensor_1_defect",
"ultrasonic_sensor_2_defect",
"ultrasonic_sensor_3_defect",
"ultrasonic_sensor_4_defect",
"unexpected_cutting_height_adj",
"unexpected_error",
"upside_down",
"weak_gps_signal",
"wheel_drive_problem_left",
"wheel_drive_problem_rear_left",
"wheel_drive_problem_rear_right",
"wheel_drive_problem_right",
"wheel_motor_blocked_left",
"wheel_motor_blocked_rear_left",
"wheel_motor_blocked_rear_right",
"wheel_motor_blocked_right",
"wheel_motor_overloaded_left",
"wheel_motor_overloaded_rear_left",
"wheel_motor_overloaded_rear_right",
"wheel_motor_overloaded_right",
"work_area_not_valid",
"wrong_loop_signal",
"wrong_pin_code",
"zone_generator_problem",
]

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
from collections.abc import Callable
from datetime import timedelta
from datetime import datetime, timedelta
import logging
from typing import override
@@ -12,9 +12,10 @@ from aioautomower.exceptions import (
ApiError,
AuthError,
HusqvarnaTimeoutError,
HusqvarnaWSClientError,
HusqvarnaWSServerHandshakeError,
)
from aioautomower.model import MowerDictionary
from aioautomower.model import MowerDictionary, MowerStates
from aioautomower.session import AutomowerSession
from homeassistant.config_entries import ConfigEntry
@@ -29,7 +30,9 @@ _LOGGER = logging.getLogger(__name__)
MAX_WS_RECONNECT_TIME = 600
SCAN_INTERVAL = timedelta(minutes=8)
DEFAULT_RECONNECT_TIME = 2 # Define a default reconnect time
PONG_TIMEOUT = timedelta(seconds=90)
PING_INTERVAL = timedelta(seconds=10)
PING_TIMEOUT = timedelta(seconds=5)
type AutomowerConfigEntry = ConfigEntry[AutomowerDataUpdateCoordinator]
@@ -58,6 +61,9 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
self.new_devices_callbacks: list[Callable[[set[str]], None]] = []
self.new_zones_callbacks: list[Callable[[str, set[str]], None]] = []
self.new_areas_callbacks: list[Callable[[str, set[int]], None]] = []
self.pong: datetime | None = None
self.websocket_alive: bool = False
self._watchdog_task: asyncio.Task | None = None
@override
@callback
@@ -71,6 +77,18 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
await self.api.connect()
self.api.register_data_callback(self.handle_websocket_updates)
self.ws_connected = True
def start_watchdog() -> None:
if self._watchdog_task is not None and not self._watchdog_task.done():
_LOGGER.debug("Cancelling previous watchdog task")
self._watchdog_task.cancel()
self._watchdog_task = self.config_entry.async_create_background_task(
self.hass,
self._pong_watchdog(),
"websocket_watchdog",
)
self.api.register_ws_ready_callback(start_watchdog)
try:
data = await self.api.get_status()
except ApiError as err:
@@ -93,6 +111,19 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
mower_data.capabilities.work_areas for mower_data in self.data.values()
):
self._async_add_remove_work_areas()
if (
not self._should_poll()
and self.update_interval is not None
and self.websocket_alive
):
_LOGGER.debug("All mowers inactive and websocket alive: stop polling")
self.update_interval = None
if self.update_interval is None and self._should_poll():
_LOGGER.debug(
"Polling re-enabled via WebSocket: at least one mower active"
)
self.update_interval = SCAN_INTERVAL
self.hass.async_create_task(self.async_request_refresh())
@callback
def handle_websocket_updates(self, ws_data: MowerDictionary) -> None:
@@ -142,7 +173,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
# Reset reconnect time after successful connection
self.reconnect_time = DEFAULT_RECONNECT_TIME
await automower_client.start_listening()
except HusqvarnaWSServerHandshakeError as err:
except (HusqvarnaWSServerHandshakeError, HusqvarnaWSClientError) as err:
_LOGGER.debug(
"Failed to connect to websocket. Trying to reconnect: %s",
err,
@@ -161,6 +192,30 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
"reconnect_task",
)
def _should_poll(self) -> bool:
"""Return True if at least one mower is connected and at least one is not OFF."""
return any(mower.metadata.connected for mower in self.data.values()) and any(
mower.mower.state != MowerStates.OFF for mower in self.data.values()
)
async def _pong_watchdog(self) -> None:
_LOGGER.debug("Watchdog started")
try:
while True:
_LOGGER.debug("Sending ping")
self.websocket_alive = await self.api.send_empty_message()
_LOGGER.debug("Ping result: %s", self.websocket_alive)
await asyncio.sleep(60)
_LOGGER.debug("Websocket alive %s", self.websocket_alive)
if not self.websocket_alive:
_LOGGER.debug("No pong received → restart polling")
if self.update_interval is None:
self.update_interval = SCAN_INTERVAL
await self.async_request_refresh()
except asyncio.CancelledError:
_LOGGER.debug("Watchdog cancelled")
def _async_add_remove_devices(self) -> None:
"""Add new devices and remove orphaned devices from the registry."""
current_devices = set(self.data)

View File

@@ -0,0 +1,108 @@
"""Creates the event entities for supported mowers."""
from collections.abc import Callable
from aioautomower.model import SingleMessageData
from homeassistant.components.event import (
DOMAIN as EVENT_DOMAIN,
EventEntity,
EventEntityDescription,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AutomowerConfigEntry
from .const import ERROR_KEYS
from .coordinator import AutomowerDataUpdateCoordinator
from .entity import AutomowerBaseEntity
PARALLEL_UPDATES = 1
ATTR_SEVERITY = "severity"
ATTR_LATITUDE = "latitude"
ATTR_LONGITUDE = "longitude"
ATTR_DATE_TIME = "date_time"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AutomowerConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Automower message event entities.
Entities are created dynamically based on messages received from the API,
but only for mowers that support message events.
"""
coordinator = config_entry.runtime_data
entity_registry = er.async_get(hass)
restored_mowers = {
entry.unique_id.removesuffix("_message")
for entry in er.async_entries_for_config_entry(
entity_registry, config_entry.entry_id
)
if entry.domain == EVENT_DOMAIN
}
async_add_entities(
AutomowerMessageEventEntity(mower_id, coordinator)
for mower_id in restored_mowers
if mower_id in coordinator.data
)
@callback
def _handle_message(msg: SingleMessageData) -> None:
if msg.id in restored_mowers:
return
restored_mowers.add(msg.id)
async_add_entities([AutomowerMessageEventEntity(msg.id, coordinator)])
coordinator.api.register_single_message_callback(_handle_message)
class AutomowerMessageEventEntity(AutomowerBaseEntity, EventEntity):
"""EventEntity for Automower message events."""
entity_description: EventEntityDescription
_message_cb: Callable[[SingleMessageData], None]
_attr_translation_key = "message"
_attr_event_types = ERROR_KEYS
def __init__(
self,
mower_id: str,
coordinator: AutomowerDataUpdateCoordinator,
) -> None:
"""Initialize Automower message event entity."""
super().__init__(mower_id, coordinator)
self._attr_unique_id = f"{mower_id}_message"
@callback
def _handle(self, msg: SingleMessageData) -> None:
"""Handle a message event from the API and trigger the event entity if it matches the entity's mower ID."""
if msg.id != self.mower_id:
return
message = msg.attributes.message
self._trigger_event(
message.code,
{
ATTR_SEVERITY: message.severity,
ATTR_LATITUDE: message.latitude,
ATTR_LONGITUDE: message.longitude,
ATTR_DATE_TIME: message.time,
},
)
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Register callback when entity is added to hass."""
await super().async_added_to_hass()
self.coordinator.api.register_single_message_callback(self._handle)
async def async_will_remove_from_hass(self) -> None:
"""Unregister WebSocket callback when entity is removed."""
self.coordinator.api.unregister_single_message_callback(self._handle)

View File

@@ -8,6 +8,14 @@
"button": {
"sync_clock": {
"default": "mdi:clock-check-outline"
},
"reset_cutting_blade_usage_time": {
"default": "mdi:saw-blade"
}
},
"event": {
"message": {
"default": "mdi:alert-circle-check-outline"
}
},
"number": {

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"quality_scale": "silver",
"requirements": ["aioautomower==2.1.1"]
"requirements": ["aioautomower==2.1.2"]
}

View File

@@ -28,7 +28,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from . import AutomowerConfigEntry
from .const import ERROR_STATES
from .const import ERROR_KEYS, ERROR_STATES
from .coordinator import AutomowerDataUpdateCoordinator
from .entity import (
AutomowerBaseEntity,
@@ -42,135 +42,8 @@ PARALLEL_UPDATES = 0
ATTR_WORK_AREA_ID_ASSIGNMENT = "work_area_id_assignment"
ERROR_KEYS = [
"alarm_mower_in_motion",
"alarm_mower_lifted",
"alarm_mower_stopped",
"alarm_mower_switched_off",
"alarm_mower_tilted",
"alarm_outside_geofence",
"angular_sensor_problem",
"battery_problem",
"battery_restriction_due_to_ambient_temperature",
"can_error",
"charging_current_too_high",
"charging_station_blocked",
"charging_system_problem",
"collision_sensor_defect",
"collision_sensor_error",
"collision_sensor_problem_front",
"collision_sensor_problem_rear",
"com_board_not_available",
"communication_circuit_board_sw_must_be_updated",
"complex_working_area",
"connection_changed",
"connection_not_changed",
"connectivity_problem",
"connectivity_settings_restored",
"cutting_drive_motor_1_defect",
"cutting_drive_motor_2_defect",
"cutting_drive_motor_3_defect",
"cutting_height_blocked",
"cutting_height_problem_curr",
"cutting_height_problem_dir",
"cutting_height_problem_drive",
"cutting_height_problem",
"cutting_motor_problem",
"cutting_stopped_slope_too_steep",
"cutting_system_blocked",
"cutting_system_imbalance_warning",
"cutting_system_major_imbalance",
"destination_not_reachable",
"difficult_finding_home",
"docking_sensor_defect",
"electronic_problem",
"empty_battery",
"folding_cutting_deck_sensor_defect",
"folding_sensor_activated",
"geofence_problem",
"gps_navigation_problem",
"guide_1_not_found",
"guide_2_not_found",
"guide_3_not_found",
"guide_calibration_accomplished",
"guide_calibration_failed",
"high_charging_power_loss",
"high_internal_power_loss",
"high_internal_temperature",
"internal_voltage_error",
"invalid_battery_combination_invalid_combination_of_different_battery_types",
"invalid_sub_device_combination",
"invalid_system_configuration",
"left_brush_motor_overloaded",
"lift_sensor_defect",
"lifted",
"limited_cutting_height_range",
"loop_sensor_defect",
"loop_sensor_problem_front",
"loop_sensor_problem_left",
"loop_sensor_problem_rear",
"loop_sensor_problem_right",
"low_battery",
"memory_circuit_problem",
"mower_lifted",
"mower_tilted",
"no_accurate_position_from_satellites",
"no_confirmed_position",
"no_drive",
"no_error",
"no_loop_signal",
"no_power_in_charging_station",
"no_response_from_charger",
"outside_working_area",
"poor_signal_quality",
"reference_station_communication_problem",
"right_brush_motor_overloaded",
"safety_function_faulty",
"settings_restored",
"sim_card_locked",
"sim_card_not_found",
"sim_card_requires_pin",
"slipped_mower_has_slipped_situation_not_solved_with_moving_pattern",
"slope_too_steep",
"sms_could_not_be_sent",
"stop_button_problem",
"stuck_in_charging_station",
"switch_cord_problem",
"temporary_battery_problem",
"tilt_sensor_problem",
"too_high_discharge_current",
"too_high_internal_current",
"trapped",
"ultrasonic_problem",
"ultrasonic_sensor_1_defect",
"ultrasonic_sensor_2_defect",
"ultrasonic_sensor_3_defect",
"ultrasonic_sensor_4_defect",
"unexpected_cutting_height_adj",
"unexpected_error",
"upside_down",
"weak_gps_signal",
"wheel_drive_problem_left",
"wheel_drive_problem_rear_left",
"wheel_drive_problem_rear_right",
"wheel_drive_problem_right",
"wheel_motor_blocked_left",
"wheel_motor_blocked_rear_left",
"wheel_motor_blocked_rear_right",
"wheel_motor_blocked_right",
"wheel_motor_overloaded_left",
"wheel_motor_overloaded_rear_left",
"wheel_motor_overloaded_rear_right",
"wheel_motor_overloaded_right",
"work_area_not_valid",
"wrong_loop_signal",
"wrong_pin_code",
"zone_generator_problem",
]
ERROR_KEY_LIST = list(
dict.fromkeys(ERROR_KEYS + [state.lower() for state in ERROR_STATES])
ERROR_KEY_LIST = sorted(
set(ERROR_KEYS) | {state.lower() for state in ERROR_STATES} | {"no_error"}
)
INACTIVE_REASONS: list = [

View File

@@ -53,6 +53,161 @@
},
"sync_clock": {
"name": "Sync clock"
},
"reset_cutting_blade_usage_time": {
"name": "Reset cutting blade usage time"
}
},
"event": {
"message": {
"name": "Message",
"state_attributes": {
"event_type": {
"state": {
"alarm_mower_in_motion": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_in_motion%]",
"alarm_mower_lifted": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_lifted%]",
"alarm_mower_stopped": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_stopped%]",
"alarm_mower_switched_off": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_switched_off%]",
"alarm_mower_tilted": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_tilted%]",
"alarm_outside_geofence": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_outside_geofence%]",
"angular_sensor_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::angular_sensor_problem%]",
"battery_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::battery_problem%]",
"battery_restriction_due_to_ambient_temperature": "[%key:component::husqvarna_automower::entity::sensor::error::state::battery_restriction_due_to_ambient_temperature%]",
"can_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::can_error%]",
"charging_current_too_high": "[%key:component::husqvarna_automower::entity::sensor::error::state::charging_current_too_high%]",
"charging_station_blocked": "[%key:component::husqvarna_automower::entity::sensor::error::state::charging_station_blocked%]",
"charging_system_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::charging_system_problem%]",
"collision_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::collision_sensor_defect%]",
"collision_sensor_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::collision_sensor_error%]",
"collision_sensor_problem_front": "[%key:component::husqvarna_automower::entity::sensor::error::state::collision_sensor_problem_front%]",
"collision_sensor_problem_rear": "[%key:component::husqvarna_automower::entity::sensor::error::state::collision_sensor_problem_rear%]",
"com_board_not_available": "[%key:component::husqvarna_automower::entity::sensor::error::state::com_board_not_available%]",
"communication_circuit_board_sw_must_be_updated": "[%key:component::husqvarna_automower::entity::sensor::error::state::communication_circuit_board_sw_must_be_updated%]",
"complex_working_area": "[%key:component::husqvarna_automower::entity::sensor::error::state::complex_working_area%]",
"connection_changed": "[%key:component::husqvarna_automower::entity::sensor::error::state::connection_changed%]",
"connection_not_changed": "[%key:component::husqvarna_automower::entity::sensor::error::state::connection_not_changed%]",
"connectivity_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::connectivity_problem%]",
"connectivity_settings_restored": "[%key:component::husqvarna_automower::entity::sensor::error::state::connectivity_settings_restored%]",
"cutting_drive_motor_1_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_drive_motor_1_defect%]",
"cutting_drive_motor_2_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_drive_motor_2_defect%]",
"cutting_drive_motor_3_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_drive_motor_3_defect%]",
"cutting_height_blocked": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_blocked%]",
"cutting_height_problem_curr": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_problem_curr%]",
"cutting_height_problem_dir": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_problem_dir%]",
"cutting_height_problem_drive": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_problem_drive%]",
"cutting_height_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_problem%]",
"cutting_motor_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_motor_problem%]",
"cutting_stopped_slope_too_steep": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_stopped_slope_too_steep%]",
"cutting_system_blocked": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_system_blocked%]",
"cutting_system_imbalance_warning": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_system_imbalance_warning%]",
"cutting_system_major_imbalance": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_system_major_imbalance%]",
"destination_not_reachable": "[%key:component::husqvarna_automower::entity::sensor::error::state::destination_not_reachable%]",
"difficult_finding_home": "[%key:component::husqvarna_automower::entity::sensor::error::state::difficult_finding_home%]",
"docking_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::docking_sensor_defect%]",
"electronic_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::electronic_problem%]",
"empty_battery": "[%key:component::husqvarna_automower::entity::sensor::error::state::empty_battery%]",
"error_at_power_up": "[%key:component::husqvarna_automower::entity::sensor::error::state::error_at_power_up%]",
"error": "[%key:common::state::error%]",
"fatal_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::fatal_error%]",
"folding_cutting_deck_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::folding_cutting_deck_sensor_defect%]",
"folding_sensor_activated": "[%key:component::husqvarna_automower::entity::sensor::error::state::folding_sensor_activated%]",
"geofence_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::geofence_problem%]",
"gps_navigation_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::gps_navigation_problem%]",
"guide_1_not_found": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_1_not_found%]",
"guide_2_not_found": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_2_not_found%]",
"guide_3_not_found": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_3_not_found%]",
"guide_calibration_accomplished": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_calibration_accomplished%]",
"guide_calibration_failed": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_calibration_failed%]",
"high_charging_power_loss": "[%key:component::husqvarna_automower::entity::sensor::error::state::high_charging_power_loss%]",
"high_internal_power_loss": "[%key:component::husqvarna_automower::entity::sensor::error::state::high_internal_power_loss%]",
"high_internal_temperature": "[%key:component::husqvarna_automower::entity::sensor::error::state::high_internal_temperature%]",
"internal_voltage_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::internal_voltage_error%]",
"invalid_battery_combination_invalid_combination_of_different_battery_types": "[%key:component::husqvarna_automower::entity::sensor::error::state::invalid_battery_combination_invalid_combination_of_different_battery_types%]",
"invalid_sub_device_combination": "[%key:component::husqvarna_automower::entity::sensor::error::state::invalid_sub_device_combination%]",
"invalid_system_configuration": "[%key:component::husqvarna_automower::entity::sensor::error::state::invalid_system_configuration%]",
"left_brush_motor_overloaded": "[%key:component::husqvarna_automower::entity::sensor::error::state::left_brush_motor_overloaded%]",
"lift_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::lift_sensor_defect%]",
"lifted": "[%key:component::husqvarna_automower::entity::sensor::error::state::lifted%]",
"limited_cutting_height_range": "[%key:component::husqvarna_automower::entity::sensor::error::state::limited_cutting_height_range%]",
"loop_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_defect%]",
"loop_sensor_problem_front": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_problem_front%]",
"loop_sensor_problem_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_problem_left%]",
"loop_sensor_problem_rear": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_problem_rear%]",
"loop_sensor_problem_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_problem_right%]",
"low_battery": "[%key:component::husqvarna_automower::entity::sensor::error::state::low_battery%]",
"memory_circuit_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::memory_circuit_problem%]",
"mower_lifted": "[%key:component::husqvarna_automower::entity::sensor::error::state::mower_lifted%]",
"mower_tilted": "[%key:component::husqvarna_automower::entity::sensor::error::state::mower_tilted%]",
"no_accurate_position_from_satellites": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_accurate_position_from_satellites%]",
"no_confirmed_position": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_confirmed_position%]",
"no_drive": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_drive%]",
"no_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_error%]",
"no_loop_signal": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_loop_signal%]",
"no_power_in_charging_station": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_power_in_charging_station%]",
"no_response_from_charger": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_response_from_charger%]",
"off": "[%key:common::state::off%]",
"outside_working_area": "[%key:component::husqvarna_automower::entity::sensor::error::state::outside_working_area%]",
"poor_signal_quality": "[%key:component::husqvarna_automower::entity::sensor::error::state::poor_signal_quality%]",
"reference_station_communication_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::reference_station_communication_problem%]",
"right_brush_motor_overloaded": "[%key:component::husqvarna_automower::entity::sensor::error::state::right_brush_motor_overloaded%]",
"safety_function_faulty": "[%key:component::husqvarna_automower::entity::sensor::error::state::safety_function_faulty%]",
"settings_restored": "[%key:component::husqvarna_automower::entity::sensor::error::state::settings_restored%]",
"sim_card_locked": "[%key:component::husqvarna_automower::entity::sensor::error::state::sim_card_locked%]",
"sim_card_not_found": "[%key:component::husqvarna_automower::entity::sensor::error::state::sim_card_not_found%]",
"sim_card_requires_pin": "[%key:component::husqvarna_automower::entity::sensor::error::state::sim_card_requires_pin%]",
"slipped_mower_has_slipped_situation_not_solved_with_moving_pattern": "[%key:component::husqvarna_automower::entity::sensor::error::state::slipped_mower_has_slipped_situation_not_solved_with_moving_pattern%]",
"slope_too_steep": "[%key:component::husqvarna_automower::entity::sensor::error::state::slope_too_steep%]",
"sms_could_not_be_sent": "[%key:component::husqvarna_automower::entity::sensor::error::state::sms_could_not_be_sent%]",
"stop_button_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::stop_button_problem%]",
"stopped": "[%key:common::state::stopped%]",
"stuck_in_charging_station": "[%key:component::husqvarna_automower::entity::sensor::error::state::stuck_in_charging_station%]",
"switch_cord_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::switch_cord_problem%]",
"temporary_battery_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::temporary_battery_problem%]",
"tilt_sensor_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::tilt_sensor_problem%]",
"too_high_discharge_current": "[%key:component::husqvarna_automower::entity::sensor::error::state::too_high_discharge_current%]",
"too_high_internal_current": "[%key:component::husqvarna_automower::entity::sensor::error::state::too_high_internal_current%]",
"trapped": "[%key:component::husqvarna_automower::entity::sensor::error::state::trapped%]",
"ultrasonic_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_problem%]",
"ultrasonic_sensor_1_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_sensor_1_defect%]",
"ultrasonic_sensor_2_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_sensor_2_defect%]",
"ultrasonic_sensor_3_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_sensor_3_defect%]",
"ultrasonic_sensor_4_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_sensor_4_defect%]",
"unexpected_cutting_height_adj": "[%key:component::husqvarna_automower::entity::sensor::error::state::unexpected_cutting_height_adj%]",
"unexpected_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::unexpected_error%]",
"upside_down": "[%key:component::husqvarna_automower::entity::sensor::error::state::upside_down%]",
"wait_power_up": "[%key:component::husqvarna_automower::entity::sensor::error::state::wait_power_up%]",
"wait_updating": "[%key:component::husqvarna_automower::entity::sensor::error::state::wait_updating%]",
"weak_gps_signal": "[%key:component::husqvarna_automower::entity::sensor::error::state::weak_gps_signal%]",
"wheel_drive_problem_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_drive_problem_left%]",
"wheel_drive_problem_rear_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_drive_problem_rear_left%]",
"wheel_drive_problem_rear_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_drive_problem_rear_right%]",
"wheel_drive_problem_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_drive_problem_right%]",
"wheel_motor_blocked_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_blocked_left%]",
"wheel_motor_blocked_rear_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_blocked_rear_left%]",
"wheel_motor_blocked_rear_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_blocked_rear_right%]",
"wheel_motor_blocked_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_blocked_right%]",
"wheel_motor_overloaded_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_overloaded_left%]",
"wheel_motor_overloaded_rear_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_overloaded_rear_left%]",
"wheel_motor_overloaded_rear_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_overloaded_rear_right%]",
"wheel_motor_overloaded_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_overloaded_right%]",
"work_area_not_valid": "[%key:component::husqvarna_automower::entity::sensor::error::state::work_area_not_valid%]",
"wrong_loop_signal": "[%key:component::husqvarna_automower::entity::sensor::error::state::wrong_loop_signal%]",
"wrong_pin_code": "[%key:component::husqvarna_automower::entity::sensor::error::state::wrong_pin_code%]",
"zone_generator_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::zone_generator_problem%]"
}
},
"severity": {
"state": {
"fatal": "Fatal",
"error": "[%key:common::state::error%]",
"warning": "Warning",
"info": "Info",
"debug": "Debug",
"sw": "Software",
"unknown": "Unknown"
}
}
}
}
},
"number": {

View File

@@ -12,5 +12,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower_ble",
"iot_class": "local_polling",
"requirements": ["automower-ble==0.2.1"]
"requirements": ["automower-ble==0.2.7"]
}

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/huum",
"iot_class": "cloud_polling",
"requirements": ["huum==0.8.0"]
"requirements": ["huum==0.8.1"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["imgw_pib==1.5.1"]
"requirements": ["imgw_pib==1.5.3"]
}

View File

@@ -42,10 +42,19 @@
"local_name": "Ink@IAM-T1",
"connectable": true
},
{
"local_name": "Ink@IAM-T2",
"connectable": true
},
{
"manufacturer_id": 12628,
"manufacturer_data_start": [65, 67, 45],
"connectable": true
},
{
"manufacturer_id": 12884,
"manufacturer_data_start": [0, 98, 0],
"connectable": false
}
],
"codeowners": ["@bdraco"],
@@ -53,5 +62,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/inkbird",
"iot_class": "local_push",
"requirements": ["inkbird-ble==0.16.2"]
"requirements": ["inkbird-ble==1.1.0"]
}

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