Compare commits

..

208 Commits

Author SHA1 Message Date
Wendelin
39d970347e Refactor input_weekday configuration validation and update test cases to use unique_id 2025-10-09 12:00:35 +02:00
Wendelin
9cccc96f63 Fix test 2025-10-09 10:35:03 +02:00
Wendelin
a32ada3155 Use input_weekday in automations 2025-10-09 08:48:41 +02:00
Wendelin
77f078e57d Add weekdays to build-in helpers 2025-10-08 17:23:19 +02:00
Wendelin
8657bfd0bf Add input helper weekdays 2025-10-08 17:08:48 +02:00
Joost Lekkerkerker
fe4eb8766d Don't mark ZHA coordinator as via_device with itself (#154004) 2025-10-08 16:05:54 +02:00
Mark Adkins
2d9f14c401 Add 3rd maintainer to sharkiq (#153961) 2025-10-08 15:17:52 +02:00
dependabot[bot]
7b6ccb07fd Bump github/codeql-action from 3.30.6 to 4.30.7 (#153979)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-08 13:42:25 +02:00
Shay Levy
2ba5728060 Enable Shelly binary input sensors by default (#154001) 2025-10-08 14:41:53 +03:00
epenet
b5f163cc85 Update Tuya fixture for product ID IAYz2WK1th0cMLmL (#154000) 2025-10-08 13:28:11 +02:00
Marc Mueller
65540a3e0b Update mypy dev to 1.19.0a4 (#153995) 2025-10-08 13:24:54 +02:00
Erwin Douna
cbf1b39edb Portainer add sensor platform (#153059)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Manu <4445816+tr4nt0r@users.noreply.github.com>
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2025-10-08 11:02:20 +02:00
G Johansson
142daf5e49 Call async_track_template_result with template without hass now fails (#153473)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-08 10:14:51 +02:00
Erik Montnemery
8bd0ff7cca Replace has_mean with mean_type in mill external statistics (#153985) 2025-10-08 09:52:07 +02:00
Erik Montnemery
ac676e12f6 Remove has_mean from suez_water external statistics (#153986) 2025-10-08 09:51:44 +02:00
Glenn Vandeuren (aka Iondependent)
c0ac3292cd FIx brightness always 100% when toggling the light (#153765)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-08 09:48:41 +02:00
Denis Shulyaka
80fd07c128 Add GPT-5 Pro and GPT-5 Codex support (#153936) 2025-10-08 09:48:07 +02:00
Michael Davie
3701d8859a Bump env-canada to 0.11.3 (#153967) 2025-10-08 09:40:55 +02:00
Jesse Hills
6dd26bae88 Bump aioesphomeapi to 41.13.0 (#153974) 2025-10-07 18:28:56 -10:00
Dave T
1a0abe296c Remove deprecated conductivity constants (#153942) 2025-10-07 23:20:36 +01:00
G Johansson
de6c61a4ab Bump psutil 7.1.0 (#153954) 2025-10-07 23:16:49 +01:00
Glenn Vandeuren (aka Iondependent)
33c677596e Update nhc to 0.6.1 (#153962) 2025-10-07 23:16:04 +01:00
peetersch
e9b4b8e99b Modbus Fix message_wait_milliseconds is no longer applied (#153709) 2025-10-07 23:38:05 +02:00
Maciej Bieniek
0525c04c42 Fix update interval for AccuWeather hourly forecast (#153957) 2025-10-07 23:25:04 +02:00
Shay Levy
d57b502551 Migrate Shelly virtual button platfrom unique IDs to include roles (#153865) 2025-10-07 23:01:30 +03:00
G Johansson
9fb708baf4 Bump holidays to 0.82 (#153952) 2025-10-07 23:00:38 +03:00
Josef Zweck
abdf24b7a0 Bump pylamarzocco to 2.1.2 (#153950) 2025-10-07 22:07:39 +03:00
TheJulianJES
29bfbd27bb Do not auto-set up ZHA zeroconf discoveries during onboarding (#153914) 2025-10-07 15:02:02 -04:00
starkillerOG
224553f8d9 Reverse Motion Blinds tilt direction (#149777)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-07 18:50:39 +01:00
mbo18
7c9f6a061f Add icons for SmartThings climate presets (#153929) 2025-10-07 19:15:15 +02:00
Marc Mueller
8e115d4685 Update pydantic to 2.12.0 (#153937) 2025-10-07 17:50:40 +01:00
Denis Shulyaka
00c189844f Bump openai to 2.2.0 (#153926) 2025-10-07 17:41:52 +01:00
Ståle Storø Hauknes
4587c286bb Add new sensors for Airthings Wave Enhance (#153879) 2025-10-07 17:44:30 +02:00
Artur Pragacz
b46097a7fc Move agent functionality from http (#153917) 2025-10-07 14:49:11 +02:00
mbo18
299cb6a2ff Change smart preset name to smart saver (#153916) 2025-10-07 14:11:00 +02:00
Erik Montnemery
1b7b91b328 Remove unused test fixtures from nintendo_parental (#153894) 2025-10-07 14:03:29 +02:00
Maciej Bieniek
01a1480ebd Use aioshelly methods for switches (#153746) 2025-10-07 13:28:58 +02:00
Jordan Harvey
26b8abb118 Bump pynintendoparental to 1.1.1 (#153874) 2025-10-07 13:28:08 +02:00
FMKaiba
53d1bbb530 Add support for gas detector status to SmartThings (#153831)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-10-07 12:56:53 +02:00
Tom Matheussen
a3ef55274e Add missing translation string for Satel Integra subentry type (#153905) 2025-10-07 12:18:51 +02:00
Joost Lekkerkerker
2034915457 Add fixture to SmartThings (#153902) 2025-10-07 12:13:12 +02:00
Joost Lekkerkerker
9e46d7964a Update SmartThings comments (#153903) 2025-10-07 11:46:44 +02:00
Maciej Bieniek
f9828a227b Bump aioshelly to version 13.12.0 (#153899) 2025-10-07 11:43:56 +02:00
Simone Chemelli
3341fa5f33 Code optimization for Comelit SimpleHome (#153029) 2025-10-07 10:31:44 +01:00
Christopher Fenner
e38ae47e76 Add language and location selector to OpenWeatherMap config flow (#153645)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-07 11:06:04 +02:00
Christopher Fenner
934c0e3c4c fix typo in icon assignment of AccuWeather integration (#153890) 2025-10-07 10:15:01 +02:00
Simone Chemelli
994a6ae7ed Fix restore cover state for Comelit SimpleHome (#153887) 2025-10-07 09:06:55 +02:00
Christopher Fenner
cdbe93c289 Set display precision for sensors in OpenWeatherMap integration (#153858) 2025-10-07 08:58:18 +02:00
Marc Mueller
56f90e4d96 Update pytest warnings filter (#153881) 2025-10-07 09:55:50 +03:00
TheJulianJES
34977abfec Remove Z-Wave JS voltage sensor overriding suggested precision (#153882) 2025-10-07 08:44:53 +02:00
Marc Mueller
5622103eb1 Fix nintendo_parental RuntimeWarning in tests (#153884) 2025-10-07 08:44:34 +02:00
Artur Pragacz
b9a1ab4a44 Clean up core references in conversation (#153880) 2025-10-07 00:46:47 +02:00
David Rapan
18997833c4 Shelly's power sensors naming paradigm standardization (#153822)
Signed-off-by: David Rapan <david@rapan.cz>
2025-10-07 01:32:44 +03:00
David Rapan
f99b194afc Shelly's current sensors naming paradigm standardization (#153827)
Signed-off-by: David Rapan <david@rapan.cz>
2025-10-07 01:32:25 +03:00
Dave T
566a347da7 Remove deprecated alarm panel constants (#153876) 2025-10-06 23:03:29 +01:00
Shay Levy
881306f6a4 Migrate Shelly virtual component unique IDs to include roles (#153844) 2025-10-07 00:50:47 +03:00
Marc Mueller
f63504af01 Update aiohttp to 3.13.0 (#153875) 2025-10-06 15:47:33 -05:00
derytive
d140b82a70 Add plate_count for Miele KM7575 (#153868) 2025-10-06 21:53:09 +02:00
Allen Porter
681211b1a5 Add Model Context Protocol support for OAuth scopes (#153150) 2025-10-06 15:32:42 -04:00
Joost Lekkerkerker
6c8b1f3618 Catch update exception in AirGradient (#153828) 2025-10-06 21:31:55 +02:00
Abílio Costa
d341065c34 Replace inner function with lambda in Idasen Desk (#153862) 2025-10-06 21:25:10 +02:00
G Johansson
81b1346080 Handle timeout errors gracefully in Nord Pool services (#153856) 2025-10-06 22:15:38 +03:00
J. Nick Koston
5613be3980 Bump yarl to 1.22.0 (#153860) 2025-10-06 13:43:37 -05:00
Alec
fbcf0eb94c Increase connect and configuration time for rfxtrx (#153834)
Increase the allowed time for connection and configuration. Some devices take a long time to respond to configuration changes and this time is counted for both network and configuration of the device.
2025-10-06 20:25:44 +02:00
Felipe Santos
1c7b9cc354 Avoid storing entities list in ONVIF binary_sensor and sensor (#153857) 2025-10-06 19:52:24 +02:00
William Scanlon
75e900606e Update water heater max temperature (#150970) 2025-10-06 19:21:21 +02:00
Norbert Rittel
7c665c53b5 Change translation of box in number to "Input field" for consistency (#153850) 2025-10-06 19:07:48 +02:00
Jordan Harvey
f72047eb02 Add new Nintendo Parental Controls integration (#145343)
Co-authored-by: Manu <4445816+tr4nt0r@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-06 18:36:46 +02:00
Marc Mueller
ade424c074 Update attrs to 25.4.0 (#153849) 2025-10-06 17:54:19 +02:00
Joost Lekkerkerker
5ad805de3c Add motion presets to SmartThings AC (#153830) 2025-10-06 17:29:33 +02:00
Simone Chemelli
ece77cf620 Fix PIN validation for Comelit SimpleHome (#153840) 2025-10-06 17:02:50 +02:00
Simone Chemelli
7eaa559056 Bump aiocomelit to 1.1.1 (#153843) 2025-10-06 16:57:40 +02:00
Allen Porter
08a9377373 Update the MCP Server API endpoint to mcp (#153845) 2025-10-06 16:56:45 +02:00
Jan Bouwhuis
a2837e6aee Add MQTT number subentry support (#153358) 2025-10-06 16:50:42 +02:00
Abílio Costa
fa03f6194d Remove log file write check (#153842) 2025-10-06 16:49:04 +02:00
Felipe Santos
d2851ea1df Deduplicate ONVIF sensor and binary sensor entity names (#153505) 2025-10-06 16:35:48 +02:00
Pavel Tarasov
72f8ac7857 Add BME680 sensor support for Altruist Insight (#153463) 2025-10-06 15:55:37 +02:00
NANI
77a267bc2f Updated VRM client and accounted for missing forecasts (#153464) 2025-10-06 15:37:49 +02:00
Arie Catsman
ad238daadc Enphase_envoy to use alternate data source for current transformers (#153621) 2025-10-06 15:33:25 +02:00
Øyvind Matheson Wergeland
42370ba203 Synology DSM: Don't reinitialize API during configuration (#153739) 2025-10-06 15:25:10 +02:00
Christopher Fenner
d9691c2a3b Add sensor for hydraulic separator temperature in ViCare integration (#153696) 2025-10-06 15:15:51 +02:00
Michael
66cca981a9 Expose climate current temp as dedicated sensor in FRITZ!SmartHome (#153558) 2025-10-06 15:00:29 +02:00
Ståle Storø Hauknes
9640ebb593 Add support for Wave Enhance and Corentium Home 2 in Airthings BLE integration (#153780) 2025-10-06 14:59:21 +02:00
Joost Lekkerkerker
645f32fd65 Bump pySmartThings to 3.3.1 (#153826) 2025-10-06 14:57:52 +02:00
hanwg
cb6e65f972 Refactor Telegram bot entity (#153609)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-06 14:57:46 +02:00
cdnninja
425bdc0ba6 Vesync add oscillation to fan (#153297) 2025-10-06 14:50:11 +02:00
cdnninja
c36341e51f vesync correct fan set modes (#153761) 2025-10-06 14:49:05 +02:00
Robert Resch
553d896899 Add Ecovacs active map select entity (#153748)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-06 14:42:01 +02:00
Åke Strandberg
ac79b3072e Use customized miele device name if set (#153835) 2025-10-06 14:40:57 +02:00
Andrew Jackson
c0aa9bfd4b Update Mealie quality scale to platinum (#153810) 2025-10-06 12:14:05 +02:00
epenet
e97100028d Add new test fixture for Tuya cl category (#153800) 2025-10-06 11:51:10 +02:00
tronikos
da89617432 Google Assistant SDK: improve init tests (#153795) 2025-10-06 11:49:06 +02:00
dependabot[bot]
e6203dffd3 Bump actions/stale from 10.0.0 to 10.1.0 (#153799)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-06 11:45:39 +02:00
Josef Zweck
c13cfe9c37 Re-add AGENTS.md as symlink (#153804) 2025-10-06 11:45:21 +02:00
Ludovic BOUÉ
2447df9341 Add Matter speaker mute toggle (#150104)
Add Matter speaker mute toggle functionality:
- OnOff attribute == True state means volume is on, so HA should show mute switch as off
- OnOff attribute == False means volume is off (muted), so HA should show mute switch as on
2025-10-06 11:09:59 +02:00
Erik Montnemery
1c1fbe0ec1 Log when failing to remove foreign key in recorder EventIDPostMigration (#153812) 2025-10-06 10:58:47 +02:00
Erik Montnemery
4a6d2017fd Fix stale docstring in recorder (#153811) 2025-10-06 10:58:24 +02:00
Simone Chemelli
b4997a52df Remove stale entities from Alexa Devices (#153759) 2025-10-06 10:57:21 +02:00
Joris Pelgröm
464dec1dcb Update LetPot integration quality scale to silver (#153783) 2025-10-06 10:28:43 +02:00
tronikos
85506ac78a Google Assistant SDK: use setup_credentials in setup_integration (#153793) 2025-10-06 10:23:44 +02:00
Marc Mueller
6d97355b42 Update raspyrfm-client to 1.2.9 (#153789) 2025-10-06 10:20:15 +02:00
Ludovic BOUÉ
f9e75c616a Use TEMPERATURE_SCALING_FACTOR for Matter sensors (#153807) 2025-10-06 10:15:37 +02:00
tronikos
a821d02dfb Translate reauthentication error message in Google Assistant SDK (#153797)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-06 01:10:40 -07:00
epenet
e05169c7a4 Fix Tuya cover position when only control is available (#153803) 2025-10-06 08:50:38 +02:00
tronikos
1cc3431529 Fix missing google_assistant_sdk.send_text_command (#153735) 2025-10-05 23:46:03 -07:00
Maciej Bieniek
4ba765f265 Add Shelly Wall Display XL to the list of devices without firmware changelog (#153781) 2025-10-06 06:52:15 +02:00
Paulus Schoutsen
50a7af4179 Handle ESPHome discoveries with uninitialized Z-Wave antennas (#153790) 2025-10-05 23:06:10 -04:00
Allen Porter
e0a2116e88 Update MCP server to support the newer HTTP protocol (#153779)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2025-10-05 21:16:53 -04:00
Aidan Timson
d8e1ed5f4a Fix power device classes for system bridge (#153201) 2025-10-05 23:52:35 +02:00
Denis Shulyaka
f1b8e8a963 Ollama thinking content (#150393) 2025-10-05 17:33:45 -04:00
Marc Mueller
9a9fd44c62 Use yaml anchors in ci workflow (#152586) 2025-10-05 23:21:38 +02:00
G Johansson
bc3fe7a18e Use automatic reload options flow in min_max (#153143) 2025-10-05 23:17:37 +02:00
G Johansson
19f3559345 Remove previously deprecated template attach function (#153370) 2025-10-05 23:16:57 +02:00
Jan Bouwhuis
fad0e23797 Allow to set the manufacturer in a MQTT device subentry setup (#153747) 2025-10-05 23:15:00 +02:00
Erik Montnemery
7f931e4d70 Add device class filter to hydrawise services (#153249) 2025-10-05 23:14:12 +02:00
Erik Montnemery
a04835629b Make hassfest fail on services with device filter on targets (#152794) 2025-10-05 23:13:33 +02:00
Andrew Jackson
78cd80746d Bump aiomealie to 1.0.0, update min Mealie instance version to v2. (#153203) 2025-10-05 23:12:05 +02:00
G Johansson
9ac93920d8 Cleanup process_fds addition in systemmonitor (#153568) 2025-10-05 22:41:19 +02:00
G Johansson
1818fce1ae Validating schema outside the event loop will now fail (#153472) 2025-10-05 22:37:14 +02:00
Erik Montnemery
f524edc4b9 Add pytest command line option to drop recorder db before test (#153527) 2025-10-05 22:36:24 +02:00
Paulus Schoutsen
19f990ed31 ESPHome to set Z-Wave discovery as next_flow (#153706) 2025-10-05 22:33:12 +02:00
David Rapan
5d83c82b81 Shelly's energy sensors naming paradigm standardization (#153729) 2025-10-05 22:32:39 +02:00
Fredrik Erlandsson
d63d154457 Daikin increase timeout (#153722)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-05 22:18:31 +02:00
Josef Zweck
933b15ce36 Revert "AGENTS.md" (#153777) 2025-10-05 22:05:04 +02:00
Denis Shulyaka
6ec7b63ebe Add support for Anthropic Claude Sonnet 4.5 (#153769) 2025-10-05 21:29:24 +02:00
Denis Shulyaka
26bfbc55e9 Bump anthropic to 0.69.0 (#153764) 2025-10-05 20:59:50 +02:00
Simone Chemelli
d75ca0f5f3 Bump aioamazondevices to 6.2.9 (#153756) 2025-10-05 20:59:02 +02:00
Sander Jochems
fed8f137e9 Upgrade python-melcloud to 0.1.2 (#153742) 2025-10-05 19:49:22 +02:00
Josef Zweck
f44d65e023 Migrate tolo to entry.runtime_data (#153744) 2025-10-05 18:43:37 +02:00
Christopher Fenner
a270bd76de Add sensors for battery charge amount to ViCare integration (#153631)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-05 18:34:45 +02:00
Christopher Fenner
9209e419ec Change style for critical number entities in ViCare integration (#153634) 2025-10-05 16:36:42 +02:00
Ståle Storø Hauknes
98f8f15e90 Fix crash when setting up Airthings BLE device (#153510) 2025-10-05 09:18:47 -05:00
Denis Shulyaka
b2a2868afd AGENTS.md (#153680) 2025-10-05 15:51:46 +02:00
J. Nick Koston
0d4737d360 Bump aiohomekit to 3.2.20 (#153750) 2025-10-05 15:49:55 +02:00
Ståle Storø Hauknes
2b370a0eca Use full serial number when adding an Airthings device (#153499) 2025-10-05 08:23:02 -05:00
Ståle Storø Hauknes
618fe81207 Check if firmware is outdated when adding an Airthings BLE device (#153559) 2025-10-05 14:49:34 +02:00
Maciej Bieniek
c0fe4861f9 Align Shelly presencezone entity to the new API/firmware (#153737) 2025-10-05 15:36:57 +03:00
Simone Chemelli
dfd33fdab1 Fix sensors availability check for Alexa Devices (#153743) 2025-10-05 14:26:16 +02:00
Josef Zweck
cceee05c15 Fix lamarzocco brewing start time sensor availability (#153732) 2025-10-05 13:04:28 +02:00
Manu
f560d2a05e Update suggested display precision for ntfy attachment size to 2 (#153741) 2025-10-05 13:03:55 +02:00
Ville Skyttä
3601cff88e Upgrade upcloud-api to 2.9.0 (#153727) 2025-10-05 13:58:35 +03:00
Maciej Bieniek
ca5c0a759f Remove Shelly presencezone component from VIRTUAL_COMPONENTS tuple (#153740) 2025-10-05 13:46:42 +03:00
Tom
6f9e6909ce Bump airOS to 0.5.5 using formdata for v6 firmware (#153736) 2025-10-05 12:43:43 +02:00
Manu
ccf563437b Bump aiontfy to v0.6.1 (#153738) 2025-10-05 12:34:18 +02:00
Josef Zweck
78e97428fd Add debouncer to acaia (#153725) 2025-10-05 12:31:45 +02:00
Denis Shulyaka
8b4c730993 Gemini: Use default model instead of recommended where applicable (#153676) 2025-10-05 02:59:51 -07:00
Fredrik Erlandsson
0a071a13e2 Version bump pydaikin to 2.17.1 (#153726) 2025-10-05 11:12:10 +02:00
Shay Levy
ab80991eac Add Shelly support for climate entities (#153450)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-05 11:53:52 +03:00
Erwin Douna
ee7262efb4 Portainer add button platform (#153063) 2025-10-05 10:36:52 +02:00
Fredrik Erlandsson
ea5a52cdc8 Version bump pydaikin to 2.17.0 (#153718) 2025-10-05 10:49:14 +03:00
tronikos
31fe0322ab Clarify description for media player entity in Google Assistant SDK (#153715) 2025-10-05 09:47:46 +02:00
tronikos
e8e0eabb99 Double max retries in Google Drive (#153717) 2025-10-05 10:35:50 +03:00
tronikos
1629dad1a8 Bump opower to 0.15.6 (#153714) 2025-10-05 09:25:45 +02:00
Shay Levy
d9baad530a Shelly code quality and cleanup (#153692) 2025-10-05 09:14:13 +02:00
cdnninja
4a1d00e59a Bump pyvesync to 3.1.0 (#153693) 2025-10-05 09:56:25 +03:00
Daniel Hjelseth Høyer
437e4e027c Bump Mill library (#153683)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2025-10-05 09:55:48 +03:00
J. Nick Koston
3726f7eca9 Bump zeroconf to 0.148.0 (#153704) 2025-10-04 21:57:00 -05:00
Marc Mueller
c943cf515c Add zeroconf to hassfest version requirements (#153703) 2025-10-05 02:55:35 +02:00
Christopher Fenner
3b0c2a7e56 Fix ViCare pressure sensors missing unit of measurement (#153691) 2025-10-05 02:14:32 +02:00
Christopher Fenner
6ebaa9cd1d Bump PyViCare to 2.52.0 (#153629) 2025-10-05 02:05:53 +02:00
J. Nick Koston
f81c32f6ea Bump aioesphomeapi to 41.12.0 (#153698) 2025-10-04 18:55:36 -05:00
J. Nick Koston
c0cd7a1a62 Bump propcache to 0.4.0 (#153694) 2025-10-04 18:03:53 -05:00
J. Nick Koston
7a61c818c6 Bump ulid-transform to 1.5.2 (#153690) 2025-10-04 23:51:59 +02:00
J. Nick Koston
2800625bcf Bump dbus-fast to 2.44.5 (#153686) 2025-10-04 23:04:37 +02:00
J. Nick Koston
cfec998221 Bump fnv-hash-fast to 1.6.0 (#153682) 2025-10-04 15:25:11 -05:00
Joakim Plate
7203cffbd7 Schedule update coordinator again if it is active (#153596) 2025-10-04 15:13:41 -05:00
Josef Zweck
23397ef6a9 Smarter calculation of chunk size in onedrive (#153679)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-04 21:52:58 +02:00
Joakim Plate
0e154635ff Limit shelly tests to single platform (#153681) 2025-10-04 22:45:48 +03:00
J. Nick Koston
2e6e518722 Bump cached-ipaddress to 1.0.1 (#153670) 2025-10-04 13:49:13 -05:00
J. Nick Koston
e0cded97c7 Bump bleak-esphome to 3.4.0 (#153669) 2025-10-04 20:38:56 +02:00
J. Nick Koston
87a6a029bb Bump habluetooth to 5.7.0 (#153665) 2025-10-04 12:16:51 -05:00
Jan Bouwhuis
1cc3c22d3f Fix MQTT Lock state reset to unknown when a reset payload is received (#153647) 2025-10-04 17:32:18 +02:00
Marc Mueller
2341d1d965 Fix flaky template test (#153624) 2025-10-04 18:28:36 +03:00
J. Nick Koston
a0bae9485c Bump bluetooth-data-tools to 1.28.3 (#153653) 2025-10-04 18:27:09 +03:00
J. Nick Koston
f281b0fc6b Bump annotatedyaml to 1.0.2 (#153651) 2025-10-04 18:26:55 +03:00
Manu
6f89fe81cc Remove Plum Lightpad integration (#153590) 2025-10-04 16:42:28 +02:00
Hessel
34f6ead7a1 Wallbox fix Rate Limit issue for multiple chargers (#153074) 2025-10-04 16:38:11 +02:00
Kevin McCormack
8985527a87 Bump libpyvivotek to 0.6.1 and add strict typing for Vivotek integration (#153342)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-04 16:34:37 +02:00
Marc Mueller
bd87a3aa4d Update PyYAML to 6.0.3 (#153626) 2025-10-04 16:27:22 +02:00
Manu
768a505904 Add translations and icons to OralB integration (#153605) 2025-10-04 16:23:27 +02:00
Marc Mueller
d97c1f0fc3 Update grpcio to 1.75.1 (#153643) 2025-10-04 16:21:16 +02:00
Joakim Plate
c3fcd34d4c Fix blue current mocking out platform with empty string (#153604)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-04 15:17:56 +02:00
Joakim Plate
44d9eaea95 Correct kraken test issues (#153601) 2025-10-04 12:25:35 +02:00
G Johansson
0f34f5139a Fix sql repair string (#153619) 2025-10-04 10:39:26 +02:00
Ludovic BOUÉ
2afb1a673d Add Matter Thermostat OccupancySensor (#153166)
Co-authored-by: Björn Ebbinghaus <bjoern@ebbinghaus.me>
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2025-10-04 10:31:17 +02:00
G Johansson
c2f7f29630 Setup platform services during integration start in sensibo (#153571) 2025-10-04 10:30:01 +02:00
G Johansson
b01f5dd24b Raise repairs on platform setup for sql (#153581) 2025-10-04 07:02:36 +02:00
Luke Lashley
0cda0c449f Update the map parser in Roborock vacuum to use coord parser. (#153520) 2025-10-03 19:59:57 -07:00
dollaransh17
40fdf12bc9 Fix string interpolation in local_todo error messages (#153580)
Co-authored-by: dollaransh17 <dollaransh17@users.noreply.github.com>
2025-10-03 19:57:50 -07:00
Luke Lashley
3939a80302 Switch Roborock to v4 of the code login api (#153593) 2025-10-03 19:41:39 -07:00
Luke Lashley
d32a102613 Add two new consumable sensors to Roborock (#153606) 2025-10-03 19:38:29 -07:00
Felipe Santos
20949d39c4 Address comments for the add-on switch entity (#153518) 2025-10-04 02:22:59 +02:00
Kevin Stillhammer
310a0c8d13 Use SensorDescription for GoogleTravelTimeSensor (#153585) 2025-10-04 01:29:26 +02:00
Arie Catsman
c9e80ac7e9 Extend enphase_envoy test data with new library data fields (#153591) 2025-10-04 01:24:11 +02:00
G Johansson
5df4e9e1cf Bump pynordpool to 0.3.1 (#153599) 2025-10-04 01:23:09 +02:00
Simone Chemelli
4022ee74e8 Bump aioamazondevices to 6.2.8 (#153592) 2025-10-04 01:22:15 +02:00
Erwin Douna
80a4115c44 Portainer follow-up points (#153594) 2025-10-03 22:36:55 +02:00
Manu
ce548efd80 Remove IBM Watson IoT Platform integration (#153567) 2025-10-03 21:18:39 +02:00
dependabot[bot]
2edf622b41 Bump github/codeql-action from 3.30.5 to 3.30.6 (#153524)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-03 21:15:47 +02:00
Manu
66ac9078aa Improve Habitica tests (#153573) 2025-10-03 20:55:38 +02:00
Erwin Douna
ba75f18f5a Portainer add switch platform (#153485)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-03 20:52:37 +02:00
Arie Catsman
8ee2ece03e Bump pyenphase to 2.4.0 (#153583) 2025-10-03 21:14:34 +03:00
Michael
7060ab8c44 Remove Vultr integration (#153560) 2025-10-03 21:12:26 +03:00
Paulus Schoutsen
85d8244b8a When discovering a Z-Wave adapter, always configure add-on in config flow (#153575) 2025-10-03 19:16:51 +02:00
Abílio Costa
3f9421ab08 Debounce updates in Idasen Desk (#153503) 2025-10-03 18:39:14 +02:00
Luke Lashley
2f3fbf00b7 Bump python-roborock to 2.50.2 (#153561) 2025-10-03 09:30:30 -07:00
Paulus Schoutsen
d595ec8a07 Z-Wave to support migrating from USB to socket with same home ID (#153522) 2025-10-03 10:46:12 -04:00
Ståle Storø Hauknes
4ff5462cc4 Bump Airthings BLE to 1.1.1 (#153529) 2025-10-03 15:50:20 +02:00
448 changed files with 13191 additions and 5943 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Initialize CodeQL
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
with:
category: "/language:python"

View File

@@ -17,7 +17,7 @@ jobs:
# - No PRs marked as no-stale
# - No issues (-1)
- name: 60 days stale PRs policy
uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 60
@@ -57,7 +57,7 @@ jobs:
# - No issues marked as no-stale or help-wanted
# - No PRs (-1)
- name: 90 days stale issues
uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with:
repo-token: ${{ steps.token.outputs.token }}
days-before-stale: 90
@@ -87,7 +87,7 @@ jobs:
# - No Issues marked as no-stale or help-wanted
# - No PRs (-1)
- name: Needs more information stale issues policy
uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with:
repo-token: ${{ steps.token.outputs.token }}
only-labels: "needs-more-information"

View File

@@ -555,6 +555,7 @@ homeassistant.components.vacuum.*
homeassistant.components.vallox.*
homeassistant.components.valve.*
homeassistant.components.velbus.*
homeassistant.components.vivotek.*
homeassistant.components.vlc_telnet.*
homeassistant.components.vodafone_station.*
homeassistant.components.volvo.*

1
AGENTS.md Symbolic link
View File

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

10
CODEOWNERS generated
View File

@@ -753,6 +753,8 @@ build.json @home-assistant/supervisor
/tests/components/input_select/ @home-assistant/core
/homeassistant/components/input_text/ @home-assistant/core
/tests/components/input_text/ @home-assistant/core
/homeassistant/components/input_weekday/ @home-assistant/core
/tests/components/input_weekday/ @home-assistant/core
/homeassistant/components/insteon/ @teharris1
/tests/components/insteon/ @teharris1
/homeassistant/components/integration/ @dgomes
@@ -1065,6 +1067,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/nilu/ @hfurubotten
/homeassistant/components/nina/ @DeerMaximum
/tests/components/nina/ @DeerMaximum
/homeassistant/components/nintendo_parental/ @pantherale0
/tests/components/nintendo_parental/ @pantherale0
/homeassistant/components/nissan_leaf/ @filcole
/homeassistant/components/noaa_tides/ @jdelaney72
/homeassistant/components/nobo_hub/ @echoromeo @oyvindwe
@@ -1196,8 +1200,6 @@ build.json @home-assistant/supervisor
/tests/components/plex/ @jjlawren
/homeassistant/components/plugwise/ @CoMPaTech @bouwew
/tests/components/plugwise/ @CoMPaTech @bouwew
/homeassistant/components/plum_lightpad/ @ColinHarrington @prystupa
/tests/components/plum_lightpad/ @ColinHarrington @prystupa
/homeassistant/components/point/ @fredrike
/tests/components/point/ @fredrike
/homeassistant/components/pooldose/ @lmaertin
@@ -1413,8 +1415,8 @@ build.json @home-assistant/supervisor
/tests/components/sfr_box/ @epenet
/homeassistant/components/sftp_storage/ @maretodoric
/tests/components/sftp_storage/ @maretodoric
/homeassistant/components/sharkiq/ @JeffResc @funkybunch
/tests/components/sharkiq/ @JeffResc @funkybunch
/homeassistant/components/sharkiq/ @JeffResc @funkybunch @TheOneOgre
/tests/components/sharkiq/ @JeffResc @funkybunch @TheOneOgre
/homeassistant/components/shell_command/ @home-assistant/core
/tests/components/shell_command/ @home-assistant/core
/homeassistant/components/shelly/ @bieniu @thecode @chemelli74 @bdraco

View File

@@ -231,6 +231,7 @@ DEFAULT_INTEGRATIONS = {
"input_datetime",
"input_number",
"input_select",
"input_weekday",
"input_text",
"schedule",
"timer",
@@ -635,25 +636,15 @@ async def async_enable_logging(
err_log_path = os.path.abspath(log_file)
if err_log_path:
err_path_exists = os.path.isfile(err_log_path)
err_dir = os.path.dirname(err_log_path)
err_handler = await hass.async_add_executor_job(
_create_log_file, err_log_path, log_rotate_days
)
# Check if we can write to the error log if it exists or that
# we can create files in the containing directory if not.
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
not err_path_exists and os.access(err_dir, os.W_OK)
):
err_handler = await hass.async_add_executor_job(
_create_log_file, err_log_path, log_rotate_days
)
err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
logger.addHandler(err_handler)
err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
logger.addHandler(err_handler)
# Save the log file location for access by other components.
hass.data[DATA_LOGGING] = err_log_path
else:
_LOGGER.error("Unable to set up error log %s (access denied)", err_log_path)
# Save the log file location for access by other components.
hass.data[DATA_LOGGING] = err_log_path
async_activate_log_queue_handler(hass)

View File

@@ -1,5 +0,0 @@
{
"domain": "ibm",
"name": "IBM",
"integrations": ["watson_iot", "watson_tts"]
}

View File

@@ -12,11 +12,13 @@ from homeassistant.components.bluetooth import async_get_scanner
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS
from homeassistant.core import HomeAssistant
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import CONF_IS_NEW_STYLE_SCALE
SCAN_INTERVAL = timedelta(seconds=15)
UPDATE_DEBOUNCE_TIME = 0.2
_LOGGER = logging.getLogger(__name__)
@@ -38,11 +40,19 @@ class AcaiaCoordinator(DataUpdateCoordinator[None]):
config_entry=entry,
)
debouncer = Debouncer(
hass=hass,
logger=_LOGGER,
cooldown=UPDATE_DEBOUNCE_TIME,
immediate=True,
function=self.async_update_listeners,
)
self._scale = AcaiaScale(
address_or_ble_device=entry.data[CONF_ADDRESS],
name=entry.title,
is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE],
notify_callback=self.async_update_listeners,
notify_callback=debouncer.async_schedule_call,
scanner=async_get_scanner(hass),
)

View File

@@ -71,4 +71,4 @@ POLLEN_CATEGORY_MAP = {
}
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=10)
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(hours=30)
UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(minutes=30)

View File

@@ -1,6 +1,9 @@
{
"entity": {
"sensor": {
"air_quality": {
"default": "mdi:air-filter"
},
"cloud_ceiling": {
"default": "mdi:weather-fog"
},
@@ -34,9 +37,6 @@
"thunderstorm_probability_night": {
"default": "mdi:weather-lightning"
},
"translation_key": {
"default": "mdi:air-filter"
},
"tree_pollen": {
"default": "mdi:tree-outline"
},

View File

@@ -1,7 +1,9 @@
"""Airgradient Update platform."""
from datetime import timedelta
import logging
from airgradient import AirGradientConnectionError
from propcache.api import cached_property
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
@@ -13,6 +15,7 @@ from .entity import AirGradientEntity
PARALLEL_UPDATES = 1
SCAN_INTERVAL = timedelta(hours=1)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
@@ -31,6 +34,7 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
"""Representation of Airgradient Update."""
_attr_device_class = UpdateDeviceClass.FIRMWARE
_server_unreachable_logged = False
def __init__(self, coordinator: AirGradientCoordinator) -> None:
"""Initialize the entity."""
@@ -47,10 +51,27 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
"""Return the installed version of the entity."""
return self.coordinator.data.measures.firmware_version
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self._attr_available
async def async_update(self) -> None:
"""Update the entity."""
self._attr_latest_version = (
await self.coordinator.client.get_latest_firmware_version(
self.coordinator.serial_number
try:
self._attr_latest_version = (
await self.coordinator.client.get_latest_firmware_version(
self.coordinator.serial_number
)
)
)
except AirGradientConnectionError:
self._attr_latest_version = None
self._attr_available = False
if not self._server_unreachable_logged:
_LOGGER.error(
"Unable to connect to AirGradient server to check for updates"
)
self._server_unreachable_logged = True
else:
self._server_unreachable_logged = False
self._attr_available = True

View File

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

View File

@@ -6,8 +6,13 @@ import dataclasses
import logging
from typing import Any
from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
from airthings_ble import (
AirthingsBluetoothDeviceData,
AirthingsDevice,
UnsupportedDeviceError,
)
from bleak import BleakError
from habluetooth import BluetoothServiceInfoBleak
import voluptuous as vol
from homeassistant.components import bluetooth
@@ -27,6 +32,7 @@ SERVICE_UUIDS = [
"b42e4a8e-ade7-11e4-89d3-123b93f75cba",
"b42e1c08-ade7-11e4-89d3-123b93f75cba",
"b42e3882-ade7-11e4-89d3-123b93f75cba",
"b42e90a2-ade7-11e4-89d3-123b93f75cba",
]
@@ -37,6 +43,7 @@ class Discovery:
name: str
discovery_info: BluetoothServiceInfo
device: AirthingsDevice
data: AirthingsBluetoothDeviceData
def get_name(device: AirthingsDevice) -> str:
@@ -44,7 +51,7 @@ def get_name(device: AirthingsDevice) -> str:
name = device.friendly_name()
if identifier := device.identifier:
name += f" ({identifier})"
name += f" ({device.model.value}{identifier})"
return name
@@ -62,8 +69,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
self._discovered_device: Discovery | None = None
self._discovered_devices: dict[str, Discovery] = {}
async def _get_device_data(
self, discovery_info: BluetoothServiceInfo
async def _get_device(
self, data: AirthingsBluetoothDeviceData, discovery_info: BluetoothServiceInfo
) -> AirthingsDevice:
ble_device = bluetooth.async_ble_device_from_address(
self.hass, discovery_info.address
@@ -72,10 +79,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.debug("no ble_device in _get_device_data")
raise AirthingsDeviceUpdateError("No ble_device")
airthings = AirthingsBluetoothDeviceData(_LOGGER)
try:
data = await airthings.update_device(ble_device)
device = await data.update_device(ble_device)
except BleakError as err:
_LOGGER.error(
"Error connecting to and getting data from %s: %s",
@@ -83,12 +88,15 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
err,
)
raise AirthingsDeviceUpdateError("Failed getting device data") from err
except UnsupportedDeviceError:
_LOGGER.debug("Skipping unsupported device: %s", discovery_info.name)
raise
except Exception as err:
_LOGGER.error(
"Unknown error occurred from %s: %s", discovery_info.address, err
)
raise
return data
return device
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfo
@@ -98,17 +106,21 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(discovery_info.address)
self._abort_if_unique_id_configured()
data = AirthingsBluetoothDeviceData(logger=_LOGGER)
try:
device = await self._get_device_data(discovery_info)
device = await self._get_device(data=data, discovery_info=discovery_info)
except AirthingsDeviceUpdateError:
return self.async_abort(reason="cannot_connect")
except UnsupportedDeviceError:
return self.async_abort(reason="unsupported_device")
except Exception:
_LOGGER.exception("Unknown error occurred")
return self.async_abort(reason="unknown")
name = get_name(device)
self.context["title_placeholders"] = {"name": name}
self._discovered_device = Discovery(name, discovery_info, device)
self._discovered_device = Discovery(name, discovery_info, device, data=data)
return await self.async_step_bluetooth_confirm()
@@ -117,6 +129,12 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Confirm discovery."""
if user_input is not None:
if (
self._discovered_device is not None
and self._discovered_device.device.firmware.need_firmware_upgrade
):
return self.async_abort(reason="firmware_upgrade_required")
return self.async_create_entry(
title=self.context["title_placeholders"]["name"], data={}
)
@@ -137,6 +155,9 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured()
discovery = self._discovered_devices[address]
if discovery.device.firmware.need_firmware_upgrade:
return self.async_abort(reason="firmware_upgrade_required")
self.context["title_placeholders"] = {
"name": discovery.name,
}
@@ -146,26 +167,47 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_create_entry(title=discovery.name, data={})
current_addresses = self._async_current_ids(include_ignore=False)
devices: list[BluetoothServiceInfoBleak] = []
for discovery_info in async_discovered_service_info(self.hass):
address = discovery_info.address
if address in current_addresses or address in self._discovered_devices:
continue
if MFCT_ID not in discovery_info.manufacturer_data:
continue
if not any(uuid in SERVICE_UUIDS for uuid in discovery_info.service_uuids):
_LOGGER.debug(
"Skipping unsupported device: %s (%s)", discovery_info.name, address
)
continue
devices.append(discovery_info)
for discovery_info in devices:
address = discovery_info.address
data = AirthingsBluetoothDeviceData(logger=_LOGGER)
try:
device = await self._get_device_data(discovery_info)
device = await self._get_device(data, discovery_info)
except AirthingsDeviceUpdateError:
return self.async_abort(reason="cannot_connect")
_LOGGER.error(
"Error connecting to and getting data from %s (%s)",
discovery_info.name,
discovery_info.address,
)
continue
except UnsupportedDeviceError:
_LOGGER.debug(
"Skipping unsupported device: %s (%s)",
discovery_info.name,
discovery_info.address,
)
continue
except Exception:
_LOGGER.exception("Unknown error occurred")
return self.async_abort(reason="unknown")
name = get_name(device)
self._discovered_devices[address] = Discovery(name, discovery_info, device)
_LOGGER.debug("Discovered Airthings device: %s (%s)", name, address)
self._discovered_devices[address] = Discovery(
name, discovery_info, device, data
)
if not self._discovered_devices:
return self.async_abort(reason="no_devices_found")

View File

@@ -17,6 +17,10 @@
{
"manufacturer_id": 820,
"service_uuid": "b42e3882-ade7-11e4-89d3-123b93f75cba"
},
{
"manufacturer_id": 820,
"service_uuid": "b42e90a2-ade7-11e4-89d3-123b93f75cba"
}
],
"codeowners": ["@vincegio", "@LaStrada"],
@@ -24,5 +28,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
"iot_class": "local_polling",
"requirements": ["airthings-ble==0.9.2"]
"requirements": ["airthings-ble==1.1.1"]
}

View File

@@ -16,10 +16,12 @@ from homeassistant.components.sensor import (
from homeassistant.const import (
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
EntityCategory,
Platform,
UnitOfPressure,
UnitOfSoundPressure,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
@@ -112,6 +114,21 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"lux": SensorEntityDescription(
key="lux",
device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement=LIGHT_LUX,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"noise": SensorEntityDescription(
key="noise",
translation_key="ambient_noise",
device_class=SensorDeviceClass.SOUND_PRESSURE,
native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
}
PARALLEL_UPDATES = 0

View File

@@ -20,6 +20,8 @@
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"firmware_upgrade_required": "Your device requires a firmware upgrade. Please use the Airthings app (Android/iOS) to upgrade it.",
"unsupported_device": "Unsupported device",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
@@ -39,6 +41,9 @@
},
"illuminance": {
"name": "[%key:component::sensor::entity_component::illuminance::name%]"
},
"ambient_noise": {
"name": "Ambient noise"
}
}
}

View File

@@ -18,7 +18,9 @@ from homeassistant.components.binary_sensor import (
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
import homeassistant.helpers.entity_registry as er
from .const import _LOGGER, DOMAIN
from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity
from .utils import async_update_unique_id
@@ -51,11 +53,47 @@ BINARY_SENSORS: Final = (
),
is_supported=lambda device, key: device.sensors.get(key) is not None,
is_available_fn=lambda device, key: (
device.online and device.sensors[key].error is False
device.online
and (sensor := device.sensors.get(key)) is not None
and sensor.error is False
),
),
)
DEPRECATED_BINARY_SENSORS: Final = (
AmazonBinarySensorEntityDescription(
key="bluetooth",
entity_category=EntityCategory.DIAGNOSTIC,
translation_key="bluetooth",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="babyCryDetectionState",
translation_key="baby_cry_detection",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="beepingApplianceDetectionState",
translation_key="beeping_appliance_detection",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="coughDetectionState",
translation_key="cough_detection",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="dogBarkDetectionState",
translation_key="dog_bark_detection",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="waterSoundsDetectionState",
translation_key="water_sounds_detection",
is_on_fn=lambda device, key: False,
),
)
async def async_setup_entry(
hass: HomeAssistant,
@@ -66,6 +104,8 @@ async def async_setup_entry(
coordinator = entry.runtime_data
entity_registry = er.async_get(hass)
# Replace unique id for "detectionState" binary sensor
await async_update_unique_id(
hass,
@@ -75,6 +115,16 @@ async def async_setup_entry(
"detectionState",
)
# Clean up deprecated sensors
for sensor_desc in DEPRECATED_BINARY_SENSORS:
for serial_num in coordinator.data:
unique_id = f"{serial_num}-{sensor_desc.key}"
if entity_id := entity_registry.async_get_entity_id(
BINARY_SENSOR_DOMAIN, DOMAIN, unique_id
):
_LOGGER.debug("Removing deprecated entity %s", entity_id)
entity_registry.async_remove(entity_id)
known_devices: set[str] = set()
def _check_device() -> None:

View File

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

View File

@@ -32,7 +32,9 @@ class AmazonSensorEntityDescription(SensorEntityDescription):
native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
device.online and device.sensors[key].error is False
device.online
and (sensor := device.sensors.get(key)) is not None
and sensor.error is False
)
@@ -40,9 +42,9 @@ SENSORS: Final = (
AmazonSensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement_fn=lambda device, _key: (
native_unit_of_measurement_fn=lambda device, key: (
UnitOfTemperature.CELSIUS
if device.sensors[_key].scale == "CELSIUS"
if key in device.sensors and device.sensors[key].scale == "CELSIUS"
else UnitOfTemperature.FAHRENHEIT
),
state_class=SensorStateClass.MEASUREMENT,

View File

@@ -18,7 +18,11 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity
from .utils import alexa_api_call, async_update_unique_id
from .utils import (
alexa_api_call,
async_remove_dnd_from_virtual_group,
async_update_unique_id,
)
PARALLEL_UPDATES = 1
@@ -29,7 +33,9 @@ class AmazonSwitchEntityDescription(SwitchEntityDescription):
is_on_fn: Callable[[AmazonDevice], bool]
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
device.online and device.sensors[key].error is False
device.online
and (sensor := device.sensors.get(key)) is not None
and sensor.error is False
)
method: str
@@ -58,6 +64,9 @@ async def async_setup_entry(
hass, coordinator, SWITCH_DOMAIN, "do_not_disturb", "dnd"
)
# Remove DND switch from virtual groups
await async_remove_dnd_from_virtual_group(hass, coordinator)
known_devices: set[str] = set()
def _check_device() -> None:

View File

@@ -4,8 +4,10 @@ from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from typing import Any, Concatenate
from aioamazondevices.const import SPEAKER_GROUP_FAMILY
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.entity_registry as er
@@ -61,3 +63,21 @@ async def async_update_unique_id(
# Update the registry with the new unique_id
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
async def async_remove_dnd_from_virtual_group(
hass: HomeAssistant,
coordinator: AmazonDevicesCoordinator,
) -> None:
"""Remove entity DND from virtual group."""
entity_registry = er.async_get(hass)
for serial_num in coordinator.data:
unique_id = f"{serial_num}-do_not_disturb"
entity_id = entity_registry.async_get_entity_id(
DOMAIN, SWITCH_DOMAIN, unique_id
)
is_group = coordinator.data[serial_num].device_family == SPEAKER_GROUP_FAMILY
if entity_id and is_group:
entity_registry.async_remove(entity_id)
_LOGGER.debug("Removed DND switch from virtual group %s", entity_id)

View File

@@ -65,6 +65,31 @@ SENSOR_DESCRIPTIONS = [
suggested_display_precision=2,
translation_placeholders={"sensor_name": "BME280"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.HUMIDITY,
key="BME680_humidity",
translation_key="humidity",
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=2,
translation_placeholders={"sensor_name": "BME680"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.PRESSURE,
key="BME680_pressure",
translation_key="pressure",
native_unit_of_measurement=UnitOfPressure.PA,
suggested_unit_of_measurement=UnitOfPressure.MMHG,
suggested_display_precision=0,
translation_placeholders={"sensor_name": "BME680"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.TEMPERATURE,
key="BME680_temperature",
translation_key="temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=2,
translation_placeholders={"sensor_name": "BME680"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.PRESSURE,
key="BMP_pressure",

View File

@@ -19,9 +19,8 @@ CONF_THINKING_BUDGET = "thinking_budget"
RECOMMENDED_THINKING_BUDGET = 0
MIN_THINKING_BUDGET = 1024
THINKING_MODELS = [
"claude-3-7-sonnet",
"claude-sonnet-4-0",
"claude-opus-4-0",
"claude-opus-4-1",
NON_THINKING_MODELS = [
"claude-3-5", # Both sonnet and haiku
"claude-3-opus",
"claude-3-haiku",
]

View File

@@ -51,11 +51,11 @@ from .const import (
DOMAIN,
LOGGER,
MIN_THINKING_BUDGET,
NON_THINKING_MODELS,
RECOMMENDED_CHAT_MODEL,
RECOMMENDED_MAX_TOKENS,
RECOMMENDED_TEMPERATURE,
RECOMMENDED_THINKING_BUDGET,
THINKING_MODELS,
)
# Max number of back and forth with the LLM to generate a response
@@ -364,7 +364,7 @@ class AnthropicBaseLLMEntity(Entity):
if tools:
model_args["tools"] = tools
if (
model.startswith(tuple(THINKING_MODELS))
not model.startswith(tuple(NON_THINKING_MODELS))
and thinking_budget >= MIN_THINKING_BUDGET
):
model_args["thinking"] = ThinkingConfigEnabledParam(

View File

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

View File

@@ -19,8 +19,8 @@
"bleak-retry-connector==4.4.3",
"bluetooth-adapters==2.1.0",
"bluetooth-auto-recovery==1.5.3",
"bluetooth-data-tools==1.28.2",
"dbus-fast==2.44.3",
"habluetooth==5.6.4"
"bluetooth-data-tools==1.28.3",
"dbus-fast==2.44.5",
"habluetooth==5.7.0"
]
}

View File

@@ -15,6 +15,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import ComelitConfigEntry, ComelitVedoSystem
from .utils import DeviceType, new_device_listener
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@@ -29,23 +30,19 @@ async def async_setup_entry(
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
known_devices: set[int] = set()
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
"""Add entities for new monitors."""
entities = [
ComelitVedoBinarySensorEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data["alarm_zones"].values()
if device in new_devices
]
if entities:
async_add_entities(entities)
def _check_device() -> None:
current_devices = set(coordinator.data["alarm_zones"])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
ComelitVedoBinarySensorEntity(
coordinator, device, config_entry.entry_id
)
for device in coordinator.data["alarm_zones"].values()
if device.index in new_devices
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
config_entry.async_on_unload(
new_device_listener(coordinator, _add_new_entities, "alarm_zones")
)
class ComelitVedoBinarySensorEntity(

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from asyncio.exceptions import TimeoutError
from collections.abc import Mapping
import re
from typing import Any
from aiocomelit import (
@@ -27,25 +28,20 @@ from .utils import async_client_session
DEFAULT_HOST = "192.168.1.252"
DEFAULT_PIN = "111111"
pin_regex = r"^[0-9]{4,10}$"
USER_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.matches_regex(pin_regex),
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.string,
vol.Required(CONF_TYPE, default=BRIDGE): vol.In(DEVICE_TYPE_LIST),
}
)
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
{vol.Required(CONF_PIN): cv.matches_regex(pin_regex)}
)
STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PIN): cv.string})
STEP_RECONFIGURE = vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.matches_regex(pin_regex),
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.string,
}
)
@@ -55,6 +51,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
api: ComelitCommonApi
if not re.fullmatch(r"[0-9]{4,10}", data[CONF_PIN]):
raise InvalidPin
session = await async_client_session(hass)
if data.get(CONF_TYPE, BRIDGE) == BRIDGE:
api = ComeliteSerialBridgeApi(
@@ -105,6 +104,8 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except InvalidPin:
errors["base"] = "invalid_pin"
except Exception: # noqa: BLE001
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
@@ -146,6 +147,8 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except InvalidPin:
errors["base"] = "invalid_pin"
except Exception: # noqa: BLE001
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
@@ -189,6 +192,8 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except InvalidPin:
errors["base"] = "invalid_pin"
except Exception: # noqa: BLE001
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
@@ -210,3 +215,7 @@ class CannotConnect(HomeAssistantError):
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
class InvalidPin(HomeAssistantError):
"""Error to indicate an invalid pin."""

View File

@@ -161,7 +161,7 @@ class ComelitSerialBridge(
entry: ComelitConfigEntry,
host: str,
port: int,
pin: int,
pin: str,
session: ClientSession,
) -> None:
"""Initialize the scanner."""
@@ -195,7 +195,7 @@ class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
entry: ComelitConfigEntry,
host: str,
port: int,
pin: int,
pin: str,
session: ClientSession,
) -> None:
"""Initialize the scanner."""

View File

@@ -7,14 +7,21 @@ from typing import Any, cast
from aiocomelit import ComelitSerialBridgeObject
from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
from homeassistant.components.cover import (
STATE_CLOSED,
STATE_CLOSING,
STATE_OPEN,
STATE_OPENING,
CoverDeviceClass,
CoverEntity,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
from .entity import ComelitBridgeBaseEntity
from .utils import bridge_api_call
from .utils import DeviceType, bridge_api_call, new_device_listener
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@@ -29,21 +36,19 @@ async def async_setup_entry(
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
known_devices: set[int] = set()
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
"""Add entities for new monitors."""
entities = [
ComelitCoverEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[dev_type].values()
if device in new_devices
]
if entities:
async_add_entities(entities)
def _check_device() -> None:
current_devices = set(coordinator.data[COVER])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
ComelitCoverEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[COVER].values()
if device.index in new_devices
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
config_entry.async_on_unload(
new_device_listener(coordinator, _add_new_entities, COVER)
)
class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
@@ -62,7 +67,6 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
super().__init__(coordinator, device, config_entry_entry_id)
# Device doesn't provide a status so we assume UNKNOWN at first startup
self._last_action: int | None = None
self._last_state: str | None = None
def _current_action(self, action: str) -> bool:
"""Return the current cover action."""
@@ -98,7 +102,6 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
@bridge_api_call
async def _cover_set_state(self, action: int, state: int) -> None:
"""Set desired cover state."""
self._last_state = self.state
await self.coordinator.api.set_device_status(COVER, self._device.index, action)
self.coordinator.data[COVER][self._device.index].status = state
self.async_write_ha_state()
@@ -124,5 +127,10 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
await super().async_added_to_hass()
if last_state := await self.async_get_last_state():
self._last_state = last_state.state
if (state := await self.async_get_last_state()) is not None:
if state.state == STATE_CLOSED:
self._last_action = STATE_COVER.index(STATE_CLOSING)
if state.state == STATE_OPEN:
self._last_action = STATE_COVER.index(STATE_OPENING)
self._attr_is_closed = state.state == STATE_CLOSED

View File

@@ -12,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
from .entity import ComelitBridgeBaseEntity
from .utils import bridge_api_call
from .utils import DeviceType, bridge_api_call, new_device_listener
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@@ -27,21 +27,19 @@ async def async_setup_entry(
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
known_devices: set[int] = set()
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
"""Add entities for new monitors."""
entities = [
ComelitLightEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[dev_type].values()
if device in new_devices
]
if entities:
async_add_entities(entities)
def _check_device() -> None:
current_devices = set(coordinator.data[LIGHT])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
ComelitLightEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[LIGHT].values()
if device.index in new_devices
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
config_entry.async_on_unload(
new_device_listener(coordinator, _add_new_entities, LIGHT)
)
class ComelitLightEntity(ComelitBridgeBaseEntity, LightEntity):

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"quality_scale": "platinum",
"requirements": ["aiocomelit==0.12.3"]
"requirements": ["aiocomelit==1.1.1"]
}

View File

@@ -20,6 +20,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import ComelitConfigEntry, ComelitSerialBridge, ComelitVedoSystem
from .entity import ComelitBridgeBaseEntity
from .utils import DeviceType, new_device_listener
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@@ -65,24 +66,22 @@ async def async_setup_bridge_entry(
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
known_devices: set[int] = set()
def _check_device() -> None:
current_devices = set(coordinator.data[OTHER])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
ComelitBridgeSensorEntity(
coordinator, device, config_entry.entry_id, sensor_desc
)
for sensor_desc in SENSOR_BRIDGE_TYPES
for device in coordinator.data[OTHER].values()
if device.index in new_devices
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
"""Add entities for new monitors."""
entities = [
ComelitBridgeSensorEntity(
coordinator, device, config_entry.entry_id, sensor_desc
)
for sensor_desc in SENSOR_BRIDGE_TYPES
for device in coordinator.data[dev_type].values()
if device in new_devices
]
if entities:
async_add_entities(entities)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
config_entry.async_on_unload(
new_device_listener(coordinator, _add_new_entities, OTHER)
)
async def async_setup_vedo_entry(
@@ -94,24 +93,22 @@ async def async_setup_vedo_entry(
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
known_devices: set[int] = set()
def _check_device() -> None:
current_devices = set(coordinator.data["alarm_zones"])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
ComelitVedoSensorEntity(
coordinator, device, config_entry.entry_id, sensor_desc
)
for sensor_desc in SENSOR_VEDO_TYPES
for device in coordinator.data["alarm_zones"].values()
if device.index in new_devices
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
"""Add entities for new monitors."""
entities = [
ComelitVedoSensorEntity(
coordinator, device, config_entry.entry_id, sensor_desc
)
for sensor_desc in SENSOR_VEDO_TYPES
for device in coordinator.data["alarm_zones"].values()
if device in new_devices
]
if entities:
async_add_entities(entities)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
config_entry.async_on_unload(
new_device_listener(coordinator, _add_new_entities, "alarm_zones")
)
class ComelitBridgeSensorEntity(ComelitBridgeBaseEntity, SensorEntity):

View File

@@ -43,11 +43,13 @@
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_pin": "The provided PIN is invalid. It must be a 4-10 digit number.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_pin": "[%key:component::comelit::config::abort::invalid_pin%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},

View File

@@ -13,7 +13,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
from .entity import ComelitBridgeBaseEntity
from .utils import bridge_api_call
from .utils import DeviceType, bridge_api_call, new_device_listener
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@@ -28,35 +28,20 @@ async def async_setup_entry(
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
entities: list[ComelitSwitchEntity] = []
entities.extend(
ComelitSwitchEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[IRRIGATION].values()
)
entities.extend(
ComelitSwitchEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[OTHER].values()
)
async_add_entities(entities)
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
"""Add entities for new monitors."""
entities = [
ComelitSwitchEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[dev_type].values()
if device in new_devices
]
if entities:
async_add_entities(entities)
known_devices: dict[str, set[int]] = {
dev_type: set() for dev_type in (IRRIGATION, OTHER)
}
def _check_device() -> None:
for dev_type in (IRRIGATION, OTHER):
current_devices = set(coordinator.data[dev_type])
new_devices = current_devices - known_devices[dev_type]
if new_devices:
known_devices[dev_type].update(new_devices)
async_add_entities(
ComelitSwitchEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[dev_type].values()
if device.index in new_devices
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
for dev_type in (IRRIGATION, OTHER):
config_entry.async_on_unload(
new_device_listener(coordinator, _add_new_entities, dev_type)
)
class ComelitSwitchEntity(ComelitBridgeBaseEntity, SwitchEntity):

View File

@@ -4,7 +4,11 @@ from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from typing import Any, Concatenate
from aiocomelit import ComelitSerialBridgeObject
from aiocomelit.api import (
ComelitSerialBridgeObject,
ComelitVedoAreaObject,
ComelitVedoZoneObject,
)
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
from aiohttp import ClientSession, CookieJar
@@ -19,8 +23,11 @@ from homeassistant.helpers import (
)
from .const import _LOGGER, DOMAIN
from .coordinator import ComelitBaseCoordinator
from .entity import ComelitBridgeBaseEntity
DeviceType = ComelitSerialBridgeObject | ComelitVedoAreaObject | ComelitVedoZoneObject
async def async_client_session(hass: HomeAssistant) -> ClientSession:
"""Return a new aiohttp session."""
@@ -113,3 +120,41 @@ def bridge_api_call[_T: ComelitBridgeBaseEntity, **_P](
self.coordinator.config_entry.async_start_reauth(self.hass)
return cmd_wrapper
def new_device_listener(
coordinator: ComelitBaseCoordinator,
new_devices_callback: Callable[
[
list[
ComelitSerialBridgeObject
| ComelitVedoAreaObject
| ComelitVedoZoneObject
],
str,
],
None,
],
data_type: str,
) -> Callable[[], None]:
"""Subscribe to coordinator updates to check for new devices."""
known_devices: set[int] = set()
def _check_devices() -> None:
"""Check for new devices and call callback with any new monitors."""
if not coordinator.data:
return
new_devices: list[DeviceType] = []
for _id in coordinator.data[data_type]:
if _id not in known_devices:
known_devices.add(_id)
new_devices.append(coordinator.data[data_type][_id])
if new_devices:
new_devices_callback(new_devices, data_type)
# Check for devices immediately
_check_devices()
return coordinator.async_add_listener(_check_devices)

View File

@@ -38,22 +38,30 @@ from home_assistant_intents import (
ErrorKey,
FuzzyConfig,
FuzzyLanguageResponses,
LanguageScores,
get_fuzzy_config,
get_fuzzy_language,
get_intents,
get_language_scores,
get_languages,
)
import yaml
from homeassistant import core
from homeassistant.components.homeassistant.exposed_entities import (
async_listen_entity_updates,
async_should_expose,
)
from homeassistant.const import EVENT_STATE_CHANGED, MATCH_ALL
from homeassistant.core import Event, callback
from homeassistant.core import (
Event,
EventStateChangedData,
HomeAssistant,
State,
callback,
)
from homeassistant.helpers import (
area_registry as ar,
config_validation as cv,
device_registry as dr,
entity_registry as er,
floor_registry as fr,
@@ -192,7 +200,7 @@ class IntentCache:
async def async_setup_default_agent(
hass: core.HomeAssistant,
hass: HomeAssistant,
entity_component: EntityComponent[ConversationEntity],
config_intents: dict[str, Any],
) -> None:
@@ -201,15 +209,13 @@ async def async_setup_default_agent(
await entity_component.async_add_entities([agent])
await get_agent_manager(hass).async_setup_default_agent(agent)
@core.callback
def async_entity_state_listener(
event: core.Event[core.EventStateChangedData],
) -> None:
@callback
def async_entity_state_listener(event: Event[EventStateChangedData]) -> None:
"""Set expose flag on new entities."""
async_should_expose(hass, DOMAIN, event.data["entity_id"])
@core.callback
def async_hass_started(hass: core.HomeAssistant) -> None:
@callback
def async_hass_started(hass: HomeAssistant) -> None:
"""Set expose flag on all entities."""
for state in hass.states.async_all():
async_should_expose(hass, DOMAIN, state.entity_id)
@@ -224,9 +230,7 @@ class DefaultAgent(ConversationEntity):
_attr_name = "Home Assistant"
_attr_supported_features = ConversationEntityFeature.CONTROL
def __init__(
self, hass: core.HomeAssistant, config_intents: dict[str, Any]
) -> None:
def __init__(self, hass: HomeAssistant, config_intents: dict[str, Any]) -> None:
"""Initialize the default agent."""
self.hass = hass
self._lang_intents: dict[str, LanguageIntents | object] = {}
@@ -259,7 +263,7 @@ class DefaultAgent(ConversationEntity):
"""Return a list of supported languages."""
return get_languages()
@core.callback
@callback
def _filter_entity_registry_changes(
self, event_data: er.EventEntityRegistryUpdatedData
) -> bool:
@@ -268,12 +272,12 @@ class DefaultAgent(ConversationEntity):
field in event_data["changes"] for field in _ENTITY_REGISTRY_UPDATE_FIELDS
)
@core.callback
def _filter_state_changes(self, event_data: core.EventStateChangedData) -> bool:
@callback
def _filter_state_changes(self, event_data: EventStateChangedData) -> bool:
"""Filter state changed events."""
return not event_data["old_state"] or not event_data["new_state"]
@core.callback
@callback
def _listen_clear_slot_list(self) -> None:
"""Listen for changes that can invalidate slot list."""
assert self._unsub_clear_slot_list is None
@@ -342,6 +346,81 @@ class DefaultAgent(ConversationEntity):
return result
async def async_debug_recognize(
self, user_input: ConversationInput
) -> dict[str, Any] | None:
"""Debug recognize from user input."""
result_dict: dict[str, Any] | None = None
if trigger_result := await self.async_recognize_sentence_trigger(user_input):
result_dict = {
# Matched a user-defined sentence trigger.
# We can't provide the response here without executing the
# trigger.
"match": True,
"source": "trigger",
"sentence_template": trigger_result.sentence_template or "",
}
elif intent_result := await self.async_recognize_intent(user_input):
successful_match = not intent_result.unmatched_entities
result_dict = {
# Name of the matching intent (or the closest)
"intent": {
"name": intent_result.intent.name,
},
# Slot values that would be received by the intent
"slots": { # direct access to values
entity_key: entity.text or entity.value
for entity_key, entity in intent_result.entities.items()
},
# Extra slot details, such as the originally matched text
"details": {
entity_key: {
"name": entity.name,
"value": entity.value,
"text": entity.text,
}
for entity_key, entity in intent_result.entities.items()
},
# Entities/areas/etc. that would be targeted
"targets": {},
# True if match was successful
"match": successful_match,
# Text of the sentence template that matched (or was closest)
"sentence_template": "",
# When match is incomplete, this will contain the best slot guesses
"unmatched_slots": _get_unmatched_slots(intent_result),
# True if match was not exact
"fuzzy_match": False,
}
if successful_match:
result_dict["targets"] = {
state.entity_id: {"matched": is_matched}
for state, is_matched in _get_debug_targets(
self.hass, intent_result
)
}
if intent_result.intent_sentence is not None:
result_dict["sentence_template"] = intent_result.intent_sentence.text
if intent_result.intent_metadata:
# Inspect metadata to determine if this matched a custom sentence
if intent_result.intent_metadata.get(METADATA_CUSTOM_SENTENCE):
result_dict["source"] = "custom"
result_dict["file"] = intent_result.intent_metadata.get(
METADATA_CUSTOM_FILE
)
else:
result_dict["source"] = "builtin"
result_dict["fuzzy_match"] = intent_result.intent_metadata.get(
METADATA_FUZZY_MATCH, False
)
return result_dict
async def _async_handle_message(
self,
user_input: ConversationInput,
@@ -890,7 +969,7 @@ class DefaultAgent(ConversationEntity):
) -> str:
# Get first matched or unmatched state.
# This is available in the response template as "state".
state1: core.State | None = None
state1: State | None = None
if intent_response.matched_states:
state1 = intent_response.matched_states[0]
elif intent_response.unmatched_states:
@@ -1528,6 +1607,10 @@ class DefaultAgent(ConversationEntity):
return None
return response
async def async_get_language_scores(self) -> dict[str, LanguageScores]:
"""Get support scores per language."""
return await self.hass.async_add_executor_job(get_language_scores)
def _make_error_result(
language: str,
@@ -1589,7 +1672,7 @@ def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str
def _get_match_error_response(
hass: core.HomeAssistant,
hass: HomeAssistant,
match_error: intent.MatchFailedError,
) -> tuple[ErrorKey, dict[str, Any]]:
"""Return key and template arguments for error when target matching fails."""
@@ -1724,3 +1807,75 @@ def _collect_list_references(expression: Expression, list_names: set[str]) -> No
elif isinstance(expression, ListReference):
# {list}
list_names.add(expression.slot_name)
def _get_debug_targets(
hass: HomeAssistant,
result: RecognizeResult,
) -> Iterable[tuple[State, bool]]:
"""Yield state/is_matched pairs for a hassil recognition."""
entities = result.entities
name: str | None = None
area_name: str | None = None
domains: set[str] | None = None
device_classes: set[str] | None = None
state_names: set[str] | None = None
if "name" in entities:
name = str(entities["name"].value)
if "area" in entities:
area_name = str(entities["area"].value)
if "domain" in entities:
domains = set(cv.ensure_list(entities["domain"].value))
if "device_class" in entities:
device_classes = set(cv.ensure_list(entities["device_class"].value))
if "state" in entities:
# HassGetState only
state_names = set(cv.ensure_list(entities["state"].value))
if (
(name is None)
and (area_name is None)
and (not domains)
and (not device_classes)
and (not state_names)
):
# Avoid "matching" all entities when there is no filter
return
states = intent.async_match_states(
hass,
name=name,
area_name=area_name,
domains=domains,
device_classes=device_classes,
)
for state in states:
# For queries, a target is "matched" based on its state
is_matched = (state_names is None) or (state.state in state_names)
yield state, is_matched
def _get_unmatched_slots(
result: RecognizeResult,
) -> dict[str, str | int | float]:
"""Return a dict of unmatched text/range slot entities."""
unmatched_slots: dict[str, str | int | float] = {}
for entity in result.unmatched_entities_list:
if isinstance(entity, UnmatchedTextEntity):
if entity.text == MISSING_ENTITY:
# Don't report <missing> since these are just missing context
# slots.
continue
unmatched_slots[entity.name] = entity.text
elif isinstance(entity, UnmatchedRangeEntity):
unmatched_slots[entity.name] = entity.value
return unmatched_slots

View File

@@ -2,21 +2,16 @@
from __future__ import annotations
from collections.abc import Iterable
from dataclasses import asdict
from typing import Any
from aiohttp import web
from hassil.recognize import MISSING_ENTITY, RecognizeResult
from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity
from home_assistant_intents import get_language_scores
import voluptuous as vol
from homeassistant.components import http, websocket_api
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.const import MATCH_ALL
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers import config_validation as cv, intent
from homeassistant.core import HomeAssistant, callback
from homeassistant.util import language as language_util
from .agent_manager import (
@@ -26,11 +21,6 @@ from .agent_manager import (
get_agent_manager,
)
from .const import DATA_COMPONENT
from .default_agent import (
METADATA_CUSTOM_FILE,
METADATA_CUSTOM_SENTENCE,
METADATA_FUZZY_MATCH,
)
from .entity import ConversationEntity
from .models import ConversationInput
@@ -206,150 +196,12 @@ async def websocket_hass_agent_debug(
language=msg.get("language", hass.config.language),
agent_id=agent.entity_id,
)
result_dict: dict[str, Any] | None = None
if trigger_result := await agent.async_recognize_sentence_trigger(user_input):
result_dict = {
# Matched a user-defined sentence trigger.
# We can't provide the response here without executing the
# trigger.
"match": True,
"source": "trigger",
"sentence_template": trigger_result.sentence_template or "",
}
elif intent_result := await agent.async_recognize_intent(user_input):
successful_match = not intent_result.unmatched_entities
result_dict = {
# Name of the matching intent (or the closest)
"intent": {
"name": intent_result.intent.name,
},
# Slot values that would be received by the intent
"slots": { # direct access to values
entity_key: entity.text or entity.value
for entity_key, entity in intent_result.entities.items()
},
# Extra slot details, such as the originally matched text
"details": {
entity_key: {
"name": entity.name,
"value": entity.value,
"text": entity.text,
}
for entity_key, entity in intent_result.entities.items()
},
# Entities/areas/etc. that would be targeted
"targets": {},
# True if match was successful
"match": successful_match,
# Text of the sentence template that matched (or was closest)
"sentence_template": "",
# When match is incomplete, this will contain the best slot guesses
"unmatched_slots": _get_unmatched_slots(intent_result),
# True if match was not exact
"fuzzy_match": False,
}
if successful_match:
result_dict["targets"] = {
state.entity_id: {"matched": is_matched}
for state, is_matched in _get_debug_targets(hass, intent_result)
}
if intent_result.intent_sentence is not None:
result_dict["sentence_template"] = intent_result.intent_sentence.text
if intent_result.intent_metadata:
# Inspect metadata to determine if this matched a custom sentence
if intent_result.intent_metadata.get(METADATA_CUSTOM_SENTENCE):
result_dict["source"] = "custom"
result_dict["file"] = intent_result.intent_metadata.get(
METADATA_CUSTOM_FILE
)
else:
result_dict["source"] = "builtin"
result_dict["fuzzy_match"] = intent_result.intent_metadata.get(
METADATA_FUZZY_MATCH, False
)
result_dict = await agent.async_debug_recognize(user_input)
result_dicts.append(result_dict)
connection.send_result(msg["id"], {"results": result_dicts})
def _get_debug_targets(
hass: HomeAssistant,
result: RecognizeResult,
) -> Iterable[tuple[State, bool]]:
"""Yield state/is_matched pairs for a hassil recognition."""
entities = result.entities
name: str | None = None
area_name: str | None = None
domains: set[str] | None = None
device_classes: set[str] | None = None
state_names: set[str] | None = None
if "name" in entities:
name = str(entities["name"].value)
if "area" in entities:
area_name = str(entities["area"].value)
if "domain" in entities:
domains = set(cv.ensure_list(entities["domain"].value))
if "device_class" in entities:
device_classes = set(cv.ensure_list(entities["device_class"].value))
if "state" in entities:
# HassGetState only
state_names = set(cv.ensure_list(entities["state"].value))
if (
(name is None)
and (area_name is None)
and (not domains)
and (not device_classes)
and (not state_names)
):
# Avoid "matching" all entities when there is no filter
return
states = intent.async_match_states(
hass,
name=name,
area_name=area_name,
domains=domains,
device_classes=device_classes,
)
for state in states:
# For queries, a target is "matched" based on its state
is_matched = (state_names is None) or (state.state in state_names)
yield state, is_matched
def _get_unmatched_slots(
result: RecognizeResult,
) -> dict[str, str | int | float]:
"""Return a dict of unmatched text/range slot entities."""
unmatched_slots: dict[str, str | int | float] = {}
for entity in result.unmatched_entities_list:
if isinstance(entity, UnmatchedTextEntity):
if entity.text == MISSING_ENTITY:
# Don't report <missing> since these are just missing context
# slots.
continue
unmatched_slots[entity.name] = entity.text
elif isinstance(entity, UnmatchedRangeEntity):
unmatched_slots[entity.name] = entity.value
return unmatched_slots
@websocket_api.websocket_command(
{
vol.Required("type"): "conversation/agent/homeassistant/language_scores",
@@ -364,10 +216,13 @@ async def websocket_hass_agent_language_scores(
msg: dict[str, Any],
) -> None:
"""Get support scores per language."""
agent = get_agent_manager(hass).default_agent
assert agent is not None
language = msg.get("language", hass.config.language)
country = msg.get("country", hass.config.country)
scores = await hass.async_add_executor_job(get_language_scores)
scores = await agent.async_get_language_scores()
matching_langs = language_util.matches(language, scores.keys(), country=country)
preferred_lang = matching_langs[0] if matching_langs else language
result = {

View File

@@ -23,7 +23,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.util.ssl import client_context_no_verify
from .const import KEY_MAC, TIMEOUT
from .const import KEY_MAC, TIMEOUT_SEC
from .coordinator import DaikinConfigEntry, DaikinCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaikinConfigEntry) -> bo
session = async_get_clientsession(hass)
host = conf[CONF_HOST]
try:
async with asyncio.timeout(TIMEOUT):
async with asyncio.timeout(TIMEOUT_SEC):
device: Appliance = await DaikinFactory(
host,
session,
@@ -53,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaikinConfigEntry) -> bo
)
_LOGGER.debug("Connection to %s successful", host)
except TimeoutError as err:
_LOGGER.debug("Connection to %s timed out in 60 seconds", host)
_LOGGER.debug("Connection to %s timed out in %s seconds", host, TIMEOUT_SEC)
raise ConfigEntryNotReady from err
except ClientConnectionError as err:
_LOGGER.debug("ClientConnectionError to %s", host)

View File

@@ -20,7 +20,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from homeassistant.util.ssl import client_context_no_verify
from .const import DOMAIN, KEY_MAC, TIMEOUT
from .const import DOMAIN, KEY_MAC, TIMEOUT_SEC
_LOGGER = logging.getLogger(__name__)
@@ -84,7 +84,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
password = None
try:
async with asyncio.timeout(TIMEOUT):
async with asyncio.timeout(TIMEOUT_SEC):
device: Appliance = await DaikinFactory(
host,
async_get_clientsession(self.hass),

View File

@@ -24,4 +24,4 @@ ATTR_STATE_OFF = "off"
KEY_MAC = "mac"
KEY_IP = "ip"
TIMEOUT = 60
TIMEOUT_SEC = 120

View File

@@ -9,7 +9,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN
from .const import DOMAIN, TIMEOUT_SEC
_LOGGER = logging.getLogger(__name__)
@@ -28,7 +28,7 @@ class DaikinCoordinator(DataUpdateCoordinator[None]):
_LOGGER,
config_entry=entry,
name=device.values.get("name", DOMAIN),
update_interval=timedelta(seconds=60),
update_interval=timedelta(seconds=TIMEOUT_SEC),
)
self.device = device

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/daikin",
"iot_class": "local_polling",
"loggers": ["pydaikin"],
"requirements": ["pydaikin==2.16.0"],
"requirements": ["pydaikin==2.17.1"],
"zeroconf": ["_dkapi._tcp.local."]
}

View File

@@ -17,6 +17,6 @@
"requirements": [
"aiodhcpwatcher==1.2.1",
"aiodiscover==2.7.1",
"cached-ipaddress==0.10.0"
"cached-ipaddress==1.0.1"
]
}

View File

@@ -116,6 +116,9 @@
}
},
"select": {
"active_map": {
"default": "mdi:floor-plan"
},
"water_amount": {
"default": "mdi:water"
},

View File

@@ -2,12 +2,13 @@
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from typing import TYPE_CHECKING, Any
from deebot_client.capabilities import CapabilitySetTypes
from deebot_client.capabilities import CapabilityMap, CapabilitySet, CapabilitySetTypes
from deebot_client.device import Device
from deebot_client.events import WorkModeEvent
from deebot_client.events.base import Event
from deebot_client.events.map import CachedMapInfoEvent, MajorMapEvent
from deebot_client.events.water_info import WaterAmountEvent
from homeassistant.components.select import SelectEntity, SelectEntityDescription
@@ -16,7 +17,11 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import EcovacsConfigEntry
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity
from .entity import (
EcovacsCapabilityEntityDescription,
EcovacsDescriptionEntity,
EcovacsEntity,
)
from .util import get_name_key, get_supported_entities
@@ -66,6 +71,12 @@ async def async_setup_entry(
entities = get_supported_entities(
controller, EcovacsSelectEntity, ENTITY_DESCRIPTIONS
)
entities.extend(
EcovacsActiveMapSelectEntity(device, device.capabilities.map)
for device in controller.devices
if (map_cap := device.capabilities.map)
and isinstance(map_cap.major, CapabilitySet)
)
if entities:
async_add_entities(entities)
@@ -103,3 +114,76 @@ class EcovacsSelectEntity[EventT: Event](
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
await self._device.execute_command(self._capability.set(option))
class EcovacsActiveMapSelectEntity(
EcovacsEntity[CapabilityMap],
SelectEntity,
):
"""Ecovacs active map select entity."""
entity_description = SelectEntityDescription(
key="active_map",
translation_key="active_map",
entity_category=EntityCategory.CONFIG,
)
def __init__(
self,
device: Device,
capability: CapabilityMap,
**kwargs: Any,
) -> None:
"""Initialize entity."""
super().__init__(device, capability, **kwargs)
self._option_to_id: dict[str, str] = {}
self._id_to_option: dict[str, str] = {}
self._handle_on_cached_map(
device.events.get_last_event(CachedMapInfoEvent)
or CachedMapInfoEvent(set())
)
def _handle_on_cached_map(self, event: CachedMapInfoEvent) -> None:
self._id_to_option.clear()
self._option_to_id.clear()
for map_info in event.maps:
name = map_info.name if map_info.name else map_info.id
self._id_to_option[map_info.id] = name
self._option_to_id[name] = map_info.id
if map_info.using:
self._attr_current_option = name
if self._attr_current_option not in self._option_to_id:
self._attr_current_option = None
# Sort named maps first, then numeric IDs (unnamed maps during building) in ascending order.
self._attr_options = sorted(
self._option_to_id.keys(), key=lambda x: (x.isdigit(), x.lower())
)
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
await super().async_added_to_hass()
async def on_cached_map(event: CachedMapInfoEvent) -> None:
self._handle_on_cached_map(event)
self.async_write_ha_state()
self._subscribe(self._capability.cached_info.event, on_cached_map)
async def on_major_map(event: MajorMapEvent) -> None:
self._attr_current_option = self._id_to_option.get(event.map_id)
self.async_write_ha_state()
self._subscribe(self._capability.major.event, on_major_map)
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
if TYPE_CHECKING:
assert isinstance(self._capability.major, CapabilitySet)
await self._device.execute_command(
self._capability.major.set(self._option_to_id[option])
)

View File

@@ -178,6 +178,9 @@
}
},
"select": {
"active_map": {
"name": "Active map"
},
"water_amount": {
"name": "[%key:component::ecovacs::entity::number::water_amount::name%]",
"state": {

View File

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

View File

@@ -396,6 +396,7 @@ class EnvoyCTSensorEntityDescription(SensorEntityDescription):
int | float | str | CtType | CtMeterStatus | CtStatusFlags | CtState | None,
]
on_phase: str | None
cttype: str | None = None
CT_NET_CONSUMPTION_SENSORS = (
@@ -409,6 +410,7 @@ CT_NET_CONSUMPTION_SENSORS = (
suggested_display_precision=3,
value_fn=attrgetter("energy_delivered"),
on_phase=None,
cttype=CtType.NET_CONSUMPTION,
),
EnvoyCTSensorEntityDescription(
key="lifetime_net_production",
@@ -420,6 +422,7 @@ CT_NET_CONSUMPTION_SENSORS = (
suggested_display_precision=3,
value_fn=attrgetter("energy_received"),
on_phase=None,
cttype=CtType.NET_CONSUMPTION,
),
EnvoyCTSensorEntityDescription(
key="net_consumption",
@@ -431,6 +434,7 @@ CT_NET_CONSUMPTION_SENSORS = (
suggested_display_precision=3,
value_fn=attrgetter("active_power"),
on_phase=None,
cttype=CtType.NET_CONSUMPTION,
),
EnvoyCTSensorEntityDescription(
key="frequency",
@@ -442,6 +446,7 @@ CT_NET_CONSUMPTION_SENSORS = (
entity_registry_enabled_default=False,
value_fn=attrgetter("frequency"),
on_phase=None,
cttype=CtType.NET_CONSUMPTION,
),
EnvoyCTSensorEntityDescription(
key="voltage",
@@ -454,6 +459,7 @@ CT_NET_CONSUMPTION_SENSORS = (
entity_registry_enabled_default=False,
value_fn=attrgetter("voltage"),
on_phase=None,
cttype=CtType.NET_CONSUMPTION,
),
EnvoyCTSensorEntityDescription(
key="net_ct_current",
@@ -466,6 +472,7 @@ CT_NET_CONSUMPTION_SENSORS = (
entity_registry_enabled_default=False,
value_fn=attrgetter("current"),
on_phase=None,
cttype=CtType.NET_CONSUMPTION,
),
EnvoyCTSensorEntityDescription(
key="net_ct_powerfactor",
@@ -476,6 +483,7 @@ CT_NET_CONSUMPTION_SENSORS = (
entity_registry_enabled_default=False,
value_fn=attrgetter("power_factor"),
on_phase=None,
cttype=CtType.NET_CONSUMPTION,
),
EnvoyCTSensorEntityDescription(
key="net_consumption_ct_metering_status",
@@ -486,6 +494,7 @@ CT_NET_CONSUMPTION_SENSORS = (
entity_registry_enabled_default=False,
value_fn=attrgetter("metering_status"),
on_phase=None,
cttype=CtType.NET_CONSUMPTION,
),
EnvoyCTSensorEntityDescription(
key="net_consumption_ct_status_flags",
@@ -495,6 +504,7 @@ CT_NET_CONSUMPTION_SENSORS = (
entity_registry_enabled_default=False,
value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags),
on_phase=None,
cttype=CtType.NET_CONSUMPTION,
),
)
@@ -525,6 +535,7 @@ CT_PRODUCTION_SENSORS = (
entity_registry_enabled_default=False,
value_fn=attrgetter("frequency"),
on_phase=None,
cttype=CtType.PRODUCTION,
),
EnvoyCTSensorEntityDescription(
key="production_ct_voltage",
@@ -537,6 +548,7 @@ CT_PRODUCTION_SENSORS = (
entity_registry_enabled_default=False,
value_fn=attrgetter("voltage"),
on_phase=None,
cttype=CtType.PRODUCTION,
),
EnvoyCTSensorEntityDescription(
key="production_ct_current",
@@ -549,6 +561,7 @@ CT_PRODUCTION_SENSORS = (
entity_registry_enabled_default=False,
value_fn=attrgetter("current"),
on_phase=None,
cttype=CtType.PRODUCTION,
),
EnvoyCTSensorEntityDescription(
key="production_ct_powerfactor",
@@ -559,6 +572,7 @@ CT_PRODUCTION_SENSORS = (
entity_registry_enabled_default=False,
value_fn=attrgetter("power_factor"),
on_phase=None,
cttype=CtType.PRODUCTION,
),
EnvoyCTSensorEntityDescription(
key="production_ct_metering_status",
@@ -569,6 +583,7 @@ CT_PRODUCTION_SENSORS = (
entity_registry_enabled_default=False,
value_fn=attrgetter("metering_status"),
on_phase=None,
cttype=CtType.PRODUCTION,
),
EnvoyCTSensorEntityDescription(
key="production_ct_status_flags",
@@ -578,6 +593,7 @@ CT_PRODUCTION_SENSORS = (
entity_registry_enabled_default=False,
value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags),
on_phase=None,
cttype=CtType.PRODUCTION,
),
)
@@ -607,6 +623,7 @@ CT_STORAGE_SENSORS = (
suggested_display_precision=3,
value_fn=attrgetter("energy_delivered"),
on_phase=None,
cttype=CtType.STORAGE,
),
EnvoyCTSensorEntityDescription(
key="lifetime_battery_charged",
@@ -618,6 +635,7 @@ CT_STORAGE_SENSORS = (
suggested_display_precision=3,
value_fn=attrgetter("energy_received"),
on_phase=None,
cttype=CtType.STORAGE,
),
EnvoyCTSensorEntityDescription(
key="battery_discharge",
@@ -629,6 +647,7 @@ CT_STORAGE_SENSORS = (
suggested_display_precision=3,
value_fn=attrgetter("active_power"),
on_phase=None,
cttype=CtType.STORAGE,
),
EnvoyCTSensorEntityDescription(
key="storage_ct_frequency",
@@ -640,6 +659,7 @@ CT_STORAGE_SENSORS = (
entity_registry_enabled_default=False,
value_fn=attrgetter("frequency"),
on_phase=None,
cttype=CtType.STORAGE,
),
EnvoyCTSensorEntityDescription(
key="storage_voltage",
@@ -652,6 +672,7 @@ CT_STORAGE_SENSORS = (
entity_registry_enabled_default=False,
value_fn=attrgetter("voltage"),
on_phase=None,
cttype=CtType.STORAGE,
),
EnvoyCTSensorEntityDescription(
key="storage_ct_current",
@@ -664,6 +685,7 @@ CT_STORAGE_SENSORS = (
entity_registry_enabled_default=False,
value_fn=attrgetter("current"),
on_phase=None,
cttype=CtType.STORAGE,
),
EnvoyCTSensorEntityDescription(
key="storage_ct_powerfactor",
@@ -674,6 +696,7 @@ CT_STORAGE_SENSORS = (
entity_registry_enabled_default=False,
value_fn=attrgetter("power_factor"),
on_phase=None,
cttype=CtType.STORAGE,
),
EnvoyCTSensorEntityDescription(
key="storage_ct_metering_status",
@@ -684,6 +707,7 @@ CT_STORAGE_SENSORS = (
entity_registry_enabled_default=False,
value_fn=attrgetter("metering_status"),
on_phase=None,
cttype=CtType.STORAGE,
),
EnvoyCTSensorEntityDescription(
key="storage_ct_status_flags",
@@ -693,6 +717,7 @@ CT_STORAGE_SENSORS = (
entity_registry_enabled_default=False,
value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags),
on_phase=None,
cttype=CtType.STORAGE,
),
)
@@ -1015,50 +1040,31 @@ async def async_setup_entry(
for description in NET_CONSUMPTION_PHASE_SENSORS[use_phase]
if phase is not None
)
# Add net consumption CT entities
if ctmeter := envoy_data.ctmeter_consumption:
# Add Current Transformer entities
if envoy_data.ctmeters:
entities.extend(
EnvoyConsumptionCTEntity(coordinator, description)
for description in CT_NET_CONSUMPTION_SENSORS
if ctmeter.measurement_type == CtType.NET_CONSUMPTION
EnvoyCTEntity(coordinator, description)
for sensors in (
CT_NET_CONSUMPTION_SENSORS,
CT_PRODUCTION_SENSORS,
CT_STORAGE_SENSORS,
)
for description in sensors
if description.cttype in envoy_data.ctmeters
)
# For each net consumption ct phase reported add net consumption entities
if phase_data := envoy_data.ctmeter_consumption_phases:
# Add Current Transformer phase entities
if ctmeters_phases := envoy_data.ctmeters_phases:
entities.extend(
EnvoyConsumptionCTPhaseEntity(coordinator, description)
for use_phase, phase in phase_data.items()
for description in CT_NET_CONSUMPTION_PHASE_SENSORS[use_phase]
if phase.measurement_type == CtType.NET_CONSUMPTION
)
# Add production CT entities
if ctmeter := envoy_data.ctmeter_production:
entities.extend(
EnvoyProductionCTEntity(coordinator, description)
for description in CT_PRODUCTION_SENSORS
if ctmeter.measurement_type == CtType.PRODUCTION
)
# For each production ct phase reported add production ct entities
if phase_data := envoy_data.ctmeter_production_phases:
entities.extend(
EnvoyProductionCTPhaseEntity(coordinator, description)
for use_phase, phase in phase_data.items()
for description in CT_PRODUCTION_PHASE_SENSORS[use_phase]
if phase.measurement_type == CtType.PRODUCTION
)
# Add storage CT entities
if ctmeter := envoy_data.ctmeter_storage:
entities.extend(
EnvoyStorageCTEntity(coordinator, description)
for description in CT_STORAGE_SENSORS
if ctmeter.measurement_type == CtType.STORAGE
)
# For each storage ct phase reported add storage ct entities
if phase_data := envoy_data.ctmeter_storage_phases:
entities.extend(
EnvoyStorageCTPhaseEntity(coordinator, description)
for use_phase, phase in phase_data.items()
for description in CT_STORAGE_PHASE_SENSORS[use_phase]
if phase.measurement_type == CtType.STORAGE
EnvoyCTPhaseEntity(coordinator, description)
for sensors in (
CT_NET_CONSUMPTION_PHASE_SENSORS,
CT_PRODUCTION_PHASE_SENSORS,
CT_STORAGE_PHASE_SENSORS,
)
for phase, descriptions in sensors.items()
for description in descriptions
if (cttype := description.cttype) in ctmeters_phases
and phase in ctmeters_phases[cttype]
)
if envoy_data.inverters:
@@ -1245,8 +1251,8 @@ class EnvoyNetConsumptionPhaseEntity(EnvoySystemSensorEntity):
return self.entity_description.value_fn(system_net_consumption)
class EnvoyConsumptionCTEntity(EnvoySystemSensorEntity):
"""Envoy net consumption CT entity."""
class EnvoyCTEntity(EnvoySystemSensorEntity):
"""Envoy CT entity."""
entity_description: EnvoyCTSensorEntityDescription
@@ -1255,13 +1261,13 @@ class EnvoyConsumptionCTEntity(EnvoySystemSensorEntity):
self,
) -> int | float | str | CtType | CtMeterStatus | CtStatusFlags | None:
"""Return the state of the CT sensor."""
if (ctmeter := self.data.ctmeter_consumption) is None:
if (cttype := self.entity_description.cttype) not in self.data.ctmeters:
return None
return self.entity_description.value_fn(ctmeter)
return self.entity_description.value_fn(self.data.ctmeters[cttype])
class EnvoyConsumptionCTPhaseEntity(EnvoySystemSensorEntity):
"""Envoy net consumption CT phase entity."""
class EnvoyCTPhaseEntity(EnvoySystemSensorEntity):
"""Envoy CT phase entity."""
entity_description: EnvoyCTSensorEntityDescription
@@ -1272,78 +1278,14 @@ class EnvoyConsumptionCTPhaseEntity(EnvoySystemSensorEntity):
"""Return the state of the CT phase sensor."""
if TYPE_CHECKING:
assert self.entity_description.on_phase
if (ctmeter := self.data.ctmeter_consumption_phases) is None:
if (cttype := self.entity_description.cttype) not in self.data.ctmeters_phases:
return None
if (phase := self.entity_description.on_phase) not in self.data.ctmeters_phases[
cttype
]:
return None
return self.entity_description.value_fn(
ctmeter[self.entity_description.on_phase]
)
class EnvoyProductionCTEntity(EnvoySystemSensorEntity):
"""Envoy net consumption CT entity."""
entity_description: EnvoyCTSensorEntityDescription
@property
def native_value(
self,
) -> int | float | str | CtType | CtMeterStatus | CtStatusFlags | None:
"""Return the state of the CT sensor."""
if (ctmeter := self.data.ctmeter_production) is None:
return None
return self.entity_description.value_fn(ctmeter)
class EnvoyProductionCTPhaseEntity(EnvoySystemSensorEntity):
"""Envoy net consumption CT phase entity."""
entity_description: EnvoyCTSensorEntityDescription
@property
def native_value(
self,
) -> int | float | str | CtType | CtMeterStatus | CtStatusFlags | None:
"""Return the state of the CT phase sensor."""
if TYPE_CHECKING:
assert self.entity_description.on_phase
if (ctmeter := self.data.ctmeter_production_phases) is None:
return None
return self.entity_description.value_fn(
ctmeter[self.entity_description.on_phase]
)
class EnvoyStorageCTEntity(EnvoySystemSensorEntity):
"""Envoy net storage CT entity."""
entity_description: EnvoyCTSensorEntityDescription
@property
def native_value(
self,
) -> int | float | str | CtType | CtMeterStatus | CtStatusFlags | None:
"""Return the state of the CT sensor."""
if (ctmeter := self.data.ctmeter_storage) is None:
return None
return self.entity_description.value_fn(ctmeter)
class EnvoyStorageCTPhaseEntity(EnvoySystemSensorEntity):
"""Envoy net storage CT phase entity."""
entity_description: EnvoyCTSensorEntityDescription
@property
def native_value(
self,
) -> int | float | str | CtType | CtMeterStatus | CtStatusFlags | None:
"""Return the state of the CT phase sensor."""
if TYPE_CHECKING:
assert self.entity_description.on_phase
if (ctmeter := self.data.ctmeter_storage_phases) is None:
return None
return self.entity_description.value_fn(
ctmeter[self.entity_description.on_phase]
self.data.ctmeters_phases[cttype][phase]
)

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env-canada==0.11.2"]
"requirements": ["env-canada==0.11.3"]
}

View File

@@ -22,19 +22,23 @@ import voluptuous as vol
from homeassistant.components import zeroconf
from homeassistant.config_entries import (
SOURCE_ESPHOME,
SOURCE_IGNORE,
SOURCE_REAUTH,
SOURCE_RECONFIGURE,
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
FlowType,
OptionsFlow,
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.data_entry_flow import AbortFlow, FlowResultType
from homeassistant.helpers import discovery_flow
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.esphome import ESPHomeServiceInfo
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from homeassistant.helpers.service_info.mqtt import MqttServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
@@ -75,6 +79,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize flow."""
self._host: str | None = None
self._connected_address: str | None = None
self.__name: str | None = None
self._port: int | None = None
self._password: str | None = None
@@ -498,18 +503,55 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
await self.hass.config_entries.async_remove(
self._entry_with_name_conflict.entry_id
)
return self._async_create_entry()
return await self._async_create_entry()
@callback
def _async_create_entry(self) -> ConfigFlowResult:
async def _async_create_entry(self) -> ConfigFlowResult:
"""Create the config entry."""
assert self._name is not None
assert self._device_info is not None
# Check if Z-Wave capabilities are present and start discovery flow
next_flow_id: str | None = None
if self._device_info.zwave_proxy_feature_flags:
assert self._connected_address is not None
assert self._port is not None
# Start Z-Wave discovery flow and get the flow ID
zwave_result = await self.hass.config_entries.flow.async_init(
"zwave_js",
context={
"source": SOURCE_ESPHOME,
"discovery_key": discovery_flow.DiscoveryKey(
domain=DOMAIN,
key=self._device_info.mac_address,
version=1,
),
},
data=ESPHomeServiceInfo(
name=self._device_info.name,
zwave_home_id=self._device_info.zwave_home_id or None,
ip_address=self._connected_address,
port=self._port,
noise_psk=self._noise_psk,
),
)
if zwave_result["type"] in (
FlowResultType.ABORT,
FlowResultType.CREATE_ENTRY,
):
_LOGGER.debug(
"Unable to continue created Z-Wave JS config flow: %s", zwave_result
)
else:
next_flow_id = zwave_result["flow_id"]
return self.async_create_entry(
title=self._name,
data=self._async_make_config_data(),
options={
CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
},
next_flow=(FlowType.CONFIG_FLOW, next_flow_id) if next_flow_id else None,
)
@callback
@@ -556,7 +598,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
if entry.data.get(CONF_DEVICE_NAME) == self._device_name:
self._entry_with_name_conflict = entry
return await self.async_step_name_conflict()
return self._async_create_entry()
return await self._async_create_entry()
async def _async_reauth_validated_connection(self) -> ConfigFlowResult:
"""Handle reauth validated connection."""
@@ -703,6 +745,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
try:
await cli.connect()
self._device_info = await cli.device_info()
self._connected_address = cli.connected_address
except InvalidAuthAPIError:
return ERROR_INVALID_PASSWORD_AUTH
except RequiresEncryptionAPIError:

View File

@@ -17,9 +17,9 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==41.11.0",
"aioesphomeapi==41.13.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.3.0"
"bleak-esphome==3.4.0"
],
"zeroconf": ["_esphomelib._tcp.local."]
}

View File

@@ -67,7 +67,7 @@ def suitable_nextchange_time(device: FritzhomeDevice) -> bool:
def suitable_temperature(device: FritzhomeDevice) -> bool:
"""Check suitablity for temperature sensor."""
return device.has_temperature_sensor and not device.has_thermostat
return bool(device.has_temperature_sensor)
def entity_category_temperature(device: FritzhomeDevice) -> EntityCategory | None:

View File

@@ -54,7 +54,7 @@ async def async_setup_entry(
except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed(
"OAuth session is not valid, reauth required"
translation_domain=DOMAIN, translation_key="reauth_required"
) from err
raise ConfigEntryNotReady from err
except aiohttp.ClientError as err:
@@ -76,10 +76,6 @@ async def async_unload_entry(
hass: HomeAssistant, entry: GoogleAssistantSDKConfigEntry
) -> bool:
"""Unload a config entry."""
if not hass.config_entries.async_loaded_entries(DOMAIN):
for service_name in hass.services.async_services_for_domain(DOMAIN):
hass.services.async_remove(DOMAIN, service_name)
conversation.async_unset_agent(hass, entry)
return True

View File

@@ -26,7 +26,7 @@ from homeassistant.components.media_player import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
from homeassistant.helpers.event import async_call_later
@@ -68,7 +68,13 @@ async def async_send_text_commands(
) -> list[CommandResponse]:
"""Send text commands to Google Assistant Service."""
# There can only be 1 entry (config_flow has single_instance_allowed)
entry: GoogleAssistantSDKConfigEntry = hass.config_entries.async_entries(DOMAIN)[0]
entries = hass.config_entries.async_loaded_entries(DOMAIN)
if not entries:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="entry_not_loaded",
)
entry: GoogleAssistantSDKConfigEntry = entries[0]
session = entry.runtime_data.session
try:

View File

@@ -1,4 +1,4 @@
"""Support for Google Assistant SDK."""
"""Services for the Google Assistant SDK integration."""
from __future__ import annotations

View File

@@ -59,14 +59,20 @@
},
"media_player": {
"name": "Media player entity",
"description": "Name(s) of media player entities to play response on."
"description": "Name(s) of media player entities to play the Google Assistant's audio response on. This does not target the device for the command itself."
}
}
}
},
"exceptions": {
"entry_not_loaded": {
"message": "Entry not loaded"
},
"grpc_error": {
"message": "Failed to communicate with Google Assistant"
},
"reauth_required": {
"message": "Credentials are invalid, re-authentication required"
}
}
}

View File

@@ -22,6 +22,7 @@ from homeassistant.exceptions import (
from homeassistant.helpers import config_entry_oauth2_flow
_UPLOAD_AND_DOWNLOAD_TIMEOUT = 12 * 3600
_UPLOAD_MAX_RETRIES = 20
_LOGGER = logging.getLogger(__name__)
@@ -150,6 +151,7 @@ class DriveClient:
backup_metadata,
open_stream,
backup.size,
max_retries=_UPLOAD_MAX_RETRIES,
timeout=ClientTimeout(total=_UPLOAD_AND_DOWNLOAD_TIMEOUT),
)
_LOGGER.debug(

View File

@@ -456,6 +456,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
"""Initialize the agent."""
self.entry = entry
self.subentry = subentry
self.default_model = default_model
self._attr_name = subentry.title
self._genai_client = entry.runtime_data
self._attr_unique_id = subentry.subentry_id
@@ -489,7 +490,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
tools = tools or []
tools.append(Tool(google_search=GoogleSearch()))
model_name = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
model_name = options.get(CONF_CHAT_MODEL, self.default_model)
# Avoid INVALID_ARGUMENT Developer instruction is not enabled for <model>
supports_system_instruction = (
"gemma" not in model_name
@@ -620,7 +621,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
def create_generate_content_config(self) -> GenerateContentConfig:
"""Create the GenerateContentConfig for the LLM."""
options = self.subentry.data
model = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
model = options.get(CONF_CHAT_MODEL, self.default_model)
thinking_config: ThinkingConfig | None = None
if model.startswith("models/gemini-2.5") and not model.endswith(
("tts", "image", "image-preview")

View File

@@ -22,6 +22,7 @@ from google.protobuf import timestamp_pb2
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
@@ -91,6 +92,16 @@ def convert_time(time_str: str) -> timestamp_pb2.Timestamp | None:
return timestamp
SENSOR_DESCRIPTIONS = [
SensorEntityDescription(
key="duration",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
)
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
@@ -105,20 +116,20 @@ async def async_setup_entry(
client_options = ClientOptions(api_key=api_key)
client = RoutesAsyncClient(client_options=client_options)
sensor = GoogleTravelTimeSensor(
config_entry, name, api_key, origin, destination, client
)
sensors = [
GoogleTravelTimeSensor(
config_entry, name, api_key, origin, destination, client, sensor_description
)
for sensor_description in SENSOR_DESCRIPTIONS
]
async_add_entities([sensor], False)
async_add_entities(sensors, False)
class GoogleTravelTimeSensor(SensorEntity):
"""Representation of a Google travel time sensor."""
_attr_attribution = ATTRIBUTION
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
_attr_device_class = SensorDeviceClass.DURATION
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__(
self,
@@ -128,8 +139,10 @@ class GoogleTravelTimeSensor(SensorEntity):
origin: str,
destination: str,
client: RoutesAsyncClient,
sensor_description: SensorEntityDescription,
) -> None:
"""Initialize the sensor."""
self.entity_description = sensor_description
self._attr_name = name
self._attr_unique_id = config_entry.entry_id
self._attr_device_info = DeviceInfo(

View File

@@ -73,7 +73,6 @@ class HassioAddonSwitch(HassioAddonEntity, SwitchEntity):
try:
await supervisor_client.addons.start_addon(self._addon_slug)
except SupervisorError as err:
_LOGGER.error("Failed to start addon %s: %s", self._addon_slug, err)
raise HomeAssistantError(err) from err
await self.coordinator.force_addon_info_data_refresh(self._addon_slug)

View File

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

View File

@@ -72,15 +72,21 @@ _TIME_TRIGGER_SCHEMA = vol.Any(
),
)
_WEEKDAY_SCHEMA = vol.Any(
vol.In(WEEKDAYS),
vol.All(cv.ensure_list, [vol.In(WEEKDAYS)]),
cv.entity_domain(["input_weekday"]),
msg=(
"Expected a weekday (mon, tue, wed, thu, fri, sat, sun), "
"a list of weekdays, or an Entity ID with domain 'input_weekday'"
),
)
TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): "time",
vol.Required(CONF_AT): vol.All(cv.ensure_list, [_TIME_TRIGGER_SCHEMA]),
vol.Optional(CONF_WEEKDAY): vol.Any(
vol.In(WEEKDAYS),
vol.All(cv.ensure_list, [vol.In(WEEKDAYS)]),
),
vol.Optional(CONF_WEEKDAY): _WEEKDAY_SCHEMA,
}
)
@@ -117,7 +123,14 @@ async def async_attach_trigger( # noqa: C901
# Check if current weekday matches the configuration
if isinstance(weekday_config, str):
if current_weekday != weekday_config:
# Could be a single weekday string or an entity_id
if weekday_config.startswith("input_weekday."):
if (weekday_state := hass.states.get(weekday_config)) is None:
return
entity_weekdays = weekday_state.attributes.get("weekdays", [])
if current_weekday not in entity_weekdays:
return
elif current_weekday != weekday_config:
return
elif current_weekday not in weekday_config:
return

View File

@@ -456,7 +456,7 @@ class HomeAccessory(Accessory): # type: ignore[misc]
return self._available
@ha_callback
@pyhap_callback # type: ignore[misc]
@pyhap_callback # type: ignore[untyped-decorator]
def run(self) -> None:
"""Handle accessory driver started event."""
if state := self.hass.states.get(self.entity_id):
@@ -725,7 +725,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
self._entry_title = entry_title
self.iid_storage = iid_storage
@pyhap_callback # type: ignore[misc]
@pyhap_callback # type: ignore[untyped-decorator]
def pair(
self, client_username_bytes: bytes, client_public: str, client_permissions: int
) -> bool:
@@ -735,7 +735,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
async_dismiss_setup_message(self.hass, self.entry_id)
return cast(bool, success)
@pyhap_callback # type: ignore[misc]
@pyhap_callback # type: ignore[untyped-decorator]
def unpair(self, client_uuid: UUID) -> None:
"""Override super function to show setup message if unpaired."""
super().unpair(client_uuid)

View File

@@ -71,7 +71,7 @@ class HomeDoorbellAccessory(HomeAccessory):
self.async_update_doorbell_state(None, state)
@ha_callback
@pyhap_callback # type: ignore[misc]
@pyhap_callback # type: ignore[untyped-decorator]
def run(self) -> None:
"""Handle doorbell event."""
if self._char_doorbell_detected:

View File

@@ -10,7 +10,7 @@
"loggers": ["pyhap"],
"requirements": [
"HAP-python==5.0.0",
"fnv-hash-fast==1.5.0",
"fnv-hash-fast==1.6.0",
"PyQRCode==1.2.1",
"base36==0.1.1"
],

View File

@@ -219,7 +219,7 @@ class AirPurifier(Fan):
return preset_mode.lower() != "auto"
@callback
@pyhap_callback # type: ignore[misc]
@pyhap_callback # type: ignore[untyped-decorator]
def run(self) -> None:
"""Handle accessory driver started event.

View File

@@ -229,7 +229,7 @@ class Camera(HomeDoorbellAccessory, PyhapCamera): # type: ignore[misc]
)
self._async_update_motion_state(None, state)
@pyhap_callback # type: ignore[misc]
@pyhap_callback # type: ignore[untyped-decorator]
@callback
def run(self) -> None:
"""Handle accessory driver started event.

View File

@@ -127,7 +127,7 @@ class GarageDoorOpener(HomeAccessory):
self.async_update_state(state)
@callback
@pyhap_callback # type: ignore[misc]
@pyhap_callback # type: ignore[untyped-decorator]
def run(self) -> None:
"""Handle accessory driver started event.

View File

@@ -178,7 +178,7 @@ class HumidifierDehumidifier(HomeAccessory):
self._async_update_current_humidity(humidity_state)
@callback
@pyhap_callback # type: ignore[misc]
@pyhap_callback # type: ignore[untyped-decorator]
def run(self) -> None:
"""Handle accessory driver started event.

View File

@@ -108,7 +108,7 @@ class DeviceTriggerAccessory(HomeAccessory):
_LOGGER.log,
)
@pyhap_callback # type: ignore[misc]
@pyhap_callback # type: ignore[untyped-decorator]
@callback
def run(self) -> None:
"""Run the accessory."""

View File

@@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"],
"requirements": ["aiohomekit==3.2.19"],
"requirements": ["aiohomekit==3.2.20"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
}

View File

@@ -122,11 +122,24 @@ async def async_setup_entry(
coordinators.main.new_zones_callbacks.append(_add_new_zones)
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(SERVICE_RESUME, None, "resume")
platform.async_register_entity_service(
SERVICE_START_WATERING, SCHEMA_START_WATERING, "start_watering"
SERVICE_RESUME,
None,
"resume",
entity_device_classes=(BinarySensorDeviceClass.RUNNING,),
)
platform.async_register_entity_service(
SERVICE_START_WATERING,
SCHEMA_START_WATERING,
"start_watering",
entity_device_classes=(BinarySensorDeviceClass.RUNNING,),
)
platform.async_register_entity_service(
SERVICE_SUSPEND,
SCHEMA_SUSPEND,
"suspend",
entity_device_classes=(BinarySensorDeviceClass.RUNNING,),
)
platform.async_register_entity_service(SERVICE_SUSPEND, SCHEMA_SUSPEND, "suspend")
class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity):

View File

@@ -8,13 +8,16 @@ from idasen_ha import Desk
from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
type IdasenDeskConfigEntry = ConfigEntry[IdasenDeskCoordinator]
UPDATE_DEBOUNCE_TIME = 0.2
class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]):
"""Class to manage updates for the Idasen Desk."""
@@ -33,9 +36,18 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]):
hass, _LOGGER, config_entry=config_entry, name=config_entry.title
)
self.address = address
self._expected_connected = False
self.desk = Desk(self._async_handle_update)
self.desk = Desk(self.async_set_updated_data)
self._expected_connected = False
self._height: int | None = None
self._debouncer = Debouncer(
hass=self.hass,
logger=_LOGGER,
cooldown=UPDATE_DEBOUNCE_TIME,
immediate=True,
function=callback(lambda: self.async_set_updated_data(self._height)),
)
async def async_connect(self) -> bool:
"""Connect to desk."""
@@ -60,3 +72,9 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]):
"""Ensure that the desk is connected if that is the expected state."""
if self._expected_connected:
await self.async_connect()
@callback
def _async_handle_update(self, height: int | None) -> None:
"""Handle an update from the desk."""
self._height = height
self._debouncer.async_schedule_call()

View File

@@ -0,0 +1,285 @@
"""Support to select weekdays for use in automation."""
from __future__ import annotations
import logging
from typing import Any, Self
import voluptuous as vol
from homeassistant.const import (
ATTR_EDITABLE,
CONF_ICON,
CONF_ID,
CONF_NAME,
SERVICE_RELOAD,
WEEKDAYS,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import collection, config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.helpers.service
from homeassistant.helpers.storage import Store
from homeassistant.helpers.typing import ConfigType, VolDictType
_LOGGER = logging.getLogger(__name__)
DOMAIN = "input_weekday"
CONF_WEEKDAYS = "weekdays"
ATTR_WEEKDAYS = "weekdays"
ATTR_WEEKDAY = "weekday"
SERVICE_SET_WEEKDAYS = "set_weekdays"
SERVICE_ADD_WEEKDAY = "add_weekday"
SERVICE_REMOVE_WEEKDAY = "remove_weekday"
SERVICE_TOGGLE_WEEKDAY = "toggle_weekday"
SERVICE_CLEAR = "clear"
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
STORAGE_FIELDS: VolDictType = {
vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
vol.Optional(CONF_WEEKDAYS, default=list): vol.All(
cv.ensure_list, [vol.In(WEEKDAYS)]
),
vol.Optional(CONF_ICON): cv.icon,
}
def _cv_input_weekday(cfg: dict[str, Any]) -> dict[str, Any]:
"""Configure validation helper for input weekday (voluptuous)."""
if CONF_WEEKDAYS in cfg:
weekdays = cfg[CONF_WEEKDAYS]
# Remove duplicates while preserving order
cfg[CONF_WEEKDAYS] = list(dict.fromkeys(weekdays))
return cfg
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: cv.schema_with_slug_keys(
vol.All(
{
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_WEEKDAYS): vol.All(
cv.ensure_list, [vol.In(WEEKDAYS)]
),
vol.Optional(CONF_ICON): cv.icon,
},
_cv_input_weekday,
)
)
},
extra=vol.ALLOW_EXTRA,
)
RELOAD_SERVICE_SCHEMA = vol.Schema({})
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up an input weekday."""
component = EntityComponent[InputWeekday](_LOGGER, DOMAIN, hass)
id_manager = collection.IDManager()
yaml_collection = collection.YamlCollection(
logging.getLogger(f"{__name__}.yaml_collection"), id_manager
)
collection.sync_entity_lifecycle(
hass, DOMAIN, DOMAIN, component, yaml_collection, InputWeekday
)
storage_collection = InputWeekdayStorageCollection(
Store(hass, STORAGE_VERSION, STORAGE_KEY),
id_manager,
)
collection.sync_entity_lifecycle(
hass, DOMAIN, DOMAIN, component, storage_collection, InputWeekday
)
await yaml_collection.async_load(
[{CONF_ID: id_, **cfg} for id_, cfg in config.get(DOMAIN, {}).items()]
)
await storage_collection.async_load()
collection.DictStorageCollectionWebsocket(
storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
).async_setup(hass)
async def reload_service_handler(service_call: ServiceCall) -> None:
"""Reload yaml entities."""
conf = await component.async_prepare_reload(skip_reset=True)
if conf is None:
conf = {DOMAIN: {}}
await yaml_collection.async_load(
[{CONF_ID: id_, **cfg} for id_, cfg in conf.get(DOMAIN, {}).items()]
)
homeassistant.helpers.service.async_register_admin_service(
hass,
DOMAIN,
SERVICE_RELOAD,
reload_service_handler,
schema=RELOAD_SERVICE_SCHEMA,
)
component.async_register_entity_service(
SERVICE_SET_WEEKDAYS,
{vol.Required(ATTR_WEEKDAYS): vol.All(cv.ensure_list, [vol.In(WEEKDAYS)])},
"async_set_weekdays",
)
component.async_register_entity_service(
SERVICE_ADD_WEEKDAY,
{vol.Required(ATTR_WEEKDAY): vol.In(WEEKDAYS)},
"async_add_weekday",
)
component.async_register_entity_service(
SERVICE_REMOVE_WEEKDAY,
{vol.Required(ATTR_WEEKDAY): vol.In(WEEKDAYS)},
"async_remove_weekday",
)
component.async_register_entity_service(
SERVICE_TOGGLE_WEEKDAY,
{vol.Required(ATTR_WEEKDAY): vol.In(WEEKDAYS)},
"async_toggle_weekday",
)
component.async_register_entity_service(
SERVICE_CLEAR,
None,
"async_clear",
)
return True
class InputWeekdayStorageCollection(collection.DictStorageCollection):
"""Input weekday storage based collection."""
CREATE_UPDATE_SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, _cv_input_weekday))
async def _process_create_data(self, data: dict[str, Any]) -> dict[str, Any]:
"""Validate the config is valid."""
return self.CREATE_UPDATE_SCHEMA(data)
@callback
def _get_suggested_id(self, info: dict[str, Any]) -> str:
"""Suggest an ID based on the config."""
return info[CONF_NAME]
async def _update_data(
self, item: dict[str, Any], update_data: dict[str, Any]
) -> dict[str, Any]:
"""Return a new updated data object."""
update_data = self.CREATE_UPDATE_SCHEMA(update_data)
return item | update_data
# pylint: disable-next=hass-enforce-class-module
class InputWeekday(collection.CollectionEntity, RestoreEntity):
"""Representation of a weekday input."""
_unrecorded_attributes = frozenset({ATTR_EDITABLE})
_attr_should_poll = False
editable: bool
def __init__(self, config: ConfigType) -> None:
"""Initialize a weekday input."""
self._config = config
self._attr_weekdays = config.get(CONF_WEEKDAYS, [])
self._attr_unique_id = config[CONF_ID]
@classmethod
def from_storage(cls, config: ConfigType) -> Self:
"""Return entity instance initialized from storage."""
input_weekday = cls(config)
input_weekday.editable = True
return input_weekday
@classmethod
def from_yaml(cls, config: ConfigType) -> Self:
"""Return entity instance initialized from yaml."""
input_weekday = cls(config)
input_weekday.entity_id = f"{DOMAIN}.{config[CONF_ID]}"
input_weekday.editable = False
return input_weekday
@property
def name(self) -> str:
"""Return name of the weekday input."""
return self._config.get(CONF_NAME) or self._config[CONF_ID]
@property
def icon(self) -> str | None:
"""Return the icon to be used for this entity."""
return self._config.get(CONF_ICON)
@property
def state(self) -> str:
"""Return the state of the entity."""
# Return a comma-separated string of selected weekdays
return ",".join(self._attr_weekdays) if self._attr_weekdays else ""
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the entity."""
return {
ATTR_WEEKDAYS: self._attr_weekdays,
ATTR_EDITABLE: self.editable,
}
async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
# Restore previous state if no initial weekdays were provided
if self._config.get(CONF_WEEKDAYS) is not None:
return
state = await self.async_get_last_state()
if state is not None and ATTR_WEEKDAYS in state.attributes:
self._attr_weekdays = state.attributes[ATTR_WEEKDAYS]
async def async_set_weekdays(self, weekdays: list[str]) -> None:
"""Set the selected weekdays."""
# Remove duplicates while preserving order
self._attr_weekdays = list(dict.fromkeys(weekdays))
self.async_write_ha_state()
async def async_add_weekday(self, weekday: str) -> None:
"""Add a weekday to the selection."""
if weekday not in self._attr_weekdays:
self._attr_weekdays.append(weekday)
self.async_write_ha_state()
async def async_remove_weekday(self, weekday: str) -> None:
"""Remove a weekday from the selection."""
if weekday in self._attr_weekdays:
self._attr_weekdays.remove(weekday)
self.async_write_ha_state()
async def async_toggle_weekday(self, weekday: str) -> None:
"""Toggle a weekday in the selection."""
if weekday in self._attr_weekdays:
self._attr_weekdays.remove(weekday)
else:
self._attr_weekdays.append(weekday)
self.async_write_ha_state()
async def async_clear(self) -> None:
"""Clear all selected weekdays."""
self._attr_weekdays = []
self.async_write_ha_state()
async def async_update_config(self, config: ConfigType) -> None:
"""Handle when the config is updated."""
self._config = config
self._attr_weekdays = config.get(CONF_WEEKDAYS, [])
self.async_write_ha_state()

View File

@@ -0,0 +1,29 @@
{
"entity": {
"input_weekday": {
"default": {
"default": "mdi:calendar-week"
}
}
},
"services": {
"set_weekdays": {
"service": "mdi:calendar-edit"
},
"add_weekday": {
"service": "mdi:calendar-plus"
},
"remove_weekday": {
"service": "mdi:calendar-minus"
},
"toggle_weekday": {
"service": "mdi:calendar-check"
},
"clear": {
"service": "mdi:calendar-remove"
},
"reload": {
"service": "mdi:reload"
}
}
}

View File

@@ -0,0 +1,8 @@
{
"domain": "input_weekday",
"name": "Input Weekday",
"codeowners": ["@home-assistant/core"],
"documentation": "https://www.home-assistant.io/integrations/input_weekday",
"integration_type": "helper",
"quality_scale": "internal"
}

View File

@@ -0,0 +1,42 @@
"""Reproduce an Input Weekday state."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import Context, HomeAssistant, State
from . import ATTR_WEEKDAYS, DOMAIN, SERVICE_SET_WEEKDAYS
_LOGGER = logging.getLogger(__name__)
async def async_reproduce_states(
hass: HomeAssistant,
states: list[State],
*,
context: Context | None = None,
reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce Input Weekday states."""
for state in states:
if ATTR_WEEKDAYS not in state.attributes:
_LOGGER.warning(
"Unable to reproduce state for %s: %s attribute is missing",
state.entity_id,
ATTR_WEEKDAYS,
)
continue
weekdays = state.attributes[ATTR_WEEKDAYS]
service_data = {
ATTR_ENTITY_ID: state.entity_id,
ATTR_WEEKDAYS: weekdays,
}
await hass.services.async_call(
DOMAIN, SERVICE_SET_WEEKDAYS, service_data, context=context, blocking=True
)

View File

@@ -0,0 +1,115 @@
set_weekdays:
target:
entity:
domain: input_weekday
fields:
weekdays:
required: true
example: '["mon", "wed", "fri"]'
selector:
select:
multiple: true
mode: list
options:
- value: mon
label: Monday
- value: tue
label: Tuesday
- value: wed
label: Wednesday
- value: thu
label: Thursday
- value: fri
label: Friday
- value: sat
label: Saturday
- value: sun
label: Sunday
add_weekday:
target:
entity:
domain: input_weekday
fields:
weekday:
required: true
example: mon
selector:
select:
mode: dropdown
options:
- value: mon
label: Monday
- value: tue
label: Tuesday
- value: wed
label: Wednesday
- value: thu
label: Thursday
- value: fri
label: Friday
- value: sat
label: Saturday
- value: sun
label: Sunday
remove_weekday:
target:
entity:
domain: input_weekday
fields:
weekday:
required: true
example: mon
selector:
select:
mode: dropdown
options:
- value: mon
label: Monday
- value: tue
label: Tuesday
- value: wed
label: Wednesday
- value: thu
label: Thursday
- value: fri
label: Friday
- value: sat
label: Saturday
- value: sun
label: Sunday
toggle_weekday:
target:
entity:
domain: input_weekday
fields:
weekday:
required: true
example: mon
selector:
select:
mode: dropdown
options:
- value: mon
label: Monday
- value: tue
label: Tuesday
- value: wed
label: Wednesday
- value: thu
label: Thursday
- value: fri
label: Friday
- value: sat
label: Saturday
- value: sun
label: Sunday
clear:
target:
entity:
domain: input_weekday
reload:

View File

@@ -0,0 +1,70 @@
{
"title": "Input Weekday",
"entity_component": {
"_": {
"name": "[%key:component::input_weekday::title%]",
"state_attributes": {
"weekdays": {
"name": "Weekdays"
},
"editable": {
"name": "[%key:common::generic::ui_managed%]",
"state": {
"true": "[%key:common::state::yes%]",
"false": "[%key:common::state::no%]"
}
}
}
}
},
"services": {
"set_weekdays": {
"name": "Set weekdays",
"description": "Sets the selected weekdays.",
"fields": {
"weekdays": {
"name": "Weekdays",
"description": "List of weekdays to select."
}
}
},
"add_weekday": {
"name": "Add weekday",
"description": "Adds a weekday to the selection.",
"fields": {
"weekday": {
"name": "Weekday",
"description": "Weekday to add."
}
}
},
"remove_weekday": {
"name": "Remove weekday",
"description": "Removes a weekday from the selection.",
"fields": {
"weekday": {
"name": "Weekday",
"description": "Weekday to remove."
}
}
},
"toggle_weekday": {
"name": "Toggle weekday",
"description": "Toggles a weekday in the selection.",
"fields": {
"weekday": {
"name": "Weekday",
"description": "Weekday to toggle."
}
}
},
"clear": {
"name": "Clear",
"description": "Clears all selected weekdays."
},
"reload": {
"name": "[%key:common::action::reload%]",
"description": "Reloads helpers from the YAML-configuration."
}
}
}

View File

@@ -147,8 +147,9 @@ class KrakenData:
def _get_websocket_name_asset_pairs(self) -> str:
return ",".join(
self.tradable_asset_pairs[tracked_pair]
pair
for tracked_pair in self._config_entry.options[CONF_TRACKED_ASSET_PAIRS]
if (pair := self.tradable_asset_pairs.get(tracked_pair)) is not None
)
def set_update_interval(self, update_interval: int) -> None:

View File

@@ -156,7 +156,7 @@ async def async_setup_entry(
for description in SENSOR_TYPES
]
)
async_add_entities(entities, True)
async_add_entities(entities)
_async_add_kraken_sensors(config_entry.options[CONF_TRACKED_ASSET_PAIRS])

View File

@@ -37,5 +37,5 @@
"iot_class": "cloud_push",
"loggers": ["pylamarzocco"],
"quality_scale": "platinum",
"requirements": ["pylamarzocco==2.1.1"]
"requirements": ["pylamarzocco==2.1.2"]
}

View File

@@ -5,7 +5,7 @@ from dataclasses import dataclass
from datetime import datetime
from typing import cast
from pylamarzocco.const import BackFlushStatus, ModelName, WidgetType
from pylamarzocco.const import BackFlushStatus, MachineState, ModelName, WidgetType
from pylamarzocco.models import (
BackFlush,
BaseWidgetOutput,
@@ -97,7 +97,14 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
).brewing_start_time
),
entity_category=EntityCategory.DIAGNOSTIC,
available_fn=(lambda coordinator: not coordinator.websocket_terminated),
available_fn=(
lambda coordinator: not coordinator.websocket_terminated
and cast(
MachineStatus,
coordinator.device.dashboard.config[WidgetType.CM_MACHINE_STATUS],
).status
is MachineState.BREWING
),
),
LaMarzoccoSensorEntityDescription(
key="steam_boiler_ready_time",

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/ld2410_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["bluetooth-data-tools==1.28.2", "ld2410-ble==0.1.1"]
"requirements": ["bluetooth-data-tools==1.28.3", "ld2410-ble==0.1.1"]
}

View File

@@ -35,5 +35,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/led_ble",
"iot_class": "local_polling",
"requirements": ["bluetooth-data-tools==1.28.2", "led-ble==1.1.7"]
"requirements": ["bluetooth-data-tools==1.28.3", "led-ble==1.1.7"]
}

View File

@@ -7,6 +7,6 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["letpot"],
"quality_scale": "bronze",
"quality_scale": "silver",
"requirements": ["letpot==0.6.2"]
}

View File

@@ -41,7 +41,10 @@ rules:
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: todo
log-when-unavailable:
status: done
comment: |
Logging handled by library when (un)available once (push) or coordinator (pull).
parallel-updates: done
reauthentication-flow: done
test-coverage: done

View File

@@ -196,11 +196,11 @@ class LocalTodoListEntity(TodoListEntity):
item_idx: dict[str, int] = {itm.uid: idx for idx, itm in enumerate(todos)}
if uid not in item_idx:
raise HomeAssistantError(
"Item '{uid}' not found in todo list {self.entity_id}"
f"Item '{uid}' not found in todo list {self.entity_id}"
)
if previous_uid and previous_uid not in item_idx:
raise HomeAssistantError(
"Item '{previous_uid}' not found in todo list {self.entity_id}"
f"Item '{previous_uid}' not found in todo list {self.entity_id}"
)
dst_idx = item_idx[previous_uid] + 1 if previous_uid else 0
src_idx = item_idx[uid]

View File

@@ -88,6 +88,17 @@ DISCOVERY_SCHEMAS = [
entity_class=MatterBinarySensor,
required_attributes=(clusters.OccupancySensing.Attributes.Occupancy,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="ThermostatOccupancySensor",
device_class=BinarySensorDeviceClass.OCCUPANCY,
# The first bit = if occupied
device_to_ha=lambda x: (x & 1 == 1) if x is not None else None,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.Thermostat.Attributes.Occupancy,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(

View File

@@ -146,6 +146,13 @@
"off": "mdi:lock-off"
}
},
"speaker_mute": {
"default": "mdi:volume-high",
"state": {
"on": "mdi:volume-mute",
"off": "mdi:volume-high"
}
},
"evse_charging_switch": {
"default": "mdi:ev-station"
},

View File

@@ -176,6 +176,7 @@ DISCOVERY_SCHEMAS = [
),
entity_class=MatterNumber,
required_attributes=(clusters.LevelControl.Attributes.OnLevel,),
not_device_type=(device_types.Speaker,),
# allow None value to account for 'default' value
allow_none_value=True,
),

View File

@@ -152,6 +152,7 @@ PUMP_CONTROL_MODE_MAP = {
clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kUnknownEnumValue: None,
}
HUMIDITY_SCALING_FACTOR = 100
TEMPERATURE_SCALING_FACTOR = 100
@@ -308,7 +309,7 @@ DISCOVERY_SCHEMAS = [
key="TemperatureSensor",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
device_to_ha=lambda x: x / 100,
device_to_ha=lambda x: x / TEMPERATURE_SCALING_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
@@ -344,7 +345,7 @@ DISCOVERY_SCHEMAS = [
key="HumiditySensor",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
device_to_ha=lambda x: x / 100,
device_to_ha=lambda x: x / HUMIDITY_SCALING_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
@@ -1136,7 +1137,7 @@ DISCOVERY_SCHEMAS = [
key="ThermostatLocalTemperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
device_to_ha=lambda x: x / 100,
device_to_ha=lambda x: x / TEMPERATURE_SCALING_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,

View File

@@ -514,6 +514,9 @@
"power": {
"name": "Power"
},
"speaker_mute": {
"name": "Mute"
},
"child_lock": {
"name": "Child lock"
},

View File

@@ -203,7 +203,6 @@ DISCOVERY_SCHEMAS = [
device_types.Refrigerator,
device_types.RoboticVacuumCleaner,
device_types.RoomAirConditioner,
device_types.Speaker,
),
),
MatterDiscoverySchema(
@@ -242,6 +241,24 @@ DISCOVERY_SCHEMAS = [
device_types.Speaker,
),
),
MatterDiscoverySchema(
platform=Platform.SWITCH,
entity_description=MatterNumericSwitchEntityDescription(
key="MatterMuteToggle",
translation_key="speaker_mute",
device_to_ha={
True: False, # True means volume is on, so HA should show mute as off
False: True, # False means volume is off (muted), so HA should show mute as on
}.get,
ha_to_device={
False: True, # HA showing mute as off means volume is on, so send True
True: False, # HA showing mute as on means volume is off (muted), so send False
}.get,
),
entity_class=MatterNumericSwitch,
required_attributes=(clusters.OnOff.Attributes.OnOff,),
device_type=(device_types.Speaker,),
),
MatterDiscoverySchema(
platform=Platform.SWITCH,
entity_description=MatterNumericSwitchEntityDescription(

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
import logging
from typing import Any, cast
@@ -23,7 +24,13 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
from . import async_get_config_entry_implementation
from .application_credentials import authorization_server_context
from .const import CONF_ACCESS_TOKEN, CONF_AUTHORIZATION_URL, CONF_TOKEN_URL, DOMAIN
from .const import (
CONF_ACCESS_TOKEN,
CONF_AUTHORIZATION_URL,
CONF_SCOPE,
CONF_TOKEN_URL,
DOMAIN,
)
from .coordinator import TokenManager, mcp_client
_LOGGER = logging.getLogger(__name__)
@@ -41,9 +48,17 @@ MCP_DISCOVERY_HEADERS = {
}
@dataclass
class OAuthConfig:
"""Class to hold OAuth configuration."""
authorization_server: AuthorizationServer
scopes: list[str] | None = None
async def async_discover_oauth_config(
hass: HomeAssistant, mcp_server_url: str
) -> AuthorizationServer:
) -> OAuthConfig:
"""Discover the OAuth configuration for the MCP server.
This implements the functionality in the MCP spec for discovery. If the MCP server URL
@@ -65,9 +80,11 @@ async def async_discover_oauth_config(
except httpx.HTTPStatusError as error:
if error.response.status_code == 404:
_LOGGER.info("Authorization Server Metadata not found, using default paths")
return AuthorizationServer(
authorize_url=str(parsed_url.with_path("/authorize")),
token_url=str(parsed_url.with_path("/token")),
return OAuthConfig(
authorization_server=AuthorizationServer(
authorize_url=str(parsed_url.with_path("/authorize")),
token_url=str(parsed_url.with_path("/token")),
)
)
raise CannotConnect from error
except httpx.HTTPError as error:
@@ -81,9 +98,15 @@ async def async_discover_oauth_config(
authorize_url = str(parsed_url.with_path(authorize_url))
if token_url.startswith("/"):
token_url = str(parsed_url.with_path(token_url))
return AuthorizationServer(
authorize_url=authorize_url,
token_url=token_url,
# We have no way to know the minimum set of scopes needed, so request
# all of them and let the user limit during the authorization step.
scopes = data.get("scopes_supported")
return OAuthConfig(
authorization_server=AuthorizationServer(
authorize_url=authorize_url,
token_url=token_url,
),
scopes=scopes,
)
@@ -130,6 +153,7 @@ class ModelContextProtocolConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
"""Initialize the config flow."""
super().__init__()
self.data: dict[str, Any] = {}
self.oauth_config: OAuthConfig | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -170,7 +194,7 @@ class ModelContextProtocolConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
to find the OAuth medata then run the OAuth authentication flow.
"""
try:
authorization_server = await async_discover_oauth_config(
oauth_config = await async_discover_oauth_config(
self.hass, self.data[CONF_URL]
)
except TimeoutConnectError:
@@ -181,11 +205,13 @@ class ModelContextProtocolConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
return self.async_abort(reason="unknown")
else:
_LOGGER.info("OAuth configuration: %s", authorization_server)
_LOGGER.info("OAuth configuration: %s", oauth_config)
self.oauth_config = oauth_config
self.data.update(
{
CONF_AUTHORIZATION_URL: authorization_server.authorize_url,
CONF_TOKEN_URL: authorization_server.token_url,
CONF_AUTHORIZATION_URL: oauth_config.authorization_server.authorize_url,
CONF_TOKEN_URL: oauth_config.authorization_server.token_url,
CONF_SCOPE: oauth_config.scopes,
}
)
return await self.async_step_credentials_choice()
@@ -197,6 +223,15 @@ class ModelContextProtocolConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
self.data[CONF_TOKEN_URL],
)
@property
def extra_authorize_data(self) -> dict:
"""Extra data that needs to be appended to the authorize url."""
data = {}
if self.data and (scopes := self.data[CONF_SCOPE]) is not None:
data[CONF_SCOPE] = " ".join(scopes)
data.update(super().extra_authorize_data)
return data
async def async_step_credentials_choice(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:

View File

@@ -5,3 +5,4 @@ DOMAIN = "mcp"
CONF_ACCESS_TOKEN = "access_token"
CONF_AUTHORIZATION_URL = "authorization_url"
CONF_TOKEN_URL = "token_url"
CONF_SCOPE = "scope"

View File

@@ -1,7 +1,16 @@
"""Model Context Protocol transport protocol for Server Sent Events (SSE).
"""Model Context Protocol transport protocol for Streamable HTTP and SSE.
This registers HTTP endpoints that supports SSE as a transport layer
for the Model Context Protocol. There are two HTTP endpoints:
This registers HTTP endpoints that support the Streamable HTTP protocol as
well as the older SSE as a transport layer.
The Streamable HTTP protocol uses a single HTTP endpoint:
- /api/mcp_server: The Streamable HTTP endpoint currently implements the
stateless protocol for simplicity. This receives client requests and
sends them to the MCP server, then waits for a response to send back to
the client.
The older SSE protocol has two HTTP endpoints:
- /mcp_server/sse: The SSE endpoint that is used to establish a session
with the client and glue to the MCP server. This is used to push responses
@@ -14,6 +23,9 @@ for the Model Context Protocol. There are two HTTP endpoints:
See https://modelcontextprotocol.io/docs/concepts/transports
"""
import asyncio
from dataclasses import dataclass
from http import HTTPStatus
import logging
from aiohttp import web
@@ -21,13 +33,14 @@ from aiohttp.web_exceptions import HTTPBadRequest, HTTPNotFound
from aiohttp_sse import sse_response
import anyio
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
from mcp import types
from mcp import JSONRPCRequest, types
from mcp.server import InitializationOptions, Server
from mcp.shared.message import SessionMessage
from homeassistant.components import conversation
from homeassistant.components.http import KEY_HASS, HomeAssistantView
from homeassistant.const import CONF_LLM_HASS_API
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import Context, HomeAssistant, callback
from homeassistant.helpers import llm
from .const import DOMAIN
@@ -37,6 +50,14 @@ from .types import MCPServerConfigEntry
_LOGGER = logging.getLogger(__name__)
# Streamable HTTP endpoint
STREAMABLE_API = "/api/mcp"
TIMEOUT = 60 # Seconds
# Content types
CONTENT_TYPE_JSON = "application/json"
# Legacy SSE endpoint
SSE_API = f"/{DOMAIN}/sse"
MESSAGES_API = f"/{DOMAIN}/messages/{{session_id}}"
@@ -46,6 +67,7 @@ def async_register(hass: HomeAssistant) -> None:
"""Register the websocket API."""
hass.http.register_view(ModelContextProtocolSSEView())
hass.http.register_view(ModelContextProtocolMessagesView())
hass.http.register_view(ModelContextProtocolStreamableView())
def async_get_config_entry(hass: HomeAssistant) -> MCPServerConfigEntry:
@@ -66,6 +88,52 @@ def async_get_config_entry(hass: HomeAssistant) -> MCPServerConfigEntry:
return config_entries[0]
@dataclass
class Streams:
"""Pairs of streams for MCP server communication."""
# The MCP server reads from the read stream. The HTTP handler receives
# incoming client messages and writes the to the read_stream_writer.
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]
read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]
# The MCP server writes to the write stream. The HTTP handler reads from
# the write stream and sends messages to the client.
write_stream: MemoryObjectSendStream[SessionMessage]
write_stream_reader: MemoryObjectReceiveStream[SessionMessage]
def create_streams() -> Streams:
"""Create a new pair of streams for MCP server communication."""
read_stream_writer, read_stream = anyio.create_memory_object_stream(0)
write_stream, write_stream_reader = anyio.create_memory_object_stream(0)
return Streams(
read_stream=read_stream,
read_stream_writer=read_stream_writer,
write_stream=write_stream,
write_stream_reader=write_stream_reader,
)
async def create_mcp_server(
hass: HomeAssistant, context: Context, entry: MCPServerConfigEntry
) -> tuple[Server, InitializationOptions]:
"""Initialize the MCP server to ensure it's ready to handle requests."""
llm_context = llm.LLMContext(
platform=DOMAIN,
context=context,
language="*",
assistant=conversation.DOMAIN,
device_id=None,
)
llm_api_id = entry.data[CONF_LLM_HASS_API]
server = await create_server(hass, llm_api_id, llm_context)
options = await hass.async_add_executor_job(
server.create_initialization_options # Reads package for version info
)
return server, options
class ModelContextProtocolSSEView(HomeAssistantView):
"""Model Context Protocol SSE endpoint."""
@@ -86,30 +154,12 @@ class ModelContextProtocolSSEView(HomeAssistantView):
entry = async_get_config_entry(hass)
session_manager = entry.runtime_data
context = llm.LLMContext(
platform=DOMAIN,
context=self.context(request),
language="*",
assistant=conversation.DOMAIN,
device_id=None,
)
llm_api_id = entry.data[CONF_LLM_HASS_API]
server = await create_server(hass, llm_api_id, context)
options = await hass.async_add_executor_job(
server.create_initialization_options # Reads package for version info
)
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]
read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]
read_stream_writer, read_stream = anyio.create_memory_object_stream(0)
write_stream: MemoryObjectSendStream[SessionMessage]
write_stream_reader: MemoryObjectReceiveStream[SessionMessage]
write_stream, write_stream_reader = anyio.create_memory_object_stream(0)
server, options = await create_mcp_server(hass, self.context(request), entry)
streams = create_streams()
async with (
sse_response(request) as response,
session_manager.create(Session(read_stream_writer)) as session_id,
session_manager.create(Session(streams.read_stream_writer)) as session_id,
):
session_uri = MESSAGES_API.format(session_id=session_id)
_LOGGER.debug("Sending SSE endpoint: %s", session_uri)
@@ -117,7 +167,7 @@ class ModelContextProtocolSSEView(HomeAssistantView):
async def sse_reader() -> None:
"""Forward MCP server responses to the client."""
async for session_message in write_stream_reader:
async for session_message in streams.write_stream_reader:
_LOGGER.debug("Sending SSE message: %s", session_message)
await response.send(
session_message.message.model_dump_json(
@@ -128,7 +178,7 @@ class ModelContextProtocolSSEView(HomeAssistantView):
async with anyio.create_task_group() as tg:
tg.start_soon(sse_reader)
await server.run(read_stream, write_stream, options)
await server.run(streams.read_stream, streams.write_stream, options)
return response
@@ -168,3 +218,64 @@ class ModelContextProtocolMessagesView(HomeAssistantView):
_LOGGER.debug("Received client message: %s", message)
await session.read_stream_writer.send(SessionMessage(message))
return web.Response(status=200)
class ModelContextProtocolStreamableView(HomeAssistantView):
"""Model Context Protocol Streamable HTTP endpoint."""
name = f"{DOMAIN}:streamable"
url = STREAMABLE_API
async def get(self, request: web.Request) -> web.StreamResponse:
"""Handle unsupported methods."""
return web.Response(
status=HTTPStatus.METHOD_NOT_ALLOWED, text="Only POST method is supported"
)
async def post(self, request: web.Request) -> web.StreamResponse:
"""Process JSON-RPC messages for the Model Context Protocol."""
hass = request.app[KEY_HASS]
entry = async_get_config_entry(hass)
# The request must include a JSON-RPC message
if CONTENT_TYPE_JSON not in request.headers.get("accept", ""):
raise HTTPBadRequest(text=f"Client must accept {CONTENT_TYPE_JSON}")
if request.content_type != CONTENT_TYPE_JSON:
raise HTTPBadRequest(text=f"Content-Type must be {CONTENT_TYPE_JSON}")
try:
json_data = await request.json()
message = types.JSONRPCMessage.model_validate(json_data)
except ValueError as err:
_LOGGER.debug("Failed to parse message as JSON-RPC message: %s", err)
raise HTTPBadRequest(text="Request must be a JSON-RPC message") from err
_LOGGER.debug("Received client message: %s", message)
# For notifications and responses only, return 202 Accepted
if not isinstance(message.root, JSONRPCRequest):
_LOGGER.debug("Notification or response received, returning 202")
return web.Response(status=HTTPStatus.ACCEPTED)
# The MCP server runs as a background task for the duration of the
# request. We open a buffered stream pair to communicate with it. The
# request is sent to the MCP server and we wait for a single response
# then shut down the server.
server, options = await create_mcp_server(hass, self.context(request), entry)
streams = create_streams()
async def run_server() -> None:
await server.run(
streams.read_stream, streams.write_stream, options, stateless=True
)
async with asyncio.timeout(TIMEOUT), anyio.create_task_group() as tg:
tg.start_soon(run_server)
await streams.read_stream_writer.send(SessionMessage(message))
session_message = await anext(streams.write_stream_reader)
tg.cancel_scope.cancel()
_LOGGER.debug("Sending response: %s", session_message)
return web.json_response(
data=session_message.message.model_dump(by_alias=True, exclude_none=True),
)

View File

@@ -59,7 +59,7 @@ async def create_server(
# Backwards compatibility with old MCP Server config
return await llm.async_get_api(hass, llm_api_id, llm_context)
@server.list_prompts() # type: ignore[no-untyped-call, misc]
@server.list_prompts() # type: ignore[no-untyped-call,untyped-decorator]
async def handle_list_prompts() -> list[types.Prompt]:
llm_api = await get_api_instance()
return [
@@ -69,7 +69,7 @@ async def create_server(
)
]
@server.get_prompt() # type: ignore[no-untyped-call, misc]
@server.get_prompt() # type: ignore[no-untyped-call,untyped-decorator]
async def handle_get_prompt(
name: str, arguments: dict[str, str] | None
) -> types.GetPromptResult:
@@ -90,13 +90,13 @@ async def create_server(
],
)
@server.list_tools() # type: ignore[no-untyped-call, misc]
@server.list_tools() # type: ignore[no-untyped-call,untyped-decorator]
async def list_tools() -> list[types.Tool]:
"""List available time tools."""
llm_api = await get_api_instance()
return [_format_tool(tool, llm_api.custom_serializer) for tool in llm_api.tools]
@server.call_tool() # type: ignore[misc]
@server.call_tool() # type: ignore[untyped-decorator]
async def call_tool(name: str, arguments: dict) -> Sequence[types.TextContent]:
"""Handle calling tools."""
llm_api = await get_api_instance()

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