Compare commits

..

109 Commits

Author SHA1 Message Date
Franck Nijhof 394dafd980 2024.6.3 (#119742) 2024-06-15 21:05:26 +02:00
Franck Nijhof eba429dc54 Temporary pin CI to Python 3.12.3 (#119261) 2024-06-15 20:36:35 +02:00
Franck Nijhof 89ce8478de Bump version to 2024.6.3 2024-06-15 18:23:39 +02:00
Franck Nijhof a4a8315376 Ensure workday issues are not persistent (#119732) 2024-06-15 18:23:29 +02:00
Franck Nijhof 3a705fd668 Ensure UniFi Protect EA warning is not persistent (#119730) 2024-06-15 18:23:25 +02:00
TheJulianJES dc0fc318b8 Bump ZHA dependencies (#119713)
* Bump bellows to 0.39.1

* Bump zigpy to 0.64.1
2024-06-15 18:23:22 +02:00
J. Nick Koston 5ceb8537eb Bump uiprotect to 1.7.2 (#119705)
changelog: https://github.com/uilibs/uiprotect/compare/v1.7.1...v1.7.2
2024-06-15 18:23:19 +02:00
J. Nick Koston d7d7782a69 Bump uiprotect to 1.7.1 (#119694)
changelog: https://github.com/uilibs/uiprotect/compare/v1.6.0...v1.7.0
2024-06-15 18:23:16 +02:00
G Johansson 2d4176d581 Fix alarm default code in concord232 (#119691) 2024-06-15 18:23:12 +02:00
J. Nick Koston 204e9a79c5 Bump uiprotect to 1.6.0 (#119661) 2024-06-15 18:23:09 +02:00
J. Nick Koston ace7da2328 Bump uiprotect to 1.4.1 (#119653) 2024-06-15 18:21:52 +02:00
mletenay dfe25ff804 Bump goodwe to 0.3.6 (#119646) 2024-06-15 18:21:49 +02:00
J. Nick Koston 2b44cf898e Soften unifiprotect EA channel message (#119641) 2024-06-15 18:21:45 +02:00
Paul Bottein c77ed921de Update frontend to 20240610.1 (#119634) 2024-06-15 18:21:03 +02:00
Jan Bouwhuis 78e13d138f Fix group enabled platforms are preloaded if they have alternative states (#119621) 2024-06-15 18:20:05 +02:00
J. Nick Koston 4e394597bd Bump uiprotect to 1.2.1 (#119620)
* Bump uiprotect to 1.2.0

changelog: https://github.com/uilibs/uiprotect/compare/v1.1.0...v1.2.0

* bump
2024-06-15 18:20:02 +02:00
starkillerOG 78c2dc708c Fix error for Reolink snapshot streams (#119572) 2024-06-15 18:19:58 +02:00
Ethem Cem Özkan 4c1d2e7ac8 Revert "Revert Use integration fallback configuration for tado water fallback" (#119526)
* Revert "Revert Use integration fallback configuration for tado water heater fallback (#119466)"

This reverts commit ade936e6d5.

* add decide method for duration

* add repair issue to let users know

* test module for repairs

* Update strings.json

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

* repair issue should not be persistent

* use issue_registery fixture instead of mocking

* fix comment

* parameterize repair issue created test case

---------

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2024-06-15 18:19:55 +02:00
Jan-Philipp Benecke 7b809a8e55 Partially revert "Add more debug logging to Ping integration" (#119487) 2024-06-15 18:19:52 +02:00
Erwin Douna 4eea448f9d Revert Use integration fallback configuration for tado water heater fallback (#119466) 2024-06-15 18:19:48 +02:00
Joakim Plate f58882c878 Add loggers to gardena bluetooth (#119460) 2024-06-15 18:19:45 +02:00
J. Nick Koston 4e6e9f35b5 Bump uiprotect to 1.1.0 (#119449) 2024-06-15 18:19:42 +02:00
Sebastian Goscik d5e9976b2c Bump uiprotect to v1.0.1 (#119436) 2024-06-15 18:19:39 +02:00
MJJ 8d547d4599 Bump buieradar to 1.0.6 (#119433) 2024-06-15 18:19:32 +02:00
J. Nick Koston 94d79440a0 Fix incorrect key name in unifiprotect options strings (#119417) 2024-06-15 18:19:29 +02:00
J. Nick Koston d602b7d19b Bump uiprotect to 1.0.0 (#119415) 2024-06-15 18:19:26 +02:00
J. Nick Koston fb5de55c3e Bump uiprotect to 0.13.0 (#119344) 2024-06-15 18:19:23 +02:00
J. Nick Koston 5cf0ee936d Bump uiprotect to 0.10.1 (#119327)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2024-06-15 18:19:19 +02:00
tronikos 7443878333 Make remaining time of timers available to LLMs (#118696)
* Include speech_slots in IntentResponse.as_dict

* Populate speech_slots only if available

* fix typo

* Add test

* test all fields

* Fix another test

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2024-06-15 18:19:16 +02:00
Franck Nijhof 090d296135 2024.6.2 (#119376) 2024-06-11 14:41:12 +02:00
Franck Nijhof 415bfb40a7 Bump version to 2024.6.2 2024-06-11 11:21:51 +02:00
Maciej Bieniek 7ced4e981e Bump imgw-pib backend library to version 1.0.5 (#119360)
Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
2024-06-11 11:17:29 +02:00
swcloudgenie b656ef4d4f Fix AladdinConnect OAuth domain (#119336)
fix aladdin connect oauth domain
2024-06-11 11:17:26 +02:00
Erik Montnemery 6ea18a7b24 Fix statistic_during_period after core restart (#119323) 2024-06-11 11:17:22 +02:00
Bram Kragten a0ac9fe6c9 Update frontend to 20240610.0 (#119320) 2024-06-11 11:16:04 +02:00
Jan-Philipp Benecke 135735126a Add more debug logging to Ping integration (#119318) 2024-06-11 11:10:36 +02:00
J. Nick Koston 3bc6cf666a Bump uiprotect to 0.4.1 (#119308) 2024-06-11 11:10:33 +02:00
Franck Nijhof 1929e103c0 Fix persistence on OpenWeatherMap raised repair issue (#119289) 2024-06-11 11:10:30 +02:00
J. Nick Koston 74b49556f9 Improve workday test coverage (#119259) 2024-06-11 11:10:27 +02:00
J. Nick Koston 8d40f4d39f Bump uiprotect to 0.4.0 (#119256) 2024-06-11 11:10:24 +02:00
Allen Porter eed126c6d4 Bump google-nest-sdm to 4.0.5 (#119255) 2024-06-11 11:10:21 +02:00
J. Nick Koston 38cd84fa5f Fix climate on/off in nexia (#119254) 2024-06-11 11:10:18 +02:00
Abílio Costa a28f5baeeb Fix wrong arg name in Idasen Desk config flow (#119247) 2024-06-11 11:10:14 +02:00
J. Nick Koston f9352dfe8f Switch unifiprotect lib to use uiprotect (#119243) 2024-06-11 11:09:20 +02:00
J. Nick Koston 5beff34069 Remove myself as codeowner for unifiprotect (#118824) 2024-06-11 10:59:51 +02:00
Joakim Plate 119d4c2316 Always provide a currentArmLevel in Google assistant (#119238) 2024-06-11 10:29:34 +02:00
epenet 1e7ab07d9e Revert SamsungTV migration (#119234) 2024-06-11 10:29:31 +02:00
Ethem Cem Özkan 7896e7675c Bump python-roborock to 2.3.0 (#119228) 2024-06-11 10:29:28 +02:00
wittypluck 8b415a0376 Fix Glances v4 network and container issues (glances-api 0.8.0) (#119226) 2024-06-11 10:29:25 +02:00
Angel Nunez Mencias 6a656c5d49 Fixes crashes when receiving malformed decoded payloads (#119216)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2024-06-11 10:29:21 +02:00
G Johansson 8d094bf12e Fix envisalink alarm (#119212) 2024-06-11 10:29:18 +02:00
Sid c71b6bdac9 Add fallback to entry_id when no mac address is retrieved in enigma2 (#119185) 2024-06-11 10:29:15 +02:00
tronikos 57cc1f841b Bump opower to 0.4.7 (#119183) 2024-06-11 10:29:12 +02:00
Quentin d8f3778d77 Fix elgato light color detection (#119177) 2024-06-11 10:29:08 +02:00
Shay Levy 9a8e3ad5cc Handle Shelly BLE errors during connect and disconnect (#119174) 2024-06-11 10:29:05 +02:00
Michael 019d33c06c Use more conservative timeout values in Synology DSM (#119169)
use ClientTimeout object
2024-06-11 10:29:02 +02:00
Michael 40ebf3b2a9 Bump py-synologydsm-api to 2.4.4 (#119156)
bump py-synologydsm-api to 2.4.4
2024-06-11 10:28:58 +02:00
Tom Brien 7912c9e95c Fix workday timezone (#119148) 2024-06-11 10:28:55 +02:00
Paulus Schoutsen 4bb1ea1da1 Ensure intent tools have safe names (#119144) 2024-06-11 10:28:51 +02:00
Joost Lekkerkerker a696ea18d3 Bump aiowaqi to 3.1.0 (#119124) 2024-06-11 10:28:48 +02:00
Shay Levy df96b94985 Bump aioshelly to 10.0.1 (#119123) 2024-06-11 10:28:45 +02:00
tronikos 0f8ed4e73d Catch GoogleAPICallError in Google Generative AI (#119118) 2024-06-11 10:28:41 +02:00
tronikos 34477d3559 Properly handle escaped unicode characters passed to tools in Google Generative AI (#119117) 2024-06-11 10:28:38 +02:00
Austin Drummond 96ac566032 Fix control 4 on os 2 (#119104) 2024-06-11 10:28:35 +02:00
J. Nick Koston 87f48b15d1 Ensure multiple executions of a restart automation in the same event loop iteration are allowed (#119100)
* Add test for restarting automation

related issue #119097

* fix

* add a delay since restart is an infinite loop

* tests
2024-06-11 10:28:31 +02:00
kaareseras a1f2140ed7 Fix Azure data explorer (#119089)
Co-authored-by: Robert Resch <robert@resch.dev>
2024-06-11 10:28:28 +02:00
tronikos db7a9321be Bump google-generativeai to 0.6.0 (#119062) 2024-06-11 10:28:24 +02:00
G Johansson ebb0a453f4 Calculate attributes when entity information available in Group sensor (#119021) 2024-06-11 10:28:21 +02:00
Joakim Plate 7da10794a8 Update gardena library to 1.4.2 (#119010) 2024-06-11 10:28:18 +02:00
Ruben Bokobza 461f0865af Bump pyElectra to 1.2.1 (#118958) 2024-06-11 10:28:15 +02:00
karwosts fc83bb1737 Fix statistic_during_period wrongly prioritizing ST statistics over LT (#115291)
* Fix statistic_during_period wrongly prioritizing ST statistics over LT

* comment

* start of a test

* more testcases

* fix sts insertion range

* update from review

* remove unneeded comments

* update logic

* min/mean/max testing
2024-06-11 10:28:09 +02:00
Franck Nijhof b28cdcfc49 2024.6.1 (#119096) 2024-06-07 21:20:44 +02:00
Franck Nijhof 3f70e2b6f0 Bump version to 2024.6.1 2024-06-07 20:26:53 +02:00
Joost Lekkerkerker ed22e98861 Fix Azure Data Explorer strings (#119067) 2024-06-07 20:24:03 +02:00
Marc Mueller 093f07c04e Add type ignore comments (#119052) 2024-06-07 20:24:00 +02:00
Joost Lekkerkerker b5693ca604 Fix AirGradient name (#119046) 2024-06-07 20:23:57 +02:00
J. Nick Koston 20b77aa15f Fix remember_the_milk calling configurator async api from the wrong thread (#119029) 2024-06-07 20:23:53 +02:00
J. Nick Koston 1cbd3ab930 Fix refactoring error in snmp switch (#119028) 2024-06-07 20:23:50 +02:00
Matthias Alphart 31b44b7846 Fix KNX climate.set_hvac_mode not turning on (#119012) 2024-06-07 20:23:47 +02:00
Matthias Alphart de3a0841d8 Increase test coverage for KNX Climate (#117903)
* Increase test coverage fro KNX Climate

* fix test type annotation
2024-06-07 20:23:40 +02:00
Mike Degatano 581fb2f9f4 Always have addon url in detached_addon_missing (#119011) 2024-06-07 20:21:25 +02:00
Shay Levy 5bb4e4f5d9 Hold connection lock in Shelly RPC reconnect (#119009) 2024-06-07 20:21:22 +02:00
J. Nick Koston cfa619b67e Remove isal from after_dependencies in http (#119000) 2024-06-07 20:21:18 +02:00
Michael Hansen 56db7fc7dc Fix exposure checks on some intents (#118988)
* Check exposure in climate intent

* Check exposure in todo list

* Check exposure for weather

* Check exposure in humidity intents

* Add extra checks to weather tests

* Add more checks to todo intent test

* Move climate intents to async_match_targets

* Update test_intent.py

* Update test_intent.py

* Remove patch
2024-06-07 20:20:41 +02:00
Joost Lekkerkerker 1f6be7b4d1 Fix unit of measurement for airgradient sensor (#118981) 2024-06-07 20:18:45 +02:00
Maciej Bieniek 52d1432d81 Bump imgw-pib library to version 1.0.4 (#118978)
Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
2024-06-07 20:18:41 +02:00
David Knowles 14da1e9b23 Bump pydrawise to 2024.6.3 (#118977) 2024-06-07 20:18:38 +02:00
G Johansson d6e1d05e87 Bump python-holidays to 0.50 (#118965) 2024-06-07 20:18:35 +02:00
G Johansson 62f73cfcca Fix Alarm control panel not require code in several integrations (#118961) 2024-06-07 20:18:32 +02:00
Maciej Bieniek 6e9a53d02e Bump imgw-pib backend library to version 1.0.2 (#118953)
Bump imgw-pib to version 1.0.2

Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
2024-06-07 20:18:28 +02:00
J. Nick Koston 394c13af1d Revert "Bump orjson to 3.10.3 (#116945)" (#118920)
This reverts commit dc50095d06.
2024-06-07 20:17:01 +02:00
Jan-Philipp Benecke 86b13e8ae3 Fix flaky Google Assistant test (#118914)
* Fix flaky Google Assistant test

* Trigger full ci
2024-06-07 20:16:58 +02:00
Rami Mosleh 5a7332a135 Check if imap message text has a value instead of checking if its not None (#118901)
* Check if message_text has a value instead of checking if its not None

* Strip message_text to ensure that its actually empty or not

* Add test with multipart payload having empty plain text
2024-06-07 20:16:55 +02:00
Michael Hansen 0f9a91d369 Prioritize literal text with name slots in sentence matching (#118900)
Prioritize literal text with name slots
2024-06-07 20:16:52 +02:00
Marc Mueller 00dd86fb4b Update requests to 2.32.3 (#118868)
Co-authored-by: Robert Resch <robert@resch.dev>
2024-06-07 20:16:47 +02:00
Franck Nijhof 460909a7f6 2024.6.0 (#118400) 2024-06-05 20:06:01 +02:00
Franck Nijhof 21fd012447 Bump version to 2024.6.0 2024-06-05 19:00:08 +02:00
Robert Resch c27f0c560e Replace slave by meter in v2c (#118893) 2024-06-05 18:59:52 +02:00
Michael Hansen 0f4a1b421e Bump intents to 2024.6.5 (#118890) 2024-06-05 18:59:49 +02:00
Erik Montnemery 5e35ce2996 Improve WS command validate_config (#118864)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Robert Resch <robert@resch.dev>
2024-06-05 18:59:44 +02:00
Franck Nijhof e5804307e7 Bump version to 2024.6.0b9 2024-06-05 15:51:19 +02:00
Bram Kragten 3b74b63b23 Update frontend to 20240605.0 (#118875) 2024-06-05 15:51:08 +02:00
Marc Mueller 06df32d9d4 Fix TypeAliasType not callable in senz (#118872) 2024-06-05 15:51:05 +02:00
Jan Bouwhuis 63947e4980 Improve repair issue when notify service is still being used (#118855)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2024-06-05 15:51:02 +02:00
Ethem Cem Özkan ac6a377478 Bump python-roborock to 2.2.3 (#118853)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2024-06-05 15:50:59 +02:00
Paulus Schoutsen 18af423a78 Fix the radio browser doing I/O in the event loop (#118842) 2024-06-05 15:50:56 +02:00
Franck Nijhof f1445bc8f5 Fix capitalization of protocols in Reolink option flow (#118839) 2024-06-05 15:50:53 +02:00
starkillerOG 3784c99305 Conserve Reolink battery by not waking the camera on each update (#118773)
* update to new cmd_list type

* Wake battery cams each 1 hour

* fix styling

* fix epoch

* fix timezone

* force full update when using generic update service

* improve comment

* Use time.time() instead of datetime

* fix import order
2024-06-05 15:50:50 +02:00
Pete Sage 0084d6c5bd Fix Hydrawise sensor availability (#118669)
Co-authored-by: Robert Resch <robert@resch.dev>
2024-06-05 15:50:47 +02:00
208 changed files with 2355 additions and 744 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ on:
env:
BUILD_TYPE: core
DEFAULT_PYTHON: "3.12"
DEFAULT_PYTHON: "3.12.3"
PIP_TIMEOUT: 60
UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true"
+2 -2
View File
@@ -37,8 +37,8 @@ env:
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 8
HA_SHORT_VERSION: "2024.6"
DEFAULT_PYTHON: "3.12"
ALL_PYTHON_VERSIONS: "['3.12']"
DEFAULT_PYTHON: "3.12.3"
ALL_PYTHON_VERSIONS: "['3.12.3']"
# 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
# 10.6 is the current long-term-support
+1 -1
View File
@@ -10,7 +10,7 @@ on:
- "**strings.json"
env:
DEFAULT_PYTHON: "3.11"
DEFAULT_PYTHON: "3.12.3"
jobs:
upload:
+1 -1
View File
@@ -17,7 +17,7 @@ on:
- "script/gen_requirements_all.py"
env:
DEFAULT_PYTHON: "3.12"
DEFAULT_PYTHON: "3.12.3"
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name}}
-1
View File
@@ -163,7 +163,6 @@ homeassistant.components.easyenergy.*
homeassistant.components.ecovacs.*
homeassistant.components.ecowitt.*
homeassistant.components.efergy.*
homeassistant.components.electrasmart.*
homeassistant.components.electric_kiwi.*
homeassistant.components.elgato.*
homeassistant.components.elkm1.*
-2
View File
@@ -1486,8 +1486,6 @@ build.json @home-assistant/supervisor
/tests/components/unifi/ @Kane610
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
/homeassistant/components/unifiled/ @florisvdk
/homeassistant/components/unifiprotect/ @bdraco
/tests/components/unifiprotect/ @bdraco
/homeassistant/components/upb/ @gwww
/tests/components/upb/ @gwww
/homeassistant/components/upc_connect/ @pvizeli @fabaff
+10 -3
View File
@@ -134,8 +134,15 @@ COOLDOWN_TIME = 60
DEBUGGER_INTEGRATIONS = {"debugpy"}
# Core integrations are unconditionally loaded
CORE_INTEGRATIONS = {"homeassistant", "persistent_notification"}
LOGGING_INTEGRATIONS = {
# Integrations that are loaded right after the core is set up
LOGGING_AND_HTTP_DEPS_INTEGRATIONS = {
# isal is loaded right away before `http` to ensure if its
# enabled, that `isal` is up to date.
"isal",
# Set log levels
"logger",
# Error logging
@@ -214,8 +221,8 @@ CRITICAL_INTEGRATIONS = {
}
SETUP_ORDER = (
# Load logging as soon as possible
("logging", LOGGING_INTEGRATIONS),
# Load logging and http deps as soon as possible
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS),
# Setup frontend and recorder
("frontend, recorder", {*FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS}),
# Start up debuggers. Start these first in case they want to wait.
@@ -43,6 +43,7 @@ class AgentBaseStation(AlarmControlPanelEntity):
| AlarmControlPanelEntityFeature.ARM_AWAY
| AlarmControlPanelEntityFeature.ARM_NIGHT
)
_attr_code_arm_required = False
_attr_has_entity_name = True
_attr_name = None
@@ -1,6 +1,6 @@
{
"domain": "airgradient",
"name": "Airgradient",
"name": "AirGradient",
"codeowners": ["@airgradienthq", "@joostlek"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airgradient",
@@ -103,6 +103,7 @@ SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = (
AirGradientSensorEntityDescription(
key="pm003",
translation_key="pm003_count",
native_unit_of_measurement="particles/dL",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.pm003_count,
),
@@ -48,7 +48,7 @@
"name": "Nitrogen index"
},
"pm003_count": {
"name": "PM0.3 count"
"name": "PM0.3"
},
"raw_total_volatile_organic_component": {
"name": "Raw total VOC"
@@ -2,5 +2,5 @@
DOMAIN = "aladdin_connect"
OAUTH2_AUTHORIZE = "https://app.aladdinconnect.com/login.html"
OAUTH2_AUTHORIZE = "https://app.aladdinconnect.net/login.html"
OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token"
@@ -62,13 +62,12 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool:
Adds an empty filter to hass data.
Tries to get a filter from yaml, if present set to hass data.
If config is empty after getting the filter, return, otherwise emit
deprecated warning and pass the rest to the config flow.
"""
hass.data.setdefault(DOMAIN, {DATA_FILTER: {}})
hass.data.setdefault(DOMAIN, {DATA_FILTER: FILTER_SCHEMA({})})
if DOMAIN in yaml_config:
hass.data[DOMAIN][DATA_FILTER] = yaml_config[DOMAIN][CONF_FILTER]
hass.data[DOMAIN][DATA_FILTER] = yaml_config[DOMAIN].pop(CONF_FILTER)
return True
@@ -207,6 +206,6 @@ class AzureDataExplorer:
if "\n" in state.state:
return None, dropped + 1
json_event = str(json.dumps(obj=state, cls=JSONEncoder).encode("utf-8"))
json_event = json.dumps(obj=state, cls=JSONEncoder)
return (json_event, dropped)
@@ -23,7 +23,7 @@ from .const import (
CONF_APP_REG_ID,
CONF_APP_REG_SECRET,
CONF_AUTHORITY_ID,
CONF_USE_FREE,
CONF_USE_QUEUED_CLIENT,
)
_LOGGER = logging.getLogger(__name__)
@@ -35,7 +35,6 @@ class AzureDataExplorerClient:
def __init__(self, data: Mapping[str, Any]) -> None:
"""Create the right class."""
self._cluster_ingest_uri = data[CONF_ADX_CLUSTER_INGEST_URI]
self._database = data[CONF_ADX_DATABASE_NAME]
self._table = data[CONF_ADX_TABLE_NAME]
self._ingestion_properties = IngestionProperties(
@@ -45,24 +44,36 @@ class AzureDataExplorerClient:
ingestion_mapping_reference="ha_json_mapping",
)
# Create cLient for ingesting and querying data
kcsb = KustoConnectionStringBuilder.with_aad_application_key_authentication(
self._cluster_ingest_uri,
data[CONF_APP_REG_ID],
data[CONF_APP_REG_SECRET],
data[CONF_AUTHORITY_ID],
# Create client for ingesting data
kcsb_ingest = (
KustoConnectionStringBuilder.with_aad_application_key_authentication(
data[CONF_ADX_CLUSTER_INGEST_URI],
data[CONF_APP_REG_ID],
data[CONF_APP_REG_SECRET],
data[CONF_AUTHORITY_ID],
)
)
if data[CONF_USE_FREE] is True:
# Queded is the only option supported on free tear of ADX
self.write_client = QueuedIngestClient(kcsb)
else:
self.write_client = ManagedStreamingIngestClient.from_dm_kcsb(kcsb)
# Create client for querying data
kcsb_query = (
KustoConnectionStringBuilder.with_aad_application_key_authentication(
data[CONF_ADX_CLUSTER_INGEST_URI].replace("ingest-", ""),
data[CONF_APP_REG_ID],
data[CONF_APP_REG_SECRET],
data[CONF_AUTHORITY_ID],
)
)
self.query_client = KustoClient(kcsb)
if data[CONF_USE_QUEUED_CLIENT] is True:
# Queded is the only option supported on free tear of ADX
self.write_client = QueuedIngestClient(kcsb_ingest)
else:
self.write_client = ManagedStreamingIngestClient.from_dm_kcsb(kcsb_ingest)
self.query_client = KustoClient(kcsb_query)
def test_connection(self) -> None:
"""Test connection, will throw Exception when it cannot connect."""
"""Test connection, will throw Exception if it cannot connect."""
query = f"{self._table} | take 1"
@@ -10,6 +10,7 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigFlowResult
from homeassistant.helpers.selector import BooleanSelector
from . import AzureDataExplorerClient
from .const import (
@@ -19,7 +20,7 @@ from .const import (
CONF_APP_REG_ID,
CONF_APP_REG_SECRET,
CONF_AUTHORITY_ID,
CONF_USE_FREE,
CONF_USE_QUEUED_CLIENT,
DEFAULT_OPTIONS,
DOMAIN,
)
@@ -34,7 +35,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_APP_REG_ID): str,
vol.Required(CONF_APP_REG_SECRET): str,
vol.Required(CONF_AUTHORITY_ID): str,
vol.Optional(CONF_USE_FREE, default=False): bool,
vol.Required(CONF_USE_QUEUED_CLIENT, default=False): BooleanSelector(),
}
)
@@ -17,7 +17,7 @@ CONF_AUTHORITY_ID = "authority_id"
CONF_SEND_INTERVAL = "send_interval"
CONF_MAX_DELAY = "max_delay"
CONF_FILTER = DATA_FILTER = "filter"
CONF_USE_FREE = "use_queued_ingestion"
CONF_USE_QUEUED_CLIENT = "use_queued_ingestion"
DATA_HUB = "hub"
STEP_USER = "user"
@@ -3,14 +3,19 @@
"step": {
"user": {
"title": "Setup your Azure Data Explorer integration",
"description": "Enter connection details.",
"description": "Enter connection details",
"data": {
"clusteringesturi": "Cluster Ingest URI",
"database": "Database name",
"table": "Table name",
"cluster_ingest_uri": "Cluster Ingest URI",
"authority_id": "Authority ID",
"client_id": "Client ID",
"client_secret": "Client secret",
"authority_id": "Authority ID"
"database": "Database name",
"table": "Table name",
"use_queued_ingestion": "Use queued ingestion"
},
"data_description": {
"cluster_ingest_uri": "Ingest-URI of the cluster",
"use_queued_ingestion": "Must be enabled when using ADX free cluster"
}
}
},
@@ -46,6 +46,7 @@ class BlinkSyncModuleHA(
"""Representation of a Blink Alarm Control Panel."""
_attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY
_attr_code_arm_required = False
_attr_has_entity_name = True
_attr_name = None
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/buienradar",
"iot_class": "cloud_polling",
"loggers": ["buienradar", "vincenty"],
"requirements": ["buienradar==1.0.5"]
"requirements": ["buienradar==1.0.6"]
}
+21 -69
View File
@@ -4,11 +4,10 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.core import HomeAssistant, State
from homeassistant.core import HomeAssistant
from homeassistant.helpers import intent
from homeassistant.helpers.entity_component import EntityComponent
from . import DOMAIN, ClimateEntity
from . import DOMAIN
INTENT_GET_TEMPERATURE = "HassClimateGetTemperature"
@@ -23,7 +22,10 @@ class GetTemperatureIntent(intent.IntentHandler):
intent_type = INTENT_GET_TEMPERATURE
description = "Gets the current temperature of a climate device or entity"
slot_schema = {vol.Optional("area"): str, vol.Optional("name"): str}
slot_schema = {
vol.Optional("area"): intent.non_empty_string,
vol.Optional("name"): intent.non_empty_string,
}
platforms = {DOMAIN}
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
@@ -31,74 +33,24 @@ class GetTemperatureIntent(intent.IntentHandler):
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
component: EntityComponent[ClimateEntity] = hass.data[DOMAIN]
entities: list[ClimateEntity] = list(component.entities)
climate_entity: ClimateEntity | None = None
climate_state: State | None = None
name: str | None = None
if "name" in slots:
name = slots["name"]["value"]
if not entities:
raise intent.IntentHandleError("No climate entities")
area: str | None = None
if "area" in slots:
area = slots["area"]["value"]
name_slot = slots.get("name", {})
entity_name: str | None = name_slot.get("value")
entity_text: str | None = name_slot.get("text")
area_slot = slots.get("area", {})
area_id = area_slot.get("value")
if area_id:
# Filter by area and optionally name
area_name = area_slot.get("text")
for maybe_climate in intent.async_match_states(
hass, name=entity_name, area_name=area_id, domains=[DOMAIN]
):
climate_state = maybe_climate
break
if climate_state is None:
raise intent.NoStatesMatchedError(
reason=intent.MatchFailedReason.AREA,
name=entity_text or entity_name,
area=area_name or area_id,
floor=None,
domains={DOMAIN},
device_classes=None,
)
climate_entity = component.get_entity(climate_state.entity_id)
elif entity_name:
# Filter by name
for maybe_climate in intent.async_match_states(
hass, name=entity_name, domains=[DOMAIN]
):
climate_state = maybe_climate
break
if climate_state is None:
raise intent.NoStatesMatchedError(
reason=intent.MatchFailedReason.NAME,
name=entity_name,
area=None,
floor=None,
domains={DOMAIN},
device_classes=None,
)
climate_entity = component.get_entity(climate_state.entity_id)
else:
# First entity
climate_entity = entities[0]
climate_state = hass.states.get(climate_entity.entity_id)
assert climate_entity is not None
if climate_state is None:
raise intent.IntentHandleError(f"No state for {climate_entity.name}")
assert climate_state is not None
match_constraints = intent.MatchTargetsConstraints(
name=name, area_name=area, domains=[DOMAIN], assistant=intent_obj.assistant
)
match_result = intent.async_match_targets(hass, match_constraints)
if not match_result.is_match:
raise intent.MatchFailedError(
result=match_result, constraints=match_constraints
)
response = intent_obj.create_response()
response.response_type = intent.IntentResponseType.QUERY_ANSWER
response.async_set_states(matched_states=[climate_state])
response.async_set_states(matched_states=match_result.states)
return response
@@ -86,6 +86,7 @@ class Concord232Alarm(AlarmControlPanelEntity):
self._attr_name = name
self._code = code
self._alarm_control_panel_option_default_code = code
self._mode = mode
self._url = url
self._alarm = concord232_client.Client(self._url)
@@ -120,7 +120,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
director_all_items = json.loads(director_all_items)
entry_data[CONF_DIRECTOR_ALL_ITEMS] = director_all_items
entry_data[CONF_UI_CONFIGURATION] = json.loads(await director.getUiConfiguration())
# Check if OS version is 3 or higher to get UI configuration
entry_data[CONF_UI_CONFIGURATION] = None
if int(entry_data[CONF_DIRECTOR_SW_VERSION].split(".")[0]) >= 3:
entry_data[CONF_UI_CONFIGURATION] = json.loads(
await director.getUiConfiguration()
)
# Load options from config entry
entry_data[CONF_SCAN_INTERVAL] = entry.options.get(
@@ -81,11 +81,18 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up Control4 rooms from a config entry."""
entry_data = hass.data[DOMAIN][entry.entry_id]
ui_config = entry_data[CONF_UI_CONFIGURATION]
# OS 2 will not have a ui_configuration
if not ui_config:
_LOGGER.debug("No UI Configuration found for Control4")
return
all_rooms = await get_rooms(hass, entry)
if not all_rooms:
return
entry_data = hass.data[DOMAIN][entry.entry_id]
scan_interval = entry_data[CONF_SCAN_INTERVAL]
_LOGGER.debug("Scan interval = %s", scan_interval)
@@ -119,8 +126,6 @@ async def async_setup_entry(
if "parentId" in item and k > 1
}
ui_config = entry_data[CONF_UI_CONFIGURATION]
entity_list = []
for room in all_rooms:
room_id = room["id"]
@@ -429,8 +429,15 @@ class DefaultAgent(ConversationEntity):
intent_context=intent_context,
language=language,
):
if ("name" in result.entities) and (
not result.entities["name"].is_wildcard
# Prioritize results with a "name" slot, but still prefer ones with
# more literal text matched.
if (
("name" in result.entities)
and (not result.entities["name"].is_wildcard)
and (
(name_result is None)
or (result.text_chunks_matched > name_result.text_chunks_matched)
)
):
name_result = result
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.3"]
"requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.5"]
}
+3 -1
View File
@@ -43,7 +43,9 @@ class EcobeeNotificationService(BaseNotificationService):
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
"""Send a message and raise issue."""
migrate_notify_issue(self.hass, DOMAIN, "Ecobee", "2024.11.0")
migrate_notify_issue(
self.hass, DOMAIN, "Ecobee", "2024.11.0", service_name=self._service_name
)
await self.hass.async_add_executor_job(
partial(self.send_message, message, **kwargs)
)
@@ -67,6 +67,7 @@ class EgardiaAlarm(AlarmControlPanelEntity):
"""Representation of a Egardia alarm."""
_attr_state: str | None
_attr_code_arm_required = False
_attr_supported_features = (
AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/electrasmart",
"iot_class": "cloud_polling",
"requirements": ["pyElectra==1.2.0"]
"requirements": ["pyElectra==1.2.1"]
}
+9 -1
View File
@@ -59,7 +59,15 @@ class ElgatoLight(ElgatoEntity, LightEntity):
self._attr_unique_id = coordinator.data.info.serial_number
# Elgato Light supporting color, have a different temperature range
if self.coordinator.data.settings.power_on_hue is not None:
if (
self.coordinator.data.info.product_name
in (
"Elgato Light Strip",
"Elgato Light Strip Pro",
)
or self.coordinator.data.settings.power_on_hue
or self.coordinator.data.state.hue is not None
):
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS}
self._attr_min_mireds = 153
self._attr_max_mireds = 285
@@ -141,10 +141,10 @@ class Enigma2Device(MediaPlayerEntity):
self._device: OpenWebIfDevice = device
self._entry = entry
self._attr_unique_id = device.mac_address
self._attr_unique_id = device.mac_address or entry.entry_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.mac_address)},
identifiers={(DOMAIN, self._attr_unique_id)},
manufacturer=about["info"]["brand"],
model=about["info"]["model"],
configuration_url=device.base,
@@ -116,8 +116,9 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
):
"""Initialize the alarm panel."""
self._partition_number = partition_number
self._code = code
self._panic_type = panic_type
self._alarm_control_panel_option_default_code = code
self._attr_code_format = CodeFormat.NUMBER
_LOGGER.debug("Setting up alarm: %s", alarm_name)
super().__init__(alarm_name, info, controller)
@@ -141,13 +142,6 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
if partition is None or int(partition) == self._partition_number:
self.async_write_ha_state()
@property
def code_format(self) -> CodeFormat | None:
"""Regex for code format or None if no code is required."""
if self._code:
return None
return CodeFormat.NUMBER
@property
def state(self) -> str:
"""Return the state of the device."""
@@ -169,34 +163,15 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
if code:
self.hass.data[DATA_EVL].disarm_partition(str(code), self._partition_number)
else:
self.hass.data[DATA_EVL].disarm_partition(
str(self._code), self._partition_number
)
self.hass.data[DATA_EVL].disarm_partition(code, self._partition_number)
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
if code:
self.hass.data[DATA_EVL].arm_stay_partition(
str(code), self._partition_number
)
else:
self.hass.data[DATA_EVL].arm_stay_partition(
str(self._code), self._partition_number
)
self.hass.data[DATA_EVL].arm_stay_partition(code, self._partition_number)
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
if code:
self.hass.data[DATA_EVL].arm_away_partition(
str(code), self._partition_number
)
else:
self.hass.data[DATA_EVL].arm_away_partition(
str(self._code), self._partition_number
)
self.hass.data[DATA_EVL].arm_away_partition(code, self._partition_number)
async def async_alarm_trigger(self, code: str | None = None) -> None:
"""Alarm trigger command. Will be used to trigger a panic alarm."""
@@ -204,9 +179,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command."""
self.hass.data[DATA_EVL].arm_night_partition(
str(code) if code else str(self._code), self._partition_number
)
self.hass.data[DATA_EVL].arm_night_partition(code, self._partition_number)
@callback
def async_alarm_keypress(self, keypress=None):
+3 -1
View File
@@ -69,7 +69,9 @@ class FileNotificationService(BaseNotificationService):
"""Send a message to a file."""
# The use of the legacy notify service was deprecated with HA Core 2024.6.0
# and will be removed with HA Core 2024.12
migrate_notify_issue(self.hass, DOMAIN, "File", "2024.12.0")
migrate_notify_issue(
self.hass, DOMAIN, "File", "2024.12.0", service_name=self._service_name
)
await self.hass.async_add_executor_job(
partial(self.send_message, message, **kwargs)
)
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20240604.0"]
"requirements": ["home-assistant-frontend==20240610.1"]
}
@@ -13,5 +13,6 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth",
"iot_class": "local_polling",
"requirements": ["gardena-bluetooth==1.4.1"]
"loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"],
"requirements": ["gardena-bluetooth==1.4.2"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/glances",
"iot_class": "local_polling",
"loggers": ["glances_api"],
"requirements": ["glances-api==0.7.0"]
"requirements": ["glances-api==0.8.0"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/goodwe",
"iot_class": "local_polling",
"loggers": ["goodwe"],
"requirements": ["goodwe==0.3.5"]
"requirements": ["goodwe==0.3.6"]
}
@@ -1586,6 +1586,17 @@ class ArmDisArmTrait(_Trait):
if features & required_feature != 0
]
def _default_arm_state(self):
states = self._supported_states()
if STATE_ALARM_TRIGGERED in states:
states.remove(STATE_ALARM_TRIGGERED)
if len(states) != 1:
raise SmartHomeError(ERR_NOT_SUPPORTED, "ArmLevel missing")
return states[0]
def sync_attributes(self):
"""Return ArmDisarm attributes for a sync request."""
response = {}
@@ -1609,10 +1620,13 @@ class ArmDisArmTrait(_Trait):
def query_attributes(self):
"""Return ArmDisarm query attributes."""
armed_state = self.state.attributes.get("next_state", self.state.state)
response = {"isArmed": armed_state in self.state_to_service}
if response["isArmed"]:
response.update({"currentArmLevel": armed_state})
return response
if armed_state in self.state_to_service:
return {"isArmed": True, "currentArmLevel": armed_state}
return {
"isArmed": False,
"currentArmLevel": self._default_arm_state(),
}
async def execute(self, command, data, params, challenge):
"""Execute an ArmDisarm command."""
@@ -1620,15 +1634,7 @@ class ArmDisArmTrait(_Trait):
# If no arm level given, we can only arm it if there is
# only one supported arm type. We never default to triggered.
if not (arm_level := params.get("armLevel")):
states = self._supported_states()
if STATE_ALARM_TRIGGERED in states:
states.remove(STATE_ALARM_TRIGGERED)
if len(states) != 1:
raise SmartHomeError(ERR_NOT_SUPPORTED, "ArmLevel missing")
arm_level = states[0]
arm_level = self._default_arm_state()
if self.state.state == arm_level:
raise SmartHomeError(ERR_ALREADY_ARMED, "System is already armed")
@@ -165,7 +165,7 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent):
await session.async_ensure_token_valid()
self.assistant = None
if not self.assistant or user_input.language != self.language:
credentials = Credentials(session.token[CONF_ACCESS_TOKEN])
credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]
self.language = user_input.language
self.assistant = TextAssistant(credentials, self.language)
@@ -72,7 +72,7 @@ async def async_send_text_commands(
entry.async_start_reauth(hass)
raise
credentials = Credentials(session.token[CONF_ACCESS_TOKEN])
credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]
language_code = entry.options.get(CONF_LANGUAGE_CODE, default_language_code(hass))
with TextAssistant(
credentials, language_code, audio_out=bool(media_players)
@@ -71,7 +71,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
try:
response = await model.generate_content_async(prompt_parts)
except (
ClientError,
GoogleAPICallError,
ValueError,
genai_types.BlockedPromptException,
genai_types.StopCandidateException,
@@ -2,11 +2,12 @@
from __future__ import annotations
import codecs
from typing import Any, Literal
import google.ai.generativelanguage as glm
from google.api_core.exceptions import GoogleAPICallError
import google.generativeai as genai
from google.generativeai import protos
import google.generativeai.types as genai_types
from google.protobuf.json_format import MessageToDict
import voluptuous as vol
@@ -93,7 +94,7 @@ def _format_tool(tool: llm.Tool) -> dict[str, Any]:
parameters = _format_schema(convert(tool.parameters))
return glm.Tool(
return protos.Tool(
{
"function_declarations": [
{
@@ -106,14 +107,14 @@ def _format_tool(tool: llm.Tool) -> dict[str, Any]:
)
def _adjust_value(value: Any) -> Any:
"""Reverse unnecessary single quotes escaping."""
def _escape_decode(value: Any) -> Any:
"""Recursively call codecs.escape_decode on all values."""
if isinstance(value, str):
return value.replace("\\'", "'")
return codecs.escape_decode(bytes(value, "utf-8"))[0].decode("utf-8") # type: ignore[attr-defined]
if isinstance(value, list):
return [_adjust_value(item) for item in value]
return [_escape_decode(item) for item in value]
if isinstance(value, dict):
return {k: _adjust_value(v) for k, v in value.items()}
return {k: _escape_decode(v) for k, v in value.items()}
return value
@@ -334,10 +335,7 @@ class GoogleGenerativeAIConversationEntity(
for function_call in function_calls:
tool_call = MessageToDict(function_call._pb) # noqa: SLF001
tool_name = tool_call["name"]
tool_args = {
key: _adjust_value(value)
for key, value in tool_call["args"].items()
}
tool_args = _escape_decode(tool_call["args"])
LOGGER.debug("Tool call: %s(%s)", tool_name, tool_args)
tool_input = llm.ToolInput(tool_name=tool_name, tool_args=tool_args)
try:
@@ -349,13 +347,13 @@ class GoogleGenerativeAIConversationEntity(
LOGGER.debug("Tool response: %s", function_response)
tool_responses.append(
glm.Part(
function_response=glm.FunctionResponse(
protos.Part(
function_response=protos.FunctionResponse(
name=tool_name, response=function_response
)
)
)
chat_request = glm.Content(parts=tool_responses)
chat_request = protos.Content(parts=tool_responses)
intent_response.async_set_speech(
" ".join([part.text.strip() for part in chat_response.parts if part.text])
@@ -9,5 +9,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["google-generativeai==0.5.4", "voluptuous-openapi==0.0.4"]
"requirements": ["google-generativeai==0.6.0", "voluptuous-openapi==0.0.4"]
}
@@ -93,7 +93,9 @@ async def async_setup_service(hass: HomeAssistant) -> None:
def _append_to_sheet(call: ServiceCall, entry: ConfigEntry) -> None:
"""Run append in the executor."""
service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN]))
service = Client(
Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]
)
try:
sheet = service.open_by_key(entry.unique_id)
except RefreshError:
@@ -61,7 +61,9 @@ class OAuth2FlowHandler(
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
"""Create an entry for the flow, or update existing entry."""
service = Client(Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN]))
service = Client(
Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]
)
if self.reauth_entry:
_LOGGER.debug("service.open_by_key")
@@ -4,7 +4,10 @@
"after_dependencies": [
"alarm_control_panel",
"climate",
"cover",
"device_tracker",
"lock",
"media_player",
"person",
"plant",
"vacuum",
+30 -2
View File
@@ -36,7 +36,14 @@ from homeassistant.const import (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.core import (
CALLBACK_TYPE,
Event,
EventStateChangedData,
HomeAssistant,
State,
callback,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.entity import (
@@ -45,6 +52,7 @@ from homeassistant.helpers.entity import (
get_unit_of_measurement,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
@@ -329,6 +337,7 @@ class SensorGroup(GroupEntity, SensorEntity):
self._native_unit_of_measurement = unit_of_measurement
self._valid_units: set[str | None] = set()
self._can_convert: bool = False
self.calculate_attributes_later: CALLBACK_TYPE | None = None
self._attr_name = name
if name == DEFAULT_NAME:
self._attr_name = f"{DEFAULT_NAME} {sensor_type}".capitalize()
@@ -345,13 +354,32 @@ class SensorGroup(GroupEntity, SensorEntity):
async def async_added_to_hass(self) -> None:
"""When added to hass."""
for entity_id in self._entity_ids:
if self.hass.states.get(entity_id) is None:
self.calculate_attributes_later = async_track_state_change_event(
self.hass, self._entity_ids, self.calculate_state_attributes
)
break
if not self.calculate_attributes_later:
await self.calculate_state_attributes()
await super().async_added_to_hass()
async def calculate_state_attributes(
self, event: Event[EventStateChangedData] | None = None
) -> None:
"""Calculate state attributes."""
for entity_id in self._entity_ids:
if self.hass.states.get(entity_id) is None:
return
if self.calculate_attributes_later:
self.calculate_attributes_later()
self.calculate_attributes_later = None
self._attr_state_class = self._calculate_state_class(self._state_class)
self._attr_device_class = self._calculate_device_class(self._device_class)
self._attr_native_unit_of_measurement = self._calculate_unit_of_measurement(
self._native_unit_of_measurement
)
self._valid_units = self._get_valid_units()
await super().async_added_to_hass()
@callback
def async_update_group_state(self) -> None:
+3 -4
View File
@@ -267,15 +267,14 @@ class SupervisorIssues:
placeholders = {PLACEHOLDER_KEY_REFERENCE: issue.reference}
if issue.key == ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING:
placeholders[PLACEHOLDER_KEY_ADDON_URL] = (
f"/hassio/addon/{issue.reference}"
)
addons = get_addons_info(self._hass)
if addons and issue.reference in addons:
placeholders[PLACEHOLDER_KEY_ADDON] = addons[issue.reference][
"name"
]
if "url" in addons[issue.reference]:
placeholders[PLACEHOLDER_KEY_ADDON_URL] = addons[
issue.reference
]["url"]
else:
placeholders[PLACEHOLDER_KEY_ADDON] = issue.reference
@@ -51,6 +51,7 @@ class HiveAlarmControlPanelEntity(HiveEntity, AlarmControlPanelEntity):
| AlarmControlPanelEntityFeature.ARM_AWAY
| AlarmControlPanelEntityFeature.TRIGGER
)
_attr_code_arm_required = False
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.49", "babel==2.13.1"]
"requirements": ["holidays==0.50", "babel==2.13.1"]
}
@@ -1,7 +1,6 @@
{
"domain": "http",
"name": "HTTP",
"after_dependencies": ["isal"],
"codeowners": ["@home-assistant/core"],
"documentation": "https://www.home-assistant.io/integrations/http",
"integration_type": "system",
+23 -22
View File
@@ -35,7 +35,7 @@ class HumidityHandler(intent.IntentHandler):
intent_type = INTENT_HUMIDITY
description = "Set desired humidity level"
slot_schema = {
vol.Required("name"): cv.string,
vol.Required("name"): intent.non_empty_string,
vol.Required("humidity"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
}
platforms = {DOMAIN}
@@ -44,18 +44,19 @@ class HumidityHandler(intent.IntentHandler):
"""Handle the hass intent."""
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
states = list(
intent.async_match_states(
hass,
name=slots["name"]["value"],
states=hass.states.async_all(DOMAIN),
)
match_constraints = intent.MatchTargetsConstraints(
name=slots["name"]["value"],
domains=[DOMAIN],
assistant=intent_obj.assistant,
)
match_result = intent.async_match_targets(hass, match_constraints)
if not match_result.is_match:
raise intent.MatchFailedError(
result=match_result, constraints=match_constraints
)
if not states:
raise intent.IntentHandleError("No entities matched")
state = states[0]
state = match_result.states[0]
service_data = {ATTR_ENTITY_ID: state.entity_id}
humidity = slots["humidity"]["value"]
@@ -89,7 +90,7 @@ class SetModeHandler(intent.IntentHandler):
intent_type = INTENT_MODE
description = "Set humidifier mode"
slot_schema = {
vol.Required("name"): cv.string,
vol.Required("name"): intent.non_empty_string,
vol.Required("mode"): cv.string,
}
platforms = {DOMAIN}
@@ -98,18 +99,18 @@ class SetModeHandler(intent.IntentHandler):
"""Handle the hass intent."""
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
states = list(
intent.async_match_states(
hass,
name=slots["name"]["value"],
states=hass.states.async_all(DOMAIN),
)
match_constraints = intent.MatchTargetsConstraints(
name=slots["name"]["value"],
domains=[DOMAIN],
assistant=intent_obj.assistant,
)
match_result = intent.async_match_targets(hass, match_constraints)
if not match_result.is_match:
raise intent.MatchFailedError(
result=match_result, constraints=match_constraints
)
if not states:
raise intent.IntentHandleError("No entities matched")
state = states[0]
state = match_result.states[0]
service_data = {ATTR_ENTITY_ID: state.entity_id}
intent.async_test_feature(state, HumidifierEntityFeature.MODES, "modes")
@@ -24,13 +24,17 @@ class HydrawiseBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes Hydrawise binary sensor."""
value_fn: Callable[[HydrawiseBinarySensor], bool | None]
always_available: bool = False
CONTROLLER_BINARY_SENSORS: tuple[HydrawiseBinarySensorEntityDescription, ...] = (
HydrawiseBinarySensorEntityDescription(
key="status",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
value_fn=lambda status_sensor: status_sensor.coordinator.last_update_success,
value_fn=lambda status_sensor: status_sensor.coordinator.last_update_success
and status_sensor.controller.online,
# Connectivtiy sensor is always available
always_available=True,
),
)
@@ -98,3 +102,10 @@ class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity):
def _update_attrs(self) -> None:
"""Update state attributes."""
self._attr_is_on = self.entity_description.value_fn(self)
@property
def available(self) -> bool:
"""Set the entity availability."""
if self.entity_description.always_available:
return True
return super().available
@@ -70,3 +70,8 @@ class HydrawiseEntity(CoordinatorEntity[HydrawiseDataUpdateCoordinator]):
self.controller = self.coordinator.data.controllers[self.controller.id]
self._update_attrs()
super()._handle_coordinator_update()
@property
def available(self) -> bool:
"""Set the entity availability."""
return super().available and self.controller.online
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
"iot_class": "cloud_polling",
"loggers": ["pydrawise"],
"requirements": ["pydrawise==2024.6.2"]
"requirements": ["pydrawise==2024.6.3"]
}
@@ -37,6 +37,7 @@ class IAlarmPanel(
AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY
)
_attr_code_arm_required = False
def __init__(self, coordinator: IAlarmDataUpdateCoordinator) -> None:
"""Create the entity with a DataUpdateCoordinator."""
@@ -64,7 +64,7 @@ class IdasenDeskConfigFlow(ConfigFlow, domain=DOMAIN):
desk = Desk(None, monitor_height=False)
try:
await desk.connect(discovery_info.device, auto_reconnect=False)
await desk.connect(discovery_info.device, retry=False)
except AuthFailedError:
errors["base"] = "auth_failed"
except TimeoutError:
+3 -3
View File
@@ -195,13 +195,13 @@ class ImapMessage:
):
message_untyped_text = str(part.get_payload())
if message_text is not None:
if message_text is not None and message_text.strip():
return message_text
if message_html is not None:
if message_html:
return message_html
if message_untyped_text is not None:
if message_untyped_text:
return message_untyped_text
return str(self.email_message.get_payload())
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["imgw_pib==1.0.1"]
"requirements": ["imgw_pib==1.0.5"]
}
+1 -4
View File
@@ -283,16 +283,13 @@ class KNXClimate(KnxEntity, ClimateEntity):
)
if knx_controller_mode in self._device.mode.controller_modes:
await self._device.mode.set_controller_mode(knx_controller_mode)
self.async_write_ha_state()
return
if self._device.supports_on_off:
if hvac_mode == HVACMode.OFF:
await self._device.turn_off()
elif not self._device.is_on:
# for default hvac mode, otherwise above would have triggered
await self._device.turn_on()
self.async_write_ha_state()
self.async_write_ha_state()
@property
def preset_mode(self) -> str | None:
+3 -1
View File
@@ -60,7 +60,9 @@ class KNXNotificationService(BaseNotificationService):
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
"""Send a notification to knx bus."""
migrate_notify_issue(self.hass, DOMAIN, "KNX", "2024.11.0")
migrate_notify_issue(
self.hass, DOMAIN, "KNX", "2024.11.0", service_name=self._service_name
)
if "target" in kwargs:
await self._async_send_to_device(message, kwargs["target"])
else:
@@ -48,6 +48,7 @@ class LupusecAlarm(LupusecDevice, AlarmControlPanelEntity):
AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY
)
_attr_code_arm_required = False
def __init__(
self, data: lupupy.Lupusec, device: lupupy.devices.LupusecAlarm, entry_id: str
+2 -2
View File
@@ -57,7 +57,7 @@ class AsyncConfigEntryAuth(AbstractAuth):
# even when it is expired to fully hand off this responsibility and
# know it is working at startup (then if not, fail loudly).
token = self._oauth_session.token
creds = Credentials(
creds = Credentials( # type: ignore[no-untyped-call]
token=token["access_token"],
refresh_token=token["refresh_token"],
token_uri=OAUTH2_TOKEN,
@@ -92,7 +92,7 @@ class AccessTokenAuthImpl(AbstractAuth):
async def async_get_creds(self) -> Credentials:
"""Return an OAuth credential for Pub/Sub Subscriber."""
return Credentials(
return Credentials( # type: ignore[no-untyped-call]
token=self._access_token,
token_uri=OAUTH2_TOKEN,
scopes=SDM_SCOPES,
+1 -1
View File
@@ -20,5 +20,5 @@
"iot_class": "cloud_push",
"loggers": ["google_nest_sdm"],
"quality_scale": "platinum",
"requirements": ["google-nest-sdm==4.0.4"]
"requirements": ["google-nest-sdm==4.0.5"]
}
+2 -2
View File
@@ -388,12 +388,12 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
async def async_turn_off(self) -> None:
"""Turn off the zone."""
await self.async_set_hvac_mode(OPERATION_MODE_OFF)
await self.async_set_hvac_mode(HVACMode.OFF)
self._signal_zone_update()
async def async_turn_on(self) -> None:
"""Turn on the zone."""
await self.async_set_hvac_mode(OPERATION_MODE_AUTO)
await self.async_set_hvac_mode(HVACMode.AUTO)
self._signal_zone_update()
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
+23 -1
View File
@@ -12,9 +12,31 @@ from .const import DOMAIN
@callback
def migrate_notify_issue(
hass: HomeAssistant, domain: str, integration_title: str, breaks_in_ha_version: str
hass: HomeAssistant,
domain: str,
integration_title: str,
breaks_in_ha_version: str,
service_name: str | None = None,
) -> None:
"""Ensure an issue is registered."""
if service_name is not None:
ir.async_create_issue(
hass,
DOMAIN,
f"migrate_notify_{domain}_{service_name}",
breaks_in_ha_version=breaks_in_ha_version,
issue_domain=domain,
is_fixable=True,
is_persistent=True,
translation_key="migrate_notify_service",
translation_placeholders={
"domain": domain,
"integration_title": integration_title,
"service_name": service_name,
},
severity=ir.IssueSeverity.WARNING,
)
return
ir.async_create_issue(
hass,
DOMAIN,
@@ -72,6 +72,17 @@
}
}
}
},
"migrate_notify_service": {
"title": "Legacy service `notify.{service_name}` stll being used",
"fix_flow": {
"step": {
"confirm": {
"description": "The {integration_title} `notify.{service_name}` service is migrated, but it seems the old `notify` service is still being used.\n\nA new `notify` entity is available now to replace each legacy `notify` service.\n\nUpdate any automations or scripts to use the new `notify.send_message` service exposed with this new entity. When this is done, select Submit and restart Home Assistant.",
"title": "Migrate legacy {integration_title} notify service for domain `{domain}`"
}
}
}
}
}
}
@@ -100,6 +100,7 @@ class NX584Alarm(AlarmControlPanelEntity):
AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY
)
_attr_code_arm_required = False
def __init__(self, name: str, alarm_client: client.Client, url: str) -> None:
"""Init the nx584 alarm panel."""
@@ -73,7 +73,7 @@ def async_create_issue(hass: HomeAssistant, entry_id: str) -> None:
domain=DOMAIN,
issue_id=_get_issue_id(entry_id),
is_fixable=True,
is_persistent=True,
is_persistent=False,
severity=ir.IssueSeverity.WARNING,
learn_more_url="https://www.home-assistant.io/integrations/openweathermap/",
translation_key="deprecated_v25",
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/opower",
"iot_class": "cloud_polling",
"loggers": ["opower"],
"requirements": ["opower==0.4.6"]
"requirements": ["opower==0.4.7"]
}
@@ -240,6 +240,7 @@ class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity
"""Representation of an Overkiz Alarm Control Panel."""
entity_description: OverkizAlarmDescription
_attr_code_arm_required = False
def __init__(
self,
+18 -4
View File
@@ -59,9 +59,17 @@ class PingDataICMPLib(PingData):
privileged=self._privileged,
)
except NameLookupError:
_LOGGER.debug("Error resolving host: %s", self.ip_address)
self.is_alive = False
return
_LOGGER.debug(
"async_ping returned: reachable=%s sent=%i received=%s",
data.is_alive,
data.packets_sent,
data.packets_received,
)
self.is_alive = data.is_alive
if not self.is_alive:
self.data = None
@@ -94,6 +102,10 @@ class PingDataSubProcess(PingData):
async def async_ping(self) -> dict[str, Any] | None:
"""Send ICMP echo request and return details if success."""
_LOGGER.debug(
"Pinging %s with: `%s`", self.ip_address, " ".join(self._ping_cmd)
)
pinger = await asyncio.create_subprocess_exec(
*self._ping_cmd,
stdin=None,
@@ -141,18 +153,20 @@ class PingDataSubProcess(PingData):
assert match is not None
rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups()
except TimeoutError:
_LOGGER.exception(
"Timed out running command: `%s`, after: %ss",
self._ping_cmd,
_LOGGER.debug(
"Timed out running command: `%s`, after: %s",
" ".join(self._ping_cmd),
self._count + PING_TIMEOUT,
)
if pinger:
with suppress(TypeError):
await pinger.kill() # type: ignore[func-returns-value]
del pinger
return None
except AttributeError:
except AttributeError as err:
_LOGGER.debug("Error matching ping output: %s", err)
return None
return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max, "mdev": rtt_mdev}
@@ -55,6 +55,7 @@ class MinutPointAlarmControl(AlarmControlPanelEntity):
"""The platform class required by Home Assistant."""
_attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY
_attr_code_arm_required = False
def __init__(self, point_client: MinutPointClient, home_id: str) -> None:
"""Initialize the entity."""
@@ -5,6 +5,7 @@ from __future__ import annotations
import mimetypes
from radios import FilterBy, Order, RadioBrowser, Station
from radios.radio_browser import pycountry
from homeassistant.components.media_player import MediaClass, MediaType
from homeassistant.components.media_source.error import Unresolvable
@@ -145,6 +146,8 @@ class RadioMediaSource(MediaSource):
# We show country in the root additionally, when there is no item
if not item.identifier or category == "country":
# Trigger the lazy loading of the country database to happen inside the executor
await self.hass.async_add_executor_job(lambda: len(pycountry.countries))
countries = await radios.countries(order=Order.NAME)
return [
BrowseMediaSource(
@@ -1245,7 +1245,7 @@ def _first_statistic(
table: type[StatisticsBase],
metadata_id: int,
) -> datetime | None:
"""Return the data of the oldest statistic row for a given metadata id."""
"""Return the date of the oldest statistic row for a given metadata id."""
stmt = lambda_stmt(
lambda: select(table.start_ts)
.filter(table.metadata_id == metadata_id)
@@ -1257,12 +1257,30 @@ def _first_statistic(
return None
def _last_statistic(
session: Session,
table: type[StatisticsBase],
metadata_id: int,
) -> datetime | None:
"""Return the date of the newest statistic row for a given metadata id."""
stmt = lambda_stmt(
lambda: select(table.start_ts)
.filter(table.metadata_id == metadata_id)
.order_by(table.start_ts.desc())
.limit(1)
)
if stats := cast(Sequence[Row], execute_stmt_lambda_element(session, stmt)):
return dt_util.utc_from_timestamp(stats[0].start_ts)
return None
def _get_oldest_sum_statistic(
session: Session,
head_start_time: datetime | None,
main_start_time: datetime | None,
tail_start_time: datetime | None,
oldest_stat: datetime | None,
oldest_5_min_stat: datetime | None,
tail_only: bool,
metadata_id: int,
) -> float | None:
@@ -1307,6 +1325,15 @@ def _get_oldest_sum_statistic(
if (
head_start_time is not None
and oldest_5_min_stat is not None
and (
# If we want stats older than the short term purge window, don't lookup
# the oldest sum in the short term table, as it would be prioritized
# over older LongTermStats.
(oldest_stat is None)
or (oldest_5_min_stat < oldest_stat)
or (oldest_5_min_stat <= head_start_time)
)
and (
oldest_sum := _get_oldest_sum_statistic_in_sub_period(
session, head_start_time, StatisticsShortTerm, metadata_id
@@ -1477,13 +1504,16 @@ def statistic_during_period(
tail_start_time: datetime | None = None
tail_end_time: datetime | None = None
if end_time is None:
tail_start_time = now.replace(minute=0, second=0, microsecond=0)
tail_start_time = _last_statistic(session, Statistics, metadata_id)
if tail_start_time:
tail_start_time += Statistics.duration
else:
tail_start_time = now.replace(minute=0, second=0, microsecond=0)
elif tail_only:
tail_start_time = start_time
tail_end_time = end_time
elif end_time.minute:
tail_start_time = (
start_time
if tail_only
else end_time.replace(minute=0, second=0, microsecond=0)
)
tail_start_time = end_time.replace(minute=0, second=0, microsecond=0)
tail_end_time = end_time
# Calculate the main period
@@ -1518,6 +1548,7 @@ def statistic_during_period(
main_start_time,
tail_start_time,
oldest_stat,
oldest_5_min_stat,
tail_only,
metadata_id,
)
@@ -137,7 +137,7 @@ def _register_new_account(
configurator.request_done(hass, request_id)
request_id = configurator.async_request_config(
request_id = configurator.request_config(
hass,
f"{DOMAIN} - {account_name}",
callback=register_account_callback,
+3 -1
View File
@@ -116,7 +116,6 @@ async def async_setup_entry(
class ReolinkCamera(ReolinkChannelCoordinatorEntity, Camera):
"""An implementation of a Reolink IP camera."""
_attr_supported_features: CameraEntityFeature = CameraEntityFeature.STREAM
entity_description: ReolinkCameraEntityDescription
def __init__(
@@ -130,6 +129,9 @@ class ReolinkCamera(ReolinkChannelCoordinatorEntity, Camera):
ReolinkChannelCoordinatorEntity.__init__(self, reolink_data, channel)
Camera.__init__(self)
if "snapshots" not in entity_description.stream:
self._attr_supported_features = CameraEntityFeature.STREAM
if self._host.api.model in DUAL_LENS_MODELS:
self._attr_translation_key = (
f"{entity_description.translation_key}_lens_{self._channel}"
@@ -25,7 +25,7 @@ from homeassistant.const import (
)
from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_validation as cv, selector
from homeassistant.helpers.device_registry import format_mac
from .const import CONF_USE_HTTPS, DOMAIN
@@ -60,7 +60,24 @@ class ReolinkOptionsFlowHandler(OptionsFlow):
vol.Required(
CONF_PROTOCOL,
default=self.config_entry.options[CONF_PROTOCOL],
): vol.In(["rtsp", "rtmp", "flv"]),
): selector.SelectSelector(
selector.SelectSelectorConfig(
options=[
selector.SelectOptionDict(
value="rtsp",
label="RTSP",
),
selector.SelectOptionDict(
value="rtmp",
label="RTMP",
),
selector.SelectOptionDict(
value="flv",
label="FLV",
),
],
),
),
}
),
)
@@ -101,6 +101,11 @@ class ReolinkHostCoordinatorEntity(ReolinkBaseCoordinatorEntity[None]):
await super().async_will_remove_from_hass()
async def async_update(self) -> None:
"""Force full update from the generic entity update service."""
self._host.last_wake = 0
await super().async_update()
class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity):
"""Parent class for Reolink hardware camera entities connected to a channel of the NVR."""
+13 -1
View File
@@ -6,6 +6,7 @@ import asyncio
from collections import defaultdict
from collections.abc import Mapping
import logging
from time import time
from typing import Any, Literal
import aiohttp
@@ -40,6 +41,10 @@ POLL_INTERVAL_NO_PUSH = 5
LONG_POLL_COOLDOWN = 0.75
LONG_POLL_ERROR_COOLDOWN = 30
# Conserve battery by not waking the battery cameras each minute during normal update
# Most props are cached in the Home Hub and updated, but some are skipped
BATTERY_WAKE_UPDATE_INTERVAL = 3600 # seconds
_LOGGER = logging.getLogger(__name__)
@@ -68,6 +73,7 @@ class ReolinkHost:
timeout=DEFAULT_TIMEOUT,
)
self.last_wake: float = 0
self._update_cmd: defaultdict[str, defaultdict[int | None, int]] = defaultdict(
lambda: defaultdict(int)
)
@@ -337,7 +343,13 @@ class ReolinkHost:
async def update_states(self) -> None:
"""Call the API of the camera device to update the internal states."""
await self._api.get_states(cmd_list=self._update_cmd)
wake = False
if time() - self.last_wake > BATTERY_WAKE_UPDATE_INTERVAL:
# wake the battery cameras for a complete update
wake = True
self.last_wake = time()
await self._api.get_states(cmd_list=self._update_cmd, wake=wake)
async def disconnect(self) -> None:
"""Disconnect from the API, so the connection will be released."""
@@ -7,7 +7,7 @@
"iot_class": "local_polling",
"loggers": ["roborock"],
"requirements": [
"python-roborock==2.2.2",
"python-roborock==2.3.0",
"vacuum-map-parser-roborock==0.1.2"
]
}
+1 -10
View File
@@ -297,16 +297,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
if version == 2:
if minor_version < 2:
# Cleanup invalid MAC addresses - see #103512
dev_reg = dr.async_get(hass)
for device in dr.async_entries_for_config_entry(
dev_reg, config_entry.entry_id
):
new_connections = device.connections.copy()
new_connections.discard((dr.CONNECTION_NETWORK_MAC, "none"))
if new_connections != device.connections:
dev_reg.async_update_device(
device.id, new_connections=new_connections
)
# Reverted due to device registry collisions - see #119082 / #119249
minor_version = 2
hass.config_entries.async_update_entry(config_entry, minor_version=2)
+1 -1
View File
@@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except RequestError as err:
raise ConfigEntryNotReady from err
coordinator = SENZDataUpdateCoordinator(
coordinator: SENZDataUpdateCoordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=account.username,
+1 -8
View File
@@ -2,7 +2,6 @@
from __future__ import annotations
import contextlib
from typing import Final
from aioshelly.block_device import BlockDevice
@@ -301,13 +300,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> b
entry, platforms
):
if shelly_entry_data.rpc:
with contextlib.suppress(DeviceConnectionError):
# If the device is restarting or has gone offline before
# the ping/pong timeout happens, the shutdown command
# will fail, but we don't care since we are unloading
# and if we setup again, we will fix anything that is
# in an inconsistent state at that time.
await shelly_entry_data.rpc.shutdown()
await shelly_entry_data.rpc.shutdown()
return unload_ok
+22 -6
View File
@@ -584,11 +584,13 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
raise UpdateFailed(
f"Sleeping device did not update within {self.sleep_period} seconds interval"
)
if self.device.connected:
return
if not await self._async_device_connect_task():
raise UpdateFailed("Device reconnect error")
async with self._connection_lock:
if self.device.connected: # Already connected
return
if not await self._async_device_connect_task():
raise UpdateFailed("Device reconnect error")
async def _async_disconnected(self, reconnect: bool) -> None:
"""Handle device disconnected."""
@@ -623,7 +625,13 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
if self.connected: # Already connected
return
self.connected = True
await self._async_run_connected_events()
try:
await self._async_run_connected_events()
except DeviceConnectionError as err:
LOGGER.error(
"Error running connected events for device %s: %s", self.name, err
)
self.last_update_success = False
async def _async_run_connected_events(self) -> None:
"""Run connected events.
@@ -697,10 +705,18 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
if self.device.connected:
try:
await async_stop_scanner(self.device)
await super().shutdown()
except InvalidAuthError:
self.entry.async_start_reauth(self.hass)
return
await super().shutdown()
except DeviceConnectionError as err:
# If the device is restarting or has gone offline before
# the ping/pong timeout happens, the shutdown command
# will fail, but we don't care since we are unloading
# and if we setup again, we will fix anything that is
# in an inconsistent state at that time.
LOGGER.debug("Error during shutdown for device %s: %s", self.name, err)
return
await self._async_disconnected(False)
@@ -9,7 +9,7 @@
"iot_class": "local_push",
"loggers": ["aioshelly"],
"quality_scale": "platinum",
"requirements": ["aioshelly==10.0.0"],
"requirements": ["aioshelly==10.0.1"],
"zeroconf": [
{
"type": "_http._tcp.local.",
+43 -33
View File
@@ -8,6 +8,8 @@ from typing import Any
import pysnmp.hlapi.asyncio as hlapi
from pysnmp.hlapi.asyncio import (
CommunityData,
ObjectIdentity,
ObjectType,
UdpTransportTarget,
UsmUserData,
getCmd,
@@ -63,7 +65,12 @@ from .const import (
MAP_PRIV_PROTOCOLS,
SNMP_VERSIONS,
)
from .util import RequestArgsType, async_create_request_cmd_args
from .util import (
CommandArgsType,
RequestArgsType,
async_create_command_cmd_args,
async_create_request_cmd_args,
)
_LOGGER = logging.getLogger(__name__)
@@ -125,23 +132,23 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the SNMP switch."""
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
name: str = config[CONF_NAME]
host: str = config[CONF_HOST]
port: int = config[CONF_PORT]
community = config.get(CONF_COMMUNITY)
baseoid: str = config[CONF_BASEOID]
command_oid = config.get(CONF_COMMAND_OID)
command_payload_on = config.get(CONF_COMMAND_PAYLOAD_ON)
command_payload_off = config.get(CONF_COMMAND_PAYLOAD_OFF)
command_oid: str | None = config.get(CONF_COMMAND_OID)
command_payload_on: str | None = config.get(CONF_COMMAND_PAYLOAD_ON)
command_payload_off: str | None = config.get(CONF_COMMAND_PAYLOAD_OFF)
version: str = config[CONF_VERSION]
username = config.get(CONF_USERNAME)
authkey = config.get(CONF_AUTH_KEY)
authproto: str = config[CONF_AUTH_PROTOCOL]
privkey = config.get(CONF_PRIV_KEY)
privproto: str = config[CONF_PRIV_PROTOCOL]
payload_on = config.get(CONF_PAYLOAD_ON)
payload_off = config.get(CONF_PAYLOAD_OFF)
vartype = config.get(CONF_VARTYPE)
payload_on: str = config[CONF_PAYLOAD_ON]
payload_off: str = config[CONF_PAYLOAD_OFF]
vartype: str = config[CONF_VARTYPE]
if version == "3":
if not authkey:
@@ -159,9 +166,11 @@ async def async_setup_platform(
else:
auth_data = CommunityData(community, mpModel=SNMP_VERSIONS[version])
transport = UdpTransportTarget((host, port))
request_args = await async_create_request_cmd_args(
hass, auth_data, UdpTransportTarget((host, port)), baseoid
hass, auth_data, transport, baseoid
)
command_args = await async_create_command_cmd_args(hass, auth_data, transport)
async_add_entities(
[
@@ -177,6 +186,7 @@ async def async_setup_platform(
command_payload_off,
vartype,
request_args,
command_args,
)
],
True,
@@ -188,21 +198,22 @@ class SnmpSwitch(SwitchEntity):
def __init__(
self,
name,
host,
port,
baseoid,
commandoid,
payload_on,
payload_off,
command_payload_on,
command_payload_off,
vartype,
request_args,
name: str,
host: str,
port: int,
baseoid: str,
commandoid: str | None,
payload_on: str,
payload_off: str,
command_payload_on: str | None,
command_payload_off: str | None,
vartype: str,
request_args: RequestArgsType,
command_args: CommandArgsType,
) -> None:
"""Initialize the switch."""
self._name = name
self._attr_name = name
self._baseoid = baseoid
self._vartype = vartype
@@ -215,7 +226,8 @@ class SnmpSwitch(SwitchEntity):
self._payload_on = payload_on
self._payload_off = payload_off
self._target = UdpTransportTarget((host, port))
self._request_args: RequestArgsType = request_args
self._request_args = request_args
self._command_args = command_args
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the switch."""
@@ -226,7 +238,7 @@ class SnmpSwitch(SwitchEntity):
"""Turn off the switch."""
await self._execute_command(self._command_payload_off)
async def _execute_command(self, command):
async def _execute_command(self, command: str) -> None:
# User did not set vartype and command is not a digit
if self._vartype == "none" and not self._command_payload_on.isdigit():
await self._set(command)
@@ -265,14 +277,12 @@ class SnmpSwitch(SwitchEntity):
self._state = None
@property
def name(self):
"""Return the switch's name."""
return self._name
@property
def is_on(self):
def is_on(self) -> bool | None:
"""Return true if switch is on; False if off. None if unknown."""
return self._state
async def _set(self, value):
await setCmd(*self._request_args, value)
async def _set(self, value: Any) -> None:
"""Set the state of the switch."""
await setCmd(
*self._command_args, ObjectType(ObjectIdentity(self._commandoid), value)
)
+29 -7
View File
@@ -25,6 +25,14 @@ DATA_SNMP_ENGINE = "snmp_engine"
_LOGGER = logging.getLogger(__name__)
type CommandArgsType = tuple[
SnmpEngine,
UsmUserData | CommunityData,
UdpTransportTarget | Udp6TransportTarget,
ContextData,
]
type RequestArgsType = tuple[
SnmpEngine,
UsmUserData | CommunityData,
@@ -34,20 +42,34 @@ type RequestArgsType = tuple[
]
async def async_create_command_cmd_args(
hass: HomeAssistant,
auth_data: UsmUserData | CommunityData,
target: UdpTransportTarget | Udp6TransportTarget,
) -> CommandArgsType:
"""Create command arguments.
The ObjectType needs to be created dynamically by the caller.
"""
engine = await async_get_snmp_engine(hass)
return (engine, auth_data, target, ContextData())
async def async_create_request_cmd_args(
hass: HomeAssistant,
auth_data: UsmUserData | CommunityData,
target: UdpTransportTarget | Udp6TransportTarget,
object_id: str,
) -> RequestArgsType:
"""Create request arguments."""
return (
await async_get_snmp_engine(hass),
auth_data,
target,
ContextData(),
ObjectType(ObjectIdentity(object_id)),
"""Create request arguments.
The same ObjectType is used for all requests.
"""
engine, auth_data, target, context_data = await async_create_command_cmd_args(
hass, auth_data, target
)
object_type = ObjectType(ObjectIdentity(object_id))
return (engine, auth_data, target, context_data, object_type)
@singleton(DATA_SNMP_ENGINE)
@@ -62,6 +62,7 @@ class SpcAlarm(AlarmControlPanelEntity):
| AlarmControlPanelEntityFeature.ARM_AWAY
| AlarmControlPanelEntityFeature.ARM_NIGHT
)
_attr_code_arm_required = False
def __init__(self, area: Area, api: SpcWebGateway) -> None:
"""Initialize the SPC alarm panel."""
@@ -2,6 +2,7 @@
from __future__ import annotations
from aiohttp import ClientTimeout
from synology_dsm.api.surveillance_station.const import SNAPSHOT_PROFILE_BALANCED
from synology_dsm.exceptions import (
SynologyDSMAPIErrorException,
@@ -40,7 +41,7 @@ DEFAULT_PORT = 5000
DEFAULT_PORT_SSL = 5001
# Options
DEFAULT_SCAN_INTERVAL = 15 # min
DEFAULT_TIMEOUT = 30 # sec
DEFAULT_TIMEOUT = ClientTimeout(total=60, connect=15)
DEFAULT_SNAPSHOT_QUALITY = SNAPSHOT_PROFILE_BALANCED
ENTITY_UNIT_LOAD = "load"
@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
"iot_class": "local_polling",
"loggers": ["synology_dsm"],
"requirements": ["py-synologydsm-api==2.4.2"],
"requirements": ["py-synologydsm-api==2.4.4"],
"ssdp": [
{
"manufacturer": "Synology",
+7 -10
View File
@@ -37,7 +37,6 @@ from .const import (
CONST_MODE_SMART_SCHEDULE,
CONST_OVERLAY_MANUAL,
CONST_OVERLAY_TADO_OPTIONS,
CONST_OVERLAY_TIMER,
DATA,
DOMAIN,
HA_TERMINATION_DURATION,
@@ -65,7 +64,7 @@ from .const import (
TYPE_HEATING,
)
from .entity import TadoZoneEntity
from .helper import decide_overlay_mode
from .helper import decide_duration, decide_overlay_mode
_LOGGER = logging.getLogger(__name__)
@@ -603,14 +602,12 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
overlay_mode=overlay_mode,
zone_id=self.zone_id,
)
# If we ended up with a timer but no duration, set a default duration
if overlay_mode == CONST_OVERLAY_TIMER and duration is None:
duration = (
int(self._tado_zone_data.default_overlay_termination_duration)
if self._tado_zone_data.default_overlay_termination_duration is not None
else 3600
)
duration = decide_duration(
tado=self._tado,
duration=duration,
zone_id=self.zone_id,
overlay_mode=overlay_mode,
)
_LOGGER.debug(
(
"Switching to %s for zone %s (%d) with temperature %s °C and duration"
+2
View File
@@ -212,3 +212,5 @@ SERVICE_ADD_METER_READING = "add_meter_reading"
CONF_CONFIG_ENTRY = "config_entry"
CONF_READING = "reading"
ATTR_MESSAGE = "message"
WATER_HEATER_FALLBACK_REPAIR = "water_heater_fallback"
+20
View File
@@ -29,3 +29,23 @@ def decide_overlay_mode(
)
return overlay_mode
def decide_duration(
tado: TadoConnector,
duration: int | None,
zone_id: int,
overlay_mode: str | None = None,
) -> None | int:
"""Return correct duration based on the selected overlay mode/duration and tado config."""
# If we ended up with a timer but no duration, set a default duration
# If we ended up with a timer but no duration, set a default duration
if overlay_mode == CONST_OVERLAY_TIMER and duration is None:
duration = (
int(tado.data["zone"][zone_id].default_overlay_termination_duration)
if tado.data["zone"][zone_id].default_overlay_termination_duration
is not None
else 3600
)
return duration
+34
View File
@@ -0,0 +1,34 @@
"""Repair implementations."""
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from .const import (
CONST_OVERLAY_MANUAL,
CONST_OVERLAY_TADO_DEFAULT,
DOMAIN,
WATER_HEATER_FALLBACK_REPAIR,
)
def manage_water_heater_fallback_issue(
hass: HomeAssistant,
water_heater_entities: list,
integration_overlay_fallback: str | None,
) -> None:
"""Notify users about water heater respecting fallback setting."""
if (
integration_overlay_fallback
in [CONST_OVERLAY_TADO_DEFAULT, CONST_OVERLAY_MANUAL]
and len(water_heater_entities) > 0
):
for water_heater_entity in water_heater_entities:
ir.async_create_issue(
hass=hass,
domain=DOMAIN,
issue_id=f"{WATER_HEATER_FALLBACK_REPAIR}_{water_heater_entity.zone_name}",
is_fixable=False,
is_persistent=False,
severity=ir.IssueSeverity.WARNING,
translation_key=WATER_HEATER_FALLBACK_REPAIR,
)
@@ -165,6 +165,10 @@
"import_failed_invalid_auth": {
"title": "Failed to import, invalid credentials",
"description": "Failed to import the configuration for the Tado Device Tracker, due to invalid credentials. Please fix the YAML configuration and restart Home Assistant. Alternatively you can use the UI to configure Tado. Don't forget to delete the YAML configuration, once the import is successful."
},
"water_heater_fallback": {
"title": "Tado Water Heater entities now support fallback options",
"description": "Due to added support for water heaters entities, these entities may use different overlay. Please configure integration entity and tado app water heater zone overlay options."
}
}
}
+14 -2
View File
@@ -32,7 +32,8 @@ from .const import (
TYPE_HOT_WATER,
)
from .entity import TadoZoneEntity
from .helper import decide_overlay_mode
from .helper import decide_duration, decide_overlay_mode
from .repairs import manage_water_heater_fallback_issue
_LOGGER = logging.getLogger(__name__)
@@ -80,6 +81,12 @@ async def async_setup_entry(
async_add_entities(entities, True)
manage_water_heater_fallback_issue(
hass=hass,
water_heater_entities=entities,
integration_overlay_fallback=tado.fallback,
)
def _generate_entities(tado: TadoConnector) -> list[WaterHeaterEntity]:
"""Create all water heater entities."""
@@ -283,7 +290,12 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity):
duration=duration,
zone_id=self.zone_id,
)
duration = decide_duration(
tado=self._tado,
duration=duration,
zone_id=self.zone_id,
overlay_mode=overlay_mode,
)
_LOGGER.debug(
"Switching to %s for zone %s (%d) with temperature %s",
self._current_tado_hvac_mode,
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/thethingsnetwork",
"integration_type": "hub",
"iot_class": "cloud_polling",
"requirements": ["ttn_client==0.0.4"]
"requirements": ["ttn_client==1.0.0"]
}
+7 -1
View File
@@ -50,7 +50,13 @@ class TibberNotificationService(BaseNotificationService):
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
"""Send a message to Tibber devices."""
migrate_notify_issue(self.hass, TIBBER_DOMAIN, "Tibber", "2024.12.0")
migrate_notify_issue(
self.hass,
TIBBER_DOMAIN,
"Tibber",
"2024.12.0",
service_name=self._service_name,
)
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
try:
await self._notify(title=title, message=message)
+10 -10
View File
@@ -4,7 +4,6 @@ from __future__ import annotations
from homeassistant.core import HomeAssistant
from homeassistant.helpers import intent
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent
from . import DOMAIN, TodoItem, TodoItemStatus, TodoListEntity
@@ -22,7 +21,7 @@ class ListAddItemIntent(intent.IntentHandler):
intent_type = INTENT_LIST_ADD_ITEM
description = "Add item to a todo list"
slot_schema = {"item": cv.string, "name": cv.string}
slot_schema = {"item": intent.non_empty_string, "name": intent.non_empty_string}
platforms = {DOMAIN}
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
@@ -37,18 +36,19 @@ class ListAddItemIntent(intent.IntentHandler):
target_list: TodoListEntity | None = None
# Find matching list
for list_state in intent.async_match_states(
hass, name=list_name, domains=[DOMAIN]
):
target_list = component.get_entity(list_state.entity_id)
if target_list is not None:
break
match_constraints = intent.MatchTargetsConstraints(
name=list_name, domains=[DOMAIN], assistant=intent_obj.assistant
)
match_result = intent.async_match_targets(hass, match_constraints)
if not match_result.is_match:
raise intent.MatchFailedError(
result=match_result, constraints=match_constraints
)
target_list = component.get_entity(match_result.states[0].entity_id)
if target_list is None:
raise intent.IntentHandleError(f"No to-do list: {list_name}")
assert target_list is not None
# Add to list
await target_list.async_create_todo_item(
TodoItem(summary=item, status=TodoItemStatus.NEEDS_ACTION)
@@ -88,6 +88,7 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity):
"""Tuya Alarm Entity."""
_attr_name = None
_attr_code_arm_required = False
def __init__(
self,

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